├── images ├── balance.png ├── emailNotif.png ├── slackNotif.png ├── architecture.png └── mysqlservice.png ├── containers ├── send-notification │ ├── Dockerfile │ ├── .gitignore │ ├── src │ │ └── main │ │ │ ├── java │ │ │ └── com │ │ │ │ └── example │ │ │ │ ├── EmailOfficeSpaceApplication.java │ │ │ │ └── controller │ │ │ │ └── TriggerEmail.java │ │ │ └── resources │ │ │ └── application.properties │ └── pom.xml ├── account-summary │ ├── tests │ │ ├── Dockerfile │ │ ├── tests.sh │ │ └── render.js │ ├── Dockerfile │ ├── package.json │ ├── views │ │ ├── stylesheets │ │ │ └── style.css │ │ ├── index.html │ │ └── app.js │ ├── .vscode │ │ └── launch.json │ ├── docker-compose.test.yml │ └── server.js ├── transaction-generator │ ├── Dockerfile │ └── transaction-generator.py └── compute-interest-api │ ├── Dockerfile │ ├── .gitignore │ ├── src │ └── main │ │ ├── java │ │ └── officespace │ │ │ ├── Application.java │ │ │ ├── models │ │ │ ├── Transaction.java │ │ │ ├── AccountDao.java │ │ │ └── Account.java │ │ │ └── controllers │ │ │ └── MainController.java │ │ └── resources │ │ └── application.properties │ ├── initialize_db.sql │ ├── custom-entrypoint.sh │ ├── pom.xml │ └── wait-for-it.sh ├── secrets.yaml ├── tests ├── test-shellcheck.sh ├── test-yamllint.sh ├── test-kubernetes.sh └── test-minikube.sh ├── scripts ├── bx_auth.sh ├── create-secrets.sh ├── install_bx.sh └── deploy-to-bluemix │ ├── bx_login.sh │ ├── install_bx.sh │ └── deploy.sh ├── .travis.yml ├── CONTRIBUTING.md ├── transaction-generator.yaml ├── account-database.yaml ├── .yamllint.yml ├── send-notification.yaml ├── compute-interest-api.yaml ├── account-summary.yaml ├── sendEmail.js ├── .bluemix ├── toolchain.yml ├── pipeline.yml ├── icon.svg ├── deploy.json └── toolchain.svg ├── MAINTAINERS.md ├── sendSlack.js ├── LICENSE ├── README-cn.md ├── README-ko.md └── README.md /images/balance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/spring-boot-microservices-on-kubernetes/HEAD/images/balance.png -------------------------------------------------------------------------------- /images/emailNotif.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/spring-boot-microservices-on-kubernetes/HEAD/images/emailNotif.png -------------------------------------------------------------------------------- /images/slackNotif.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/spring-boot-microservices-on-kubernetes/HEAD/images/slackNotif.png -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/spring-boot-microservices-on-kubernetes/HEAD/images/architecture.png -------------------------------------------------------------------------------- /images/mysqlservice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/spring-boot-microservices-on-kubernetes/HEAD/images/mysqlservice.png -------------------------------------------------------------------------------- /containers/send-notification/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:3.3.9-jdk-8-alpine 2 | COPY . /app 3 | WORKDIR /app 4 | CMD java -jar target/*.jar 5 | -------------------------------------------------------------------------------- /containers/account-summary/tests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | RUN npm install -g phantomjs 3 | ADD . /app 4 | WORKDIR /app 5 | CMD ["/app/tests.sh"] 6 | -------------------------------------------------------------------------------- /containers/transaction-generator/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7.13 2 | ADD transaction-generator.py . 3 | RUN pip install requests 4 | CMD python transaction-generator.py 5 | -------------------------------------------------------------------------------- /containers/compute-interest-api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:3.3.9-jdk-8-alpine 2 | COPY . /app 3 | WORKDIR /app 4 | RUN apk update && apk add mysql mysql-client 5 | ENTRYPOINT ["/app/custom-entrypoint.sh"] 6 | CMD java -jar target/*.jar 7 | -------------------------------------------------------------------------------- /secrets.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: demo-credentials 6 | type: Opaque 7 | data: 8 | username: bWljaGFlbGJvbHRvbg== 9 | password: cGFzc3dvcmQ= 10 | host: YWNjb3VudC1kYXRhYmFzZQ== 11 | port: MzMwNg== 12 | -------------------------------------------------------------------------------- /tests/test-shellcheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if find . -name '*.sh' -print0 | xargs -n1 -0 shellcheck -x -s bash; then 4 | echo -e "\033[0;32mShell script linting passed!\033[0m" 5 | else 6 | echo -e >&2 "\033[0;31mShell script linting failed!\033[0m" 7 | exit 1 8 | fi 9 | -------------------------------------------------------------------------------- /tests/test-yamllint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if find . \( -name '*.yml' -o -name '*.yaml' \) -print0 | xargs -n1 -0 yamllint -c .yamllint.yml; then 4 | echo -e "\033[0;32mYAML linting passed!\033[0m" 5 | else 6 | echo -e >&2 "\033[0;31mYAML linting failed!\033[0m" 7 | exit 1 8 | fi 9 | -------------------------------------------------------------------------------- /containers/compute-interest-api/.gitignore: -------------------------------------------------------------------------------- 1 | # directories 2 | .settings/ 3 | .target/ 4 | .idea/ 5 | .worksheet/ 6 | bin/ 7 | build/ 8 | dist/ 9 | logs/ 10 | node_modules/ 11 | out/ 12 | target/ 13 | tmp/ 14 | 15 | # files 16 | *.iml 17 | */.DS_Store 18 | \#* 19 | .cache 20 | .classpath 21 | .DS_Store 22 | .history 23 | .idea 24 | .idea_modules 25 | .project 26 | RUNNING_PID 27 | -------------------------------------------------------------------------------- /containers/account-summary/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:5.11.0-slim 2 | 3 | WORKDIR /app 4 | 5 | RUN npm install -g nodemon 6 | ADD package.json /app/package.json 7 | RUN npm config set registry http://registry.npmjs.org 8 | RUN npm install && npm ls 9 | RUN mv /app/node_modules /node_modules 10 | 11 | ADD . /app 12 | 13 | EXPOSE 80 14 | ENV PORT 80 15 | 16 | CMD ["node", "server.js"] 17 | -------------------------------------------------------------------------------- /containers/send-notification/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ -------------------------------------------------------------------------------- /containers/compute-interest-api/src/main/java/officespace/Application.java: -------------------------------------------------------------------------------- 1 | package officespace; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /containers/compute-interest-api/initialize_db.sql: -------------------------------------------------------------------------------- 1 | DROP DATABASE IF EXISTS dockercon2017; 2 | CREATE DATABASE IF NOT EXISTS dockercon2017; 3 | USE `dockercon2017`; 4 | 5 | CREATE TABLE `account` ( 6 | `id` bigint(20) NOT NULL AUTO_INCREMENT, 7 | `balance` double(20,2) NOT NULL, 8 | PRIMARY KEY (`id`) 9 | ) ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=latin1; 10 | 11 | INSERT INTO account (id, balance) VALUES(12345, 0); 12 | -------------------------------------------------------------------------------- /containers/send-notification/src/main/java/com/example/EmailOfficeSpaceApplication.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class EmailOfficeSpaceApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(EmailOfficeSpaceApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /containers/account-summary/tests/tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | while ! timeout 1 bash -c "echo > /dev/tcp/vote/80"; do sleep 1; done 3 | curl -sS -X POST --data "vote=b" http://vote > /dev/null 4 | sleep 10 5 | if phantomjs render.js http://result | grep -q '1 vote'; then 6 | echo -e "\e[42m------------" 7 | echo -e "\e[92mTests passed" 8 | echo -e "\e[42m------------" 9 | exit 0 10 | fi 11 | echo -e "\e[41m------------" 12 | echo -e "\e[91mTests failed" 13 | echo -e "\e[41m------------" 14 | exit 1 15 | -------------------------------------------------------------------------------- /scripts/bx_auth.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | BLUEMIX_ORG="Developer Advocacy" 4 | BLUEMIX_SPACE="dev" 5 | 6 | if [[ "$TRAVIS_PULL_REQUEST" != "false" ]]; then 7 | echo -e "\033[0;33mPull Request detected; not authenticating to Bluemix.\033[0m" 8 | exit 0 9 | fi 10 | 11 | echo "Authenticating to Bluemix" 12 | bx login -a https://api.ng.bluemix.net 13 | 14 | echo "Targeting Bluemix org and space" 15 | bx target -o "$BLUEMIX_ORG" -s "$BLUEMIX_SPACE" 16 | 17 | echo "Initializing Bluemix Container Service" 18 | bx cs init 19 | -------------------------------------------------------------------------------- /containers/compute-interest-api/custom-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -z "$MYSQL_DB_USER" ] || [ -z "$MYSQL_DB_PASSWORD" ] || [ -z "$MYSQL_DB_HOST" ] || [ -z "$MYSQL_DB_PORT" ] 3 | then 4 | echo "Environment variables not found. Using default credentials (user michaelbolton, password password, host account-database, port 3306) instead if it has been setup." 5 | else 6 | mysql -u "$MYSQL_DB_USER" --password="$MYSQL_DB_PASSWORD" --host "$MYSQL_DB_HOST" --port "$MYSQL_DB_PORT" < initialize_db.sql 7 | fi 8 | 9 | exec "$@" 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: python 3 | python: 2.7 4 | group: stable 5 | os: linux 6 | 7 | services: 8 | - docker 9 | 10 | env: 11 | - TEST_RUN="./tests/test-minikube.sh" 12 | - TEST_RUN="./tests/test-kubernetes.sh" 13 | 14 | before_install: 15 | - sudo apt-get install shellcheck 16 | - pip install yamllint 17 | 18 | install: 19 | - "./scripts/install_bx.sh" 20 | - "./scripts/bx_auth.sh" 21 | 22 | before_script: 23 | - "./tests/test-shellcheck.sh" 24 | - "./tests/test-yamllint.sh" 25 | 26 | script: "$TEST_RUN" 27 | -------------------------------------------------------------------------------- /containers/account-summary/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "result", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "body-parser": "^1.14.1", 13 | "cookie-parser": "^1.4.0", 14 | "express": "^4.13.3", 15 | "method-override": "^2.3.5", 16 | "async": "^1.5.0", 17 | "mysql": "2.16.0", 18 | "socket.io": "^2.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /containers/compute-interest-api/src/main/java/officespace/models/Transaction.java: -------------------------------------------------------------------------------- 1 | package officespace.models; 2 | 3 | public class Transaction { 4 | 5 | double amount; 6 | 7 | double interestRate; 8 | 9 | public double getAmount() { 10 | return amount; 11 | } 12 | public void setAmount(double amount) { 13 | this.amount = amount; 14 | } 15 | public double getInterestRate() { 16 | return interestRate; 17 | } 18 | public void setInterestRate(double interestRate) { 19 | this.interestRate = interestRate; 20 | } 21 | 22 | } 23 | 24 | -------------------------------------------------------------------------------- /containers/account-summary/views/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,600); 2 | 3 | *{ 4 | box-sizing:border-box; 5 | } 6 | html,body{ 7 | margin:0; 8 | padding:0; 9 | height:100%; 10 | font-family: 'Open Sans'; 11 | } 12 | body{ 13 | opacity:0; 14 | transition: all 1s linear; 15 | } 16 | 17 | #accountSummary{ 18 | position: absolute; 19 | top: 50%; 20 | left: 50%; 21 | transform: translate(-50%, -50%); 22 | } 23 | 24 | #balance{ 25 | color: green; 26 | text-align: center; 27 | font-size: 4em; 28 | } 29 | 30 | #accountHeader{ 31 | font-size: 2em; 32 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This is an open source project, and we appreciate your help! 4 | 5 | We use the GitHub issue tracker to discuss new features and non-trivial bugs. 6 | 7 | In addition to the issue tracker, [#journeys on 8 | Slack](https://dwopen.slack.com) is the best way to get into contact with the 9 | project's maintainers. 10 | 11 | To contribute code, documentation, or tests, please submit a pull request to 12 | the GitHub repository. Generally, we expect two maintainers to review your pull 13 | request before it is approved for merging. For more details, see the 14 | [MAINTAINERS](MAINTAINERS.md) page. 15 | -------------------------------------------------------------------------------- /scripts/create-secrets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Enter MySQL username: " 4 | read -r username 5 | echo "Enter MySQL password: " 6 | read -r password 7 | echo "Enter MySQL host: " 8 | read -r host 9 | echo "Enter MySQL port: " 10 | read -r port 11 | 12 | ENC_username=$(echo "$username" | tr -d '\n' | base64) 13 | ENC_password=$(echo "$password" | tr -d '\n' | base64) 14 | ENC_host=$(echo "$host" | tr -d '\n' | base64) 15 | ENC_port=$(echo "$port" | tr -d '\n' | base64) 16 | 17 | cat < Bluemix_CLI.tar.gz 10 | tar -xvf Bluemix_CLI.tar.gz 11 | sudo ./Bluemix_CLI/install_bluemix_cli 12 | 13 | echo "Configuring bx to disable version check" 14 | bx config --check-version=false 15 | echo "Checking bx version" 16 | bx --version 17 | echo "Installing Bluemix container-service plugin" 18 | bx plugin install container-service -r Bluemix 19 | 20 | echo "Installing kubectl" 21 | curl -LO https://storage.googleapis.com/kubernetes-release/release/"$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)"/bin/linux/amd64/kubectl 22 | chmod 0755 kubectl 23 | sudo mv kubectl /usr/local/bin 24 | -------------------------------------------------------------------------------- /containers/compute-interest-api/src/main/java/officespace/models/AccountDao.java: -------------------------------------------------------------------------------- 1 | package officespace.models; 2 | 3 | import javax.transaction.Transactional; 4 | 5 | import org.springframework.data.repository.CrudRepository; 6 | 7 | /** 8 | * A DAO for the entity Account is simply created by extending the CrudRepository 9 | * interface provided by spring. The following methods are some of the ones 10 | * available from such interface: save, delete, deleteAll, findOne and findAll. 11 | * The magic is that such methods must not be implemented, and moreover it is 12 | * possible create new query methods working only by defining their signature! 13 | * 14 | * @author John Zaccone 15 | */ 16 | @Transactional 17 | public interface AccountDao extends CrudRepository { 18 | 19 | /** 20 | * Return the account having the id or null if no user is found. 21 | * 22 | * @param id the account id. 23 | */ 24 | public Account findById(long id); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /containers/account-summary/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bank Account Balance 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | Account Balance 17 |
18 |
19 | {{total | currency}} 20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /scripts/deploy-to-bluemix/bx_login.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [[ -z $CF_ORG ]]; then 4 | CF_ORG="$BLUEMIX_ORG" 5 | fi 6 | if [[ -z $CF_SPACE ]]; then 7 | CF_SPACE="$BLUEMIX_SPACE" 8 | fi 9 | 10 | 11 | if ([ -z "$BLUEMIX_USER" ] || [ -z "$BLUEMIX_PASSWORD" ] || [ -z "$BLUEMIX_ACCOUNT" ]) && [[ -z "$API_KEY" ]] 12 | then 13 | echo "Define BLUEMIX_USER, BLUEMIX PASSWORD and BLUEMIX_ACCOUNT environment variables or just use the API_KEY environment variable." 14 | exit 1 15 | fi 16 | echo "Deploy pods" 17 | 18 | echo "bx login -a $CF_TARGET_URL" 19 | 20 | if [[ -z "$API_KEY" ]]; then 21 | bx login -a "$CF_TARGET_URL" -u "$BLUEMIX_USER" -p "$BLUEMIX_PASSWORD" -c "$BLUEMIX_ACCOUNT" -o "$CF_ORG" -s "$CF_SPACE" 22 | else 23 | bx login -a "$CF_TARGET_URL" --apikey "$API_KEY" -o "$CF_ORG" -s "$CF_SPACE" 24 | fi 25 | 26 | # Init container clusters 27 | echo "bx cs init" 28 | if bx cs init -ne 0 29 | then 30 | echo "Failed to initialize to Bluemix Container Service" 31 | exit 1 32 | fi 33 | -------------------------------------------------------------------------------- /scripts/deploy-to-bluemix/install_bx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Download Bluemix CLI" 4 | wget --quiet --output-document=/tmp/Bluemix_CLI_amd64.tar.gz http://public.dhe.ibm.com/cloud/bluemix/cli/bluemix-cli/latest/Bluemix_CLI_amd64.tar.gz 5 | tar -xf /tmp/Bluemix_CLI_amd64.tar.gz --directory=/tmp 6 | 7 | # Create bx alias 8 | echo "#!/bin/sh" >/tmp/Bluemix_CLI/bin/bx 9 | echo "/tmp/Bluemix_CLI/bin/bluemix \"\$@\" " >>/tmp/Bluemix_CLI/bin/bx 10 | chmod +x /tmp/Bluemix_CLI/bin/* 11 | 12 | export PATH="/tmp/Bluemix_CLI/bin:$PATH" 13 | 14 | # Install Armada CS plugin 15 | echo "Install the Bluemix container-service plugin" 16 | bx plugin install container-service -r Bluemix 17 | 18 | echo "Install kubectl" 19 | wget --quiet --output-document=/tmp/Bluemix_CLI/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/"$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)"/bin/linux/amd64/kubectl 20 | chmod +x /tmp/Bluemix_CLI/bin/kubectl 21 | 22 | if [ -n "$DEBUG" ]; then 23 | bx --version 24 | bx plugin list 25 | fi 26 | -------------------------------------------------------------------------------- /containers/account-summary/tests/render.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 IBM Corp. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | var system = require('system'); 16 | var page = require('webpage').create(); 17 | var url = system.args[1]; 18 | 19 | page.onLoadFinished = function() { 20 | setTimeout(function(){ 21 | console.log(page.content); 22 | phantom.exit(); 23 | }, 1000); 24 | }; 25 | 26 | page.open(url, function() { 27 | page.evaluate(function() { 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /containers/account-summary/docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '2' 3 | 4 | services: 5 | 6 | sut: 7 | build: ./tests/ 8 | depends_on: 9 | - vote 10 | - result 11 | - worker 12 | networks: 13 | - front-tier 14 | 15 | vote: 16 | build: ../vote/ 17 | ports: ["80"] 18 | depends_on: 19 | - redis 20 | - db 21 | networks: 22 | - front-tier 23 | - back-tier 24 | 25 | result: 26 | build: . 27 | ports: ["80"] 28 | depends_on: 29 | - redis 30 | - db 31 | networks: 32 | - front-tier 33 | - back-tier 34 | 35 | worker: 36 | build: ../worker/ 37 | depends_on: 38 | - redis 39 | - db 40 | networks: 41 | - back-tier 42 | 43 | redis: 44 | image: redis:alpine 45 | ports: ["6379"] 46 | networks: 47 | - back-tier 48 | 49 | db: 50 | image: postgres:9.4 51 | volumes: 52 | - "db-data:/var/lib/postgresql/data" 53 | networks: 54 | - back-tier 55 | 56 | volumes: 57 | db-data: 58 | 59 | networks: 60 | front-tier: 61 | back-tier: 62 | -------------------------------------------------------------------------------- /account-database.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: account-database 6 | labels: 7 | app: office-space 8 | spec: 9 | ports: 10 | - port: 3306 11 | protocol: TCP 12 | targetPort: 3306 13 | selector: 14 | app: office-space 15 | tier: database 16 | --- 17 | apiVersion: extensions/v1beta1 18 | kind: Deployment 19 | metadata: 20 | name: account-database 21 | labels: 22 | app: office-space 23 | spec: 24 | strategy: 25 | type: Recreate 26 | template: 27 | metadata: 28 | labels: 29 | app: office-space 30 | tier: database 31 | spec: 32 | containers: 33 | - image: mysql:5.6 34 | name: account-database 35 | env: 36 | - name: MYSQL_USER 37 | value: michaelbolton 38 | - name: MYSQL_PASSWORD 39 | value: password 40 | - name: MYSQL_ROOT_PASSWORD 41 | value: password 42 | - name: MYSQL_DATABASE 43 | value: dockercon2017 44 | 45 | ports: 46 | - containerPort: 3306 47 | name: db 48 | -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | rules: 3 | braces: 4 | min-spaces-inside: 0 5 | max-spaces-inside: 0 6 | min-spaces-inside-empty: 0 7 | max-spaces-inside-empty: 0 8 | brackets: 9 | min-spaces-inside: 0 10 | max-spaces-inside: 0 11 | min-spaces-inside-empty: 0 12 | max-spaces-inside-empty: 0 13 | colons: 14 | max-spaces-before: 0 15 | max-spaces-after: 1 16 | commas: 17 | max-spaces-before: 0 18 | min-spaces-after: 1 19 | max-spaces-after: 1 20 | comments: 21 | require-starting-space: true 22 | min-spaces-from-content: 2 23 | comments-indentation: enable 24 | document-end: disable 25 | document-start: 26 | present: true 27 | empty-lines: 28 | max: 2 29 | max-start: 0 30 | max-end: 0 31 | hyphens: 32 | max-spaces-after: 1 33 | indentation: 34 | spaces: consistent 35 | indent-sequences: true 36 | check-multi-line-strings: false 37 | key-duplicates: enable 38 | line-length: 39 | max: 80 40 | allow-non-breakable-words: true 41 | allow-non-breakable-inline-mappings: true 42 | level: warning 43 | new-line-at-end-of-file: enable 44 | new-lines: 45 | type: unix 46 | trailing-spaces: enable 47 | truthy: enable 48 | -------------------------------------------------------------------------------- /containers/account-summary/views/app.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 IBM Corp. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | var app = angular.module('accountSummary', []); 16 | var socket = io.connect({transports:['polling']}); 17 | 18 | app.controller('accountBalanceCtrl', function($scope){ 19 | 20 | var updateAccountSummary = function(){ 21 | socket.on('account', function (json) { 22 | data = JSON.parse(json); 23 | $scope.$apply(function () { 24 | $scope.total = data.balance; 25 | }); 26 | }); 27 | }; 28 | 29 | var init = function(){ 30 | document.body.style.opacity=1; 31 | updateAccountSummary(); 32 | }; 33 | socket.on('message',function(data){ 34 | init(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /send-notification.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: send-notification 6 | labels: 7 | app: office-space 8 | spec: 9 | ports: 10 | - port: 8080 11 | protocol: TCP 12 | targetPort: 8080 13 | selector: 14 | app: office-space 15 | tier: notification 16 | --- 17 | apiVersion: extensions/v1beta1 18 | kind: Deployment 19 | metadata: 20 | name: send-notification 21 | labels: 22 | app: office-space 23 | spec: 24 | strategy: 25 | type: Recreate 26 | template: 27 | metadata: 28 | labels: 29 | app: office-space 30 | tier: notification 31 | spec: 32 | containers: 33 | - image: anthonyamanse/send-notification:v1 34 | imagePullPolicy: Always 35 | name: send-notification 36 | env: 37 | - name: GMAIL_SENDER_USER 38 | value: 'username@gmail.com' 39 | - name: GMAIL_SENDER_PASSWORD 40 | value: 'password@gmail.com' 41 | - name: EMAIL_RECEIVER 42 | value: 'sendTo@gmail.com' 43 | - name: OPENWHISK_API_URL_SLACK 44 | value: '' 45 | - name: NOTIFICATION_MESSAGE 46 | value: 'Your balance is over $50,000.00' 47 | - name: OPENWHISK_API_URL_EMAIL 48 | value: '' 49 | ports: 50 | - containerPort: 8080 51 | -------------------------------------------------------------------------------- /containers/transaction-generator/transaction-generator.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 IBM Corp. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import requests 16 | import json 17 | import time 18 | import random 19 | 20 | #Wait for the webserver to start 21 | print "sleeping for 25 to wait for webserver" 22 | time.sleep(25) 23 | 24 | while True: 25 | 26 | transactionAmount = round(random.uniform(1000, 100000),2) 27 | interestRate = .04232 #random.uniform(.04634) 28 | 29 | print "Computing interest for transaction with amount: " + str(transactionAmount) + " and interestRate: " + str(interestRate) 30 | transaction = {'amount': str(transactionAmount), 'interestRate': str(interestRate)} 31 | headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} 32 | response = requests.post("http://compute-interest-api:8080/computeinterest", data=json.dumps(transaction), headers=headers) 33 | print response.text 34 | time.sleep(1) 35 | -------------------------------------------------------------------------------- /compute-interest-api.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: compute-interest-api 6 | labels: 7 | app: office-space 8 | spec: 9 | ports: 10 | - port: 8080 11 | protocol: TCP 12 | targetPort: 8080 13 | selector: 14 | app: office-space 15 | tier: compute 16 | --- 17 | apiVersion: extensions/v1beta1 18 | kind: Deployment 19 | metadata: 20 | name: compute-interest-api 21 | labels: 22 | app: office-space 23 | spec: 24 | strategy: 25 | type: Recreate 26 | template: 27 | metadata: 28 | labels: 29 | app: office-space 30 | tier: compute 31 | spec: 32 | containers: 33 | - image: anthonyamanse/compute-interest-api:v1 34 | imagePullPolicy: Always 35 | name: compute-interest-api 36 | env: 37 | - name: MYSQL_DB_USER 38 | valueFrom: 39 | secretKeyRef: 40 | name: demo-credentials 41 | key: username 42 | - name: MYSQL_DB_PASSWORD 43 | valueFrom: 44 | secretKeyRef: 45 | name: demo-credentials 46 | key: password 47 | - name: MYSQL_DB_HOST 48 | valueFrom: 49 | secretKeyRef: 50 | name: demo-credentials 51 | key: host 52 | - name: MYSQL_DB_PORT 53 | valueFrom: 54 | secretKeyRef: 55 | name: demo-credentials 56 | key: port 57 | ports: 58 | - containerPort: 8080 59 | -------------------------------------------------------------------------------- /containers/compute-interest-api/src/main/java/officespace/models/Account.java: -------------------------------------------------------------------------------- 1 | package officespace.models; 2 | 3 | import javax.persistence.Entity; 4 | import javax.persistence.GeneratedValue; 5 | import javax.persistence.GenerationType; 6 | import javax.persistence.Id; 7 | import javax.persistence.Table; 8 | import javax.validation.constraints.NotNull; 9 | 10 | /** 11 | * An entity Account composed by two fields (id, balance). The Entity 12 | * annotation indicates that this class is a JPA entity. The Table annotation 13 | * specifies the name for the table in the db. 14 | * 15 | * @author John Zaccone 16 | */ 17 | @Entity 18 | @Table(name = "account") 19 | public class Account { 20 | 21 | // ------------------------ 22 | // PRIVATE FIELDS 23 | // ------------------------ 24 | 25 | // An autogenerated id (unique for each account in the db) 26 | @Id 27 | @GeneratedValue(strategy = GenerationType.AUTO) 28 | private long id; 29 | 30 | // The account balance 31 | @NotNull 32 | private double balance; 33 | 34 | // ------------------------ 35 | // PUBLIC METHODS 36 | // ------------------------ 37 | 38 | public Account() { 39 | } 40 | 41 | public Account(long id) { 42 | this.id = id; 43 | } 44 | 45 | public Account(double balance) { 46 | this.balance = balance; 47 | } 48 | 49 | // Getter and setter methods 50 | public long getId() { 51 | return id; 52 | } 53 | 54 | public void setId(long value) { 55 | this.id = value; 56 | } 57 | 58 | public double getBalance() { 59 | return balance; 60 | } 61 | 62 | public void setBalance(double balance) { 63 | this.balance = balance; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /account-summary.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: account-summary 6 | labels: 7 | app: office-space 8 | spec: 9 | type: NodePort 10 | ports: 11 | - port: 80 12 | protocol: TCP 13 | targetPort: 80 14 | nodePort: 30080 15 | selector: 16 | app: office-space 17 | tier: summary 18 | --- 19 | apiVersion: extensions/v1beta1 20 | kind: Deployment 21 | metadata: 22 | name: account-summary 23 | labels: 24 | app: office-space 25 | spec: 26 | strategy: 27 | type: Recreate 28 | template: 29 | metadata: 30 | labels: 31 | app: office-space 32 | tier: summary 33 | spec: 34 | containers: 35 | - image: anthonyamanse/account-summary:secrets 36 | imagePullPolicy: Always 37 | name: account-summary 38 | env: 39 | - name: MYSQL_DB_USER 40 | valueFrom: 41 | secretKeyRef: 42 | name: demo-credentials 43 | key: username 44 | - name: MYSQL_DB_PASSWORD 45 | valueFrom: 46 | secretKeyRef: 47 | name: demo-credentials 48 | key: password 49 | - name: MYSQL_DB_HOST 50 | valueFrom: 51 | secretKeyRef: 52 | name: demo-credentials 53 | key: host 54 | - name: MYSQL_DB_PORT 55 | valueFrom: 56 | secretKeyRef: 57 | name: demo-credentials 58 | key: port 59 | ports: 60 | - containerPort: 80 61 | name: web 62 | -------------------------------------------------------------------------------- /containers/compute-interest-api/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # =============================== 2 | # = DATA SOURCE 3 | # =============================== 4 | 5 | # Set here configurations for the database connection 6 | 7 | # Connection url for the database "account-database" 8 | spring.datasource.url = jdbc:mysql://${MYSQL_DB_HOST}:${MYSQL_DB_PORT}/dockercon2017 9 | 10 | # Username and password 11 | spring.datasource.username = ${MYSQL_DB_USER} 12 | spring.datasource.password = ${MYSQL_DB_PASSWORD} 13 | 14 | # Keep the connection alive if idle for a long time (needed in production) 15 | spring.datasource.testWhileIdle = true 16 | spring.datasource.validationQuery = SELECT 1 17 | 18 | spring.datasource.tomcat.max-wait=60000 19 | 20 | # =============================== 21 | # = JPA / HIBERNATE 22 | # =============================== 23 | 24 | # Use spring.jpa.properties.* for Hibernate native properties (the prefix is 25 | # stripped before adding them to the entity manager). 26 | 27 | # Show or not log for each sql query 28 | spring.jpa.show-sql = true 29 | 30 | # Hibernate ddl auto (create, create-drop, update): with "update" the database 31 | # schema will be automatically updated accordingly to java entities found in 32 | # the project 33 | spring.jpa.hibernate.ddl-auto = update 34 | 35 | # Naming strategy 36 | spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.ImprovedNamingStrategy 37 | 38 | # Allows Hibernate to generate SQL optimized for a particular DBMS 39 | spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect 40 | 41 | # Secret to share between client and server when enabling remote live restarting or debugging. DO NOT USE IN PRODUCTION 42 | spring.devtools.remote.secret=thisismysecret 43 | -------------------------------------------------------------------------------- /sendEmail.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 IBM Corp. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | var nodemailer = require('nodemailer'); 17 | 18 | function main(params) { 19 | 20 | 21 | 22 | // create transporter object using the default SMTP transport 23 | let transporter = nodemailer.createTransport({ 24 | service: 'gmail', 25 | auth: { 26 | user: params.sender, 27 | pass: params.password 28 | } 29 | }); 30 | console.log('SMTP Configured??'); 31 | 32 | // setup email data 33 | let mailOptions = { 34 | from: '"' + params.sender + '" <' + params.sender + '>', // sender address 35 | to: params.receiver, // list of receivers 36 | subject: params.subject, // Subject line 37 | text: params.text, // plain text body 38 | }; 39 | console.log("Mail configured"); 40 | 41 | 42 | //send mail 43 | var promise = new Promise(function (resolve, reject) { 44 | transporter.sendMail(mailOptions, (error, info) => { 45 | if (error) { 46 | console.log(error); 47 | reject(error); 48 | } 49 | else { 50 | console.log('Message %s sent: %s', info.messageId, info.response); 51 | resolve(info); 52 | } 53 | }); 54 | }); 55 | return promise; 56 | 57 | } 58 | -------------------------------------------------------------------------------- /containers/send-notification/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example 7 | email-office-space 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | email-office-space 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.1.3.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-mail 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-web 35 | 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-test 40 | test 41 | 42 | 43 | 44 | 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-maven-plugin 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /containers/compute-interest-api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | officespace 7 | spring-boot-mysql-springdatajpa-hibernate 8 | 0.0.1-SNAPSHOT 9 | 10 | spring-boot-mysql-springdatajpa-hibernate 11 | Use Spring Data JPA + Hibernate + MySQL in Spring Boot 12 | 13 | 14 | org.springframework.boot 15 | spring-boot-starter-parent 16 | 2.1.3.RELEASE 17 | 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-web 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-data-jpa 28 | 29 | 30 | mysql 31 | mysql-connector-java 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-devtools 36 | 37 | 38 | 39 | 40 | UTF-8 41 | officespace.Application 42 | 1.8 43 | 44 | 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-maven-plugin 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /.bluemix/toolchain.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Deploy Kubernetes Guestbook sample to Bluemix" 3 | description: "Toolchain to deploy Kubernetes Guestbook sample to Bluemix" 4 | version: 0.1 5 | image: data:image/svg+xml;base64,$file(toolchain.svg,base64) 6 | icon: data:image/svg+xml;base64,$file(icon.svg,base64) 7 | required: 8 | - deploy 9 | - sample-repo 10 | 11 | # Github repos 12 | sample-repo: 13 | service_id: githubpublic 14 | parameters: 15 | repo_name: "{{name}}" 16 | repo_url: >- 17 | https://github.com/IBM/spring-boot-microservices-on-kubernetes.git 18 | type: clone 19 | has_issues: false 20 | 21 | # Pipelines 22 | sample-build: 23 | service_id: pipeline 24 | parameters: 25 | name: "{{name}}" 26 | ui-pipeline: true 27 | configuration: 28 | content: $file(pipeline.yml) 29 | env: 30 | SAMPLE_REPO: "sample-repo" 31 | CF_APP_NAME: "pipeline" 32 | PROD_SPACE_NAME: "{{deploy.parameters.prod-space}}" 33 | PROD_ORG_NAME: "{{deploy.parameters.prod-organization}}" 34 | PROD_REGION_ID: "{{deploy.parameters.prod-region}}" 35 | BLUEMIX_USER: "{{deploy.parameters.bluemix-user}}" 36 | BLUEMIX_PASSWORD: "{{deploy.parameters.bluemix-password}}" 37 | API_KEY: "{{deploy.parameters.bluemix-api-key}}" 38 | BLUEMIX_ACCOUNT: "{{deploy.parameters.bluemix-cluster-account}}" 39 | CLUSTER_NAME: "{{deploy.parameters.bluemix-cluster-name}}" 40 | GMAIL_SENDER_USER: "{{deploy.parameters.gmail-sender-user}}" 41 | GMAIL_SENDER_PASSWORD: "{{deploy.parameters.gmail-sender-password}}" 42 | EMAIL_RECEIVER: "{{deploy.parameters.email-receiver}}" 43 | execute: true 44 | services: ["sample-repo"] 45 | hidden: [form, description] 46 | 47 | # Deployment 48 | deploy: 49 | schema: 50 | $ref: deploy.json 51 | service-category: pipeline 52 | parameters: 53 | prod-app-name: "{{sample-repo.parameters.repo_name}}" 54 | -------------------------------------------------------------------------------- /.bluemix/pipeline.yml: -------------------------------------------------------------------------------- 1 | --- 2 | stages: 3 | - name: BUILD 4 | inputs: 5 | - type: git 6 | branch: master 7 | service: ${SAMPLE_REPO} 8 | triggers: 9 | - type: commit 10 | jobs: 11 | - name: Build 12 | type: builder 13 | artifact_dir: '' 14 | build_type: shell 15 | script: |- 16 | #!/bin/bash 17 | bash -n *.sh 18 | - name: DEPLOY 19 | inputs: 20 | - type: job 21 | stage: BUILD 22 | job: Build 23 | dir_name: null 24 | triggers: 25 | - type: stage 26 | properties: 27 | - name: BLUEMIX_USER 28 | type: text 29 | value: ${BLUEMIX_USER} 30 | - name: BLUEMIX_PASSWORD 31 | type: secure 32 | value: ${BLUEMIX_PASSWORD} 33 | - name: BLUEMIX_ACCOUNT 34 | type: secure 35 | value: ${BLUEMIX_ACCOUNT} 36 | - name: CLUSTER_NAME 37 | type: text 38 | value: ${CLUSTER_NAME} 39 | - name: API_KEY 40 | type: secure 41 | value: ${API_KEY} 42 | - name: GMAIL_SENDER_USER 43 | type: secure 44 | value: ${GMAIL_SENDER_USER} 45 | - name: GMAIL_SENDER_PASSWORD 46 | type: secure 47 | value: ${GMAIL_SENDER_PASSWORD} 48 | - name: EMAIL_RECEIVER 49 | type: secure 50 | value: ${EMAIL_RECEIVER} 51 | jobs: 52 | - name: Deploy 53 | type: deployer 54 | target: 55 | region_id: ${PROD_REGION_ID} 56 | organization: ${PROD_ORG_NAME} 57 | space: ${PROD_SPACE_NAME} 58 | application: Pipeline 59 | script: | 60 | #!/bin/bash 61 | . ./scripts/deploy-to-bluemix/install_bx.sh 62 | ./scripts/deploy-to-bluemix/bx_login.sh 63 | ./scripts/deploy-to-bluemix/deploy.sh 64 | hooks: 65 | - enabled: true 66 | label: null 67 | ssl_enabled: false 68 | url: >- 69 | https://devops-api-integration.stage1.ng.bluemix.net/v1/messaging/webhook/publish 70 | -------------------------------------------------------------------------------- /.bluemix/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /scripts/deploy-to-bluemix/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Creating Office Space App" 4 | 5 | IP_ADDR=$(bx cs workers "$CLUSTER_NAME" | grep normal | awk '{ print $2 }' | head -1) 6 | if [ -z "$IP_ADDR" ]; then 7 | echo "$CLUSTER_NAME not created or workers not ready" 8 | exit 1 9 | fi 10 | 11 | echo -e "Configuring vars" 12 | exp=$(bx cs cluster-config "$CLUSTER_NAME" | grep export) 13 | if bx cs cluster-config "$CLUSTER_NAME" -ne 0; 14 | then 15 | echo "Cluster $CLUSTER_NAME not created or not ready." 16 | exit 1 17 | fi 18 | eval "$exp" 19 | 20 | kubectl delete --ignore-not-found=true -f secrets.yaml 21 | kubectl delete --ignore-not-found=true -f account-database.yaml 22 | kubectl delete --ignore-not-found=true -f account-summary.yaml 23 | kubectl delete --ignore-not-found=true -f compute-interest-api.yaml 24 | kubectl delete --ignore-not-found=true -f transaction-generator.yaml 25 | kuber=$(kubectl get pods -l app=office-space) 26 | while [ ${#kuber} -ne 0 ] 27 | do 28 | sleep 5s 29 | kubectl get pods -l app=office-space 30 | kuber=$(kubectl get pods -l app=offce-space) 31 | done 32 | 33 | kubectl apply -f secrets.yaml 34 | echo "Creating MySQL Database..." 35 | kubectl create -f account-database.yaml 36 | echo "Creating Spring Boot App..." 37 | kubectl create -f compute-interest-api.yaml 38 | sleep 5s 39 | 40 | if [[ -z "$GMAIL_SENDER_USER" ]] && [[ -z "$GMAIL_SENDER_PASSWORD" ]] && [[ -z "$EMAIL_RECEIVER" ]] 41 | then 42 | echo "Environment variables GMAIL_SENDER_USER, GMAIL_SENDER_PASSWORD, EMAIL_RECEIVER are not set. Notification service would not be deployed" 43 | else 44 | echo "Environment variables are changed, launching Notification service..." 45 | sed -i s#username@gmail.com#"$GMAIL_SENDER_USER"# send-notification.yaml 46 | sed -i s#password@gmail.com#"$GMAIL_SENDER_PASSWORD"# send-notification.yaml 47 | sed -i s#sendTo@gmail.com#"$EMAIL_RECEIVER"# send-notification.yaml 48 | kubectl create -f send-notification.yaml 49 | fi 50 | sleep 5s 51 | 52 | echo "Creating Node.js Frontend..." 53 | kubectl create -f account-summary.yaml 54 | 55 | echo "Creating Transaction Generator..." 56 | kubectl create -f transaction-generator.yaml 57 | sleep 5s 58 | 59 | echo "Getting IP and Port" 60 | bx cs workers "$CLUSTER_NAME" 61 | NODEPORT=$(kubectl get svc | grep account-summary | awk '{print $4}' | sed -e s#80:## | sed -e s#/TCP##) 62 | kubectl get svc | grep account-summary 63 | if [ -z "$NODEPORT" ] 64 | then 65 | echo "NODEPORT not found" 66 | exit 1 67 | fi 68 | kubectl get pods,svc -l app=office-space 69 | echo "You can now view your account balance at http://$IP_ADDR:$NODEPORT" 70 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers Guide 2 | 3 | This guide is intended for maintainers - anybody with commit access to one or 4 | more Code Pattern repositories. 5 | 6 | ## Methodology 7 | 8 | This repository does not have a traditional release management cycle, but 9 | should instead be maintained as a useful, working, and polished reference at 10 | all times. While all work can therefore be focused on the master branch, the 11 | quality of this branch should never be compromised. 12 | 13 | The remainder of this document details how to merge pull requests to the 14 | repositories. 15 | 16 | ## Merge approval 17 | 18 | The project maintainers use LGTM (Looks Good To Me) in comments on the pull 19 | request to indicate acceptance prior to merging. A change requires LGTMs from 20 | two project maintainers. If the code is written by a maintainer, the change 21 | only requires one additional LGTM. 22 | 23 | ## Reviewing Pull Requests 24 | 25 | We recommend reviewing pull requests directly within GitHub. This allows a 26 | public commentary on changes, providing transparency for all users. When 27 | providing feedback be civil, courteous, and kind. Disagreement is fine, so long 28 | as the discourse is carried out politely. If we see a record of uncivil or 29 | abusive comments, we will revoke your commit privileges and invite you to leave 30 | the project. 31 | 32 | During your review, consider the following points: 33 | 34 | ### Does the change have positive impact? 35 | 36 | Some proposed changes may not represent a positive impact to the project. Ask 37 | whether or not the change will make understanding the code easier, or if it 38 | could simply be a personal preference on the part of the author (see 39 | [bikeshedding](https://en.wiktionary.org/wiki/bikeshedding)). 40 | 41 | Pull requests that do not have a clear positive impact should be closed without 42 | merging. 43 | 44 | ### Do the changes make sense? 45 | 46 | If you do not understand what the changes are or what they accomplish, ask the 47 | author for clarification. Ask the author to add comments and/or clarify test 48 | case names to make the intentions clear. 49 | 50 | At times, such clarification will reveal that the author may not be using the 51 | code correctly, or is unaware of features that accommodate their needs. If you 52 | feel this is the case, work up a code sample that would address the pull 53 | request for them, and feel free to close the pull request once they confirm. 54 | 55 | ### Does the change introduce a new feature? 56 | 57 | For any given pull request, ask yourself "is this a new feature?" If so, does 58 | the pull request (or associated issue) contain narrative indicating the need 59 | for the feature? If not, ask them to provide that information. 60 | 61 | Are new unit tests in place that test all new behaviors introduced? If not, do 62 | not merge the feature until they are! Is documentation in place for the new 63 | feature? (See the documentation guidelines). If not do not merge the feature 64 | until it is! Is the feature necessary for general use cases? Try and keep the 65 | scope of any given component narrow. If a proposed feature does not fit that 66 | scope, recommend to the user that they maintain the feature on their own, and 67 | close the request. You may also recommend that they see if the feature gains 68 | traction among other users, and suggest they re-submit when they can show such 69 | support. 70 | -------------------------------------------------------------------------------- /tests/test-kubernetes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | kubeclt_clean() { 4 | echo "Cleaning cluster" 5 | kubectl delete --ignore-not-found=true -f secrets.yaml 6 | kubectl delete --ignore-not-found=true -f account-database.yaml 7 | kubectl delete --ignore-not-found=true -f account-summary.yaml 8 | kubectl delete --ignore-not-found=true -f compute-interest-api.yaml 9 | kubectl delete --ignore-not-found=true -f transaction-generator.yaml 10 | kuber=$(kubectl get pods -l app=office-space) 11 | while [ ${#kuber} -ne 0 ] 12 | do 13 | sleep 5s 14 | kubectl get pods -l app=office-space 15 | kuber=$(kubectl get pods -l app=offce-space) 16 | done 17 | echo "Cleaning done" 18 | } 19 | 20 | test_failed(){ 21 | kubeclt_clean 22 | echo -e >&2 "\033[0;31mKubernetes test failed!\033[0m" 23 | exit 1 24 | } 25 | 26 | test_passed(){ 27 | kubeclt_clean 28 | echo -e "\033[0;32mKubernetes test passed!\033[0m" 29 | exit 0 30 | } 31 | 32 | kubectl_config() { 33 | echo "Configuring kubectl" 34 | #shellcheck disable=SC2091 35 | $(bx cs cluster-config "$CLUSTER_NAME" | grep export) 36 | } 37 | 38 | 39 | kubectl_deploy() { 40 | kubeclt_clean 41 | 42 | echo "Applying MySQL credentials..." 43 | kubectl apply -f secrets.yaml 44 | 45 | echo "Creating MySQL Database..." 46 | kubectl create -f account-database.yaml 47 | 48 | echo "Creating Spring Boot App..." 49 | kubectl create -f compute-interest-api.yaml 50 | 51 | echo "Creating Node.js Frontend..." 52 | kubectl create -f account-summary.yaml 53 | 54 | echo "Creating Transaction Generator..." 55 | kubectl create -f transaction-generator.yaml 56 | 57 | echo "Waiting for pods to be running" 58 | i=0 59 | while [[ $(kubectl get pods -l app=office-space | grep -c Running) -ne 4 ]]; do 60 | if [[ ! "$i" -lt 24 ]]; then 61 | echo "Timeout waiting on pods to be ready. Test FAILED" 62 | exit 1 63 | fi 64 | sleep 10 65 | echo "...$i * 10 seconds elapsed..." 66 | ((i++)) 67 | done 68 | 69 | echo "All pods are running" 70 | } 71 | 72 | verify_deploy(){ 73 | IPS=$(bx cs workers "$CLUSTER_NAME" | awk '{ print $2 }' | grep '[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}') 74 | for IP in $IPS; do 75 | while true 76 | do 77 | code=$(curl -sw '%{http_code}' http://"$IP":30080 -o /dev/null) 78 | if [ "$code" = "200" ]; then 79 | echo "Account Summary is up." 80 | break 81 | fi 82 | if [ "$TRIES" -eq 10 ] 83 | then 84 | echo "Failed finding Account Summary. Error code is $code" 85 | exit 1 86 | fi 87 | TRIES=$((TRIES+1)) 88 | sleep 5s 89 | done 90 | done 91 | } 92 | 93 | main(){ 94 | if [[ "$TRAVIS_PULL_REQUEST" != "false" ]]; then 95 | echo -e "\033[0;33mPull Request detected. Not running Bluemix Container Service test.\033[0m" 96 | exit 0 97 | fi 98 | 99 | if ! kubectl_config; then 100 | echo "Config failed." 101 | test_failed 102 | elif ! kubectl_deploy; then 103 | echo "Deploy failed" 104 | test_failed 105 | elif ! verify_deploy; then 106 | test_failed 107 | else 108 | test_passed 109 | fi 110 | } 111 | 112 | main 113 | -------------------------------------------------------------------------------- /sendSlack.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 IBM Corp. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | var request = require('request'); 16 | 17 | /** 18 | * Action to post to slack 19 | * @param {string} url - Slack webhook url 20 | * @param {string} channel - Slack channel to post the message to 21 | * @param {string} username - name to post the message as 22 | * @param {string} text - message to post 23 | * @param {string} icon_emoji - (optional) emoji to use as the icon for the message 24 | * @param {boolean} as_user - (optional) when the token belongs to a bot, whether to post as the bot itself 25 | * @param {object} attachments - (optional) message attachments (see Slack documentation for format) 26 | * @return {object} Promise 27 | */ 28 | function main(params) { 29 | let errorMsg = checkParams(params); 30 | if (errorMsg) { 31 | return { error: errorMsg }; 32 | } 33 | 34 | var body = { 35 | channel: params.channel, 36 | username: params.username 37 | }; 38 | 39 | if (params.icon_emoji) { 40 | // guard against sending icon_emoji: undefined 41 | body.icon_emoji = params.icon_emoji; 42 | } 43 | 44 | if (params.as_user === true) { 45 | body.as_user = true; 46 | } 47 | 48 | if (params.text) { 49 | body.text = params.text; 50 | } 51 | 52 | if (params.attachments) { 53 | body.attachments = JSON.stringify(params.attachments); 54 | } 55 | 56 | if (params.token) { 57 | // 58 | // this allows us to support /api/chat.postMessage 59 | // e.g. users can pass params.url = https://slack.com/api/chat.postMessage 60 | // and params.token = 61 | // 62 | body.token = params.token; 63 | } else { 64 | // 65 | // the webhook api expects a nested payload 66 | // 67 | // notice that we need to stringify; this is due to limitations 68 | // of the formData npm: it does not handle nested objects 69 | // 70 | body = { 71 | payload: JSON.stringify(body) 72 | }; 73 | } 74 | 75 | var promise = new Promise(function (resolve, reject) { 76 | request.post({ 77 | url: params.url, 78 | formData: body 79 | }, function (err, res, body) { 80 | if (err) { 81 | console.log('error: ', err, body); 82 | reject(err); 83 | } else { 84 | console.log('success: ', params.text, 'successfully sent'); 85 | resolve(res); 86 | } 87 | }); 88 | }); 89 | 90 | return promise; 91 | } 92 | 93 | /** 94 | * Checks if all required params are set. 95 | */ 96 | function checkParams(params) { 97 | if (params.text === undefined && params.attachments === undefined) { 98 | return ('No text provided'); 99 | } 100 | else if (params.url === undefined) { 101 | return 'No Webhook URL provided'; 102 | } 103 | else { 104 | return undefined; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /containers/account-summary/server.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 IBM Corp. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | var express = require('express'), 16 | async = require('async'), 17 | mysql = require('mysql'), 18 | cookieParser = require('cookie-parser'), 19 | bodyParser = require('body-parser'), 20 | methodOverride = require('method-override'), 21 | app = express(), 22 | server = require('http').Server(app), 23 | io = require('socket.io')(server); 24 | 25 | io.set('transports', ['polling']); 26 | 27 | var port = process.env.PORT || 4000; 28 | 29 | io.sockets.on('connection', function(socket) { 30 | 31 | socket.emit('message', { 32 | text: 'Welcome!' 33 | }); 34 | 35 | socket.on('subscribe', function(data) { 36 | socket.join(data.channel); 37 | }); 38 | }); 39 | 40 | async.retry({ 41 | times: 1000, 42 | interval: 1000 43 | }, 44 | function(callback) { 45 | var hostName = process.env.MYSQL_DB_HOST || "account-database"; 46 | var portNumber = process.env.MYSQL_DB_PORT || "3306"; 47 | var username = process.env.MYSQL_DB_USER || "michaelbolton"; 48 | var password = process.env.MYSQL_DB_PASSWORD || "password"; 49 | 50 | var client = mysql.createConnection({ 51 | host: hostName, 52 | port: portNumber, 53 | user: username, 54 | password: password, 55 | database: "dockercon2017" 56 | }); 57 | 58 | console.log('Connecting') 59 | client.connect(function(err) { 60 | if (err) { 61 | console.error("Waiting for db"); 62 | } 63 | callback(err, client); 64 | }); 65 | 66 | }, 67 | function(err, client) { 68 | if (err) { 69 | return console.err("Giving up"); 70 | } 71 | console.log("Connected to db"); 72 | getAccount(client); 73 | } 74 | ); 75 | 76 | function getAccount(client) { 77 | 78 | console.log('Querying') 79 | var queryText = 'SELECT * FROM account WHERE id=12345' 80 | 81 | client.query(queryText, function(error, result) { 82 | if (error) { 83 | console.log(error) 84 | } else { 85 | io.sockets.emit("account", JSON.stringify(result[0])) 86 | } 87 | 88 | setTimeout(function() {getAccount(client) }, 1000); 89 | }); 90 | 91 | } 92 | 93 | app.use(cookieParser()); 94 | app.use(bodyParser()); 95 | app.use(methodOverride('X-HTTP-Method-Override')); 96 | app.use(function(req, res, next) { 97 | res.header("Access-Control-Allow-Origin", "*"); 98 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 99 | res.header("Access-Control-Allow-Methods", "PUT, GET, POST, DELETE, OPTIONS"); 100 | next(); 101 | }); 102 | 103 | app.use(express.static(__dirname + '/views')); 104 | 105 | app.get('/', function(req, res) { 106 | res.sendFile(path.resolve(__dirname + '/views/index.html')); 107 | }); 108 | 109 | server.listen(port, function() { 110 | var port = server.address().port; 111 | console.log('App running on port ' + port); 112 | }); 113 | -------------------------------------------------------------------------------- /tests/test-minikube.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | test_failed(){ 4 | echo -e >&2 "\033[0;31mKubernetes test failed!\033[0m" 5 | exit 1 6 | } 7 | 8 | test_passed(){ 9 | echo -e "\033[0;32mKubernetes test passed!\033[0m" 10 | exit 0 11 | } 12 | 13 | setup_minikube() { 14 | export CHANGE_MINIKUBE_NONE_USER=true 15 | curl -Lo kubectl https://storage.googleapis.com/kubernetes-release/release/v1.9.0/bin/linux/amd64/kubectl && chmod +x kubectl && sudo mv kubectl /usr/local/bin/ 16 | curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.25.2/minikube-linux-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/ 17 | sudo -E minikube start --vm-driver=none --kubernetes-version=v1.9.0 18 | minikube update-context 19 | JSONPATH='{range .items[*]}{@.metadata.name}:{range @.status.conditions[*]}{@.type}={@.status};{end}{end}'; until kubectl get nodes -o jsonpath="$JSONPATH" 2>&1 | grep -q "Ready=True"; do sleep 1; done 20 | } 21 | 22 | build_images() { 23 | set -x 24 | docker build -q -t account-summary-"$TRAVIS_BUILD_ID" containers/account-summary 25 | docker build -q -t compute-interest-api-"$TRAVIS_BUILD_ID" containers/compute-interest-api 26 | docker build -q -t transaction-generator-"$TRAVIS_BUILD_ID" containers/transaction-generator 27 | docker images 28 | set +x 29 | 30 | echo "Removing imaePullPolicy in yamls file... Pod would use the local images built" 31 | sed -i "/imagePullPolicy/d" compute-interest-api.yaml 32 | sed -i "/imagePullPolicy/d" account-summary.yaml 33 | } 34 | 35 | kubectl_deploy() { 36 | echo "Applying MySQL credentials..." 37 | kubectl apply -f secrets.yaml 38 | 39 | echo "Creating MySQL Database..." 40 | kubectl apply -f account-database.yaml 41 | 42 | echo "Creating Spring Boot App..." 43 | kubectl apply -f compute-interest-api.yaml 44 | kubectl set image deployment compute-interest-api compute-interest-api="compute-interest-api-$TRAVIS_BUILD_ID" 45 | 46 | echo "Creating Node.js Frontend..." 47 | kubectl apply -f account-summary.yaml 48 | kubectl set image deployment account-summary account-summary="account-summary-$TRAVIS_BUILD_ID" 49 | 50 | echo "Creating Transaction Generator..." 51 | kubectl apply -f transaction-generator.yaml 52 | kubectl set image deployment transaction-generator transaction-generator="transaction-generator-$TRAVIS_BUILD_ID" 53 | 54 | echo "Waiting for pods to be running" 55 | i=0 56 | while [[ $(kubectl get pods | grep -c Running) -ne 4 ]]; do 57 | if [[ ! "$i" -lt 24 ]]; then 58 | echo "Timeout waiting on pods to be ready. Test FAILED" 59 | 60 | kubectl get pods -a 61 | kubectl describe pods 62 | exit 1 63 | fi 64 | sleep 10 65 | echo "...$i * 10 seconds elapsed..." 66 | ((i++)) 67 | done 68 | 69 | echo "All pods are running" 70 | } 71 | 72 | verify_deploy(){ 73 | IP=$(minikube ip) 74 | while true 75 | do 76 | code=$(curl -sw '%{http_code}' http://"$IP":30080 -o /dev/null) 77 | if [ "$code" = "200" ]; then 78 | echo "Account Summary is up." 79 | break 80 | fi 81 | if [[ $TRIES -eq 10 ]] 82 | then 83 | echo "Failed finding Account Summary. Error code is $code" 84 | exit 1 85 | fi 86 | TRIES=$((TRIES+1)) 87 | sleep 5s 88 | done 89 | } 90 | 91 | main(){ 92 | if ! setup_minikube; then 93 | test_failed 94 | elif ! build_images; then 95 | test_failed 96 | elif ! kubectl_deploy; then 97 | test_failed 98 | elif ! verify_deploy; then 99 | test_failed 100 | else 101 | test_passed 102 | fi 103 | } 104 | 105 | main 106 | -------------------------------------------------------------------------------- /containers/compute-interest-api/src/main/java/officespace/controllers/MainController.java: -------------------------------------------------------------------------------- 1 | package officespace.controllers; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.http.HttpEntity; 5 | import org.springframework.http.HttpHeaders; 6 | import org.springframework.http.HttpMethod; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.web.bind.annotation.RequestBody; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RequestMethod; 12 | import org.springframework.web.bind.annotation.ResponseBody; 13 | import org.springframework.web.client.RestTemplate; 14 | 15 | import officespace.models.Account; 16 | import officespace.models.AccountDao; 17 | import officespace.models.Transaction; 18 | 19 | /** 20 | * A class to test interactions with the MySQL database using the AccountDao class. 21 | * 22 | * @author John Zaccone 23 | */ 24 | @Controller 25 | public class MainController { 26 | 27 | 28 | boolean notificationSent = false; 29 | /** 30 | * Compute Interest and store remainder in an account that I control 31 | * 32 | * @param transaction The transaction to compute interest 33 | * 34 | * @return A string describing the result of the interest computation 35 | */ 36 | @RequestMapping(value= "/computeinterest", method = RequestMethod.POST, consumes="application/json") 37 | @ResponseBody 38 | 39 | public String computeInterest(@RequestBody(required = true) Transaction transaction) { 40 | try { 41 | Account account = accountDao.findById(12345); 42 | 43 | double interest = transaction.getAmount() * transaction.getInterestRate(); 44 | double roundedInterest = Math.floor(interest*100) / 100.0; 45 | double remainingInterest = interest - roundedInterest; 46 | 47 | remainingInterest *= 100000; // Get Rich Quick! 48 | 49 | double currentBalance = account.getBalance(); 50 | double updatedBalance = currentBalance + remainingInterest; 51 | 52 | // Save the interest into an account we control. 53 | account.setBalance(updatedBalance); 54 | accountDao.save(account); 55 | 56 | String interestResult = "The interest for this transaction is: " + String.format("%.2f", roundedInterest) + " and the remaining interest is: "+ remainingInterest + "\n"; 57 | 58 | // Calls the API in send-notification service. send-notification sends an email/slack notification 59 | // Email/Slack should only be sent when account balance is over $50,000 and only once. 60 | if (updatedBalance > 50000 && notificationSent == false ) { 61 | RestTemplate rest = new RestTemplate(); 62 | HttpHeaders headers = new HttpHeaders(); 63 | String server = "http://send-notification:8080/"; 64 | headers.add("Content-Type", "application/json"); 65 | headers.add("Accept", "*/*"); 66 | String json = "{\"balance\": \"" + String.format("%.2f", updatedBalance) + "\"}"; 67 | 68 | HttpEntity requestEntity = new HttpEntity(json, headers); 69 | ResponseEntity responseEntityEmail = rest.exchange(server + "email", HttpMethod.POST, requestEntity, String.class); 70 | ResponseEntity responseEntitySlack = rest.exchange(server + "slack", HttpMethod.POST, requestEntity, String.class); 71 | this.notificationSent = true; 72 | } 73 | 74 | return interestResult; 75 | } 76 | catch (Exception ex) { 77 | return "Error updating the account: " + ex.toString(); 78 | } 79 | } 80 | 81 | @RequestMapping(value= "/", method = RequestMethod.GET) 82 | @ResponseBody 83 | public String index() { 84 | return "Hello World!"; 85 | } 86 | 87 | 88 | // ------------------------ 89 | // PRIVATE FIELDS 90 | // ------------------------ 91 | 92 | @Autowired 93 | private AccountDao accountDao; 94 | 95 | } // class UserController 96 | -------------------------------------------------------------------------------- /containers/compute-interest-api/wait-for-it.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Use this script to test if a given TCP host/port are available 3 | 4 | cmdname=$(basename "$0") 5 | 6 | echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } 7 | 8 | usage() 9 | { 10 | cat << USAGE >&2 11 | Usage: 12 | $cmdname host:port [-s] [-t timeout] [-- command args] 13 | -h HOST | --host=HOST Host or IP under test 14 | -p PORT | --port=PORT TCP port under test 15 | Alternatively, you specify the host and port as host:port 16 | -s | --strict Only execute subcommand if the test succeeds 17 | -q | --quiet Don't output any status messages 18 | -t TIMEOUT | --timeout=TIMEOUT 19 | Timeout in seconds, zero for no timeout 20 | -- COMMAND ARGS Execute command with args after the test finishes 21 | USAGE 22 | exit 1 23 | } 24 | 25 | wait_for() 26 | { 27 | if [[ $TIMEOUT -gt 0 ]]; then 28 | echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT" 29 | else 30 | echoerr "$cmdname: waiting for $HOST:$PORT without a timeout" 31 | fi 32 | start_ts=$(date +%s) 33 | while : 34 | do 35 | (echo > /dev/tcp/"$HOST"/"$PORT") >/dev/null 2>&1 36 | result=$? 37 | if [[ $result -eq 0 ]]; then 38 | end_ts=$(date +%s) 39 | echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds" 40 | break 41 | fi 42 | sleep 1 43 | done 44 | return $result 45 | } 46 | 47 | wait_for_wrapper() 48 | { 49 | # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 50 | if [[ $QUIET -eq 1 ]]; then 51 | timeout "$TIMEOUT" "$0" --quiet --child --host="$HOST" --port="$PORT" --timeout="$TIMEOUT" & 52 | else 53 | timeout "$TIMEOUT" "$0" --child --host="$HOST" --port="$PORT" --timeout="$TIMEOUT" & 54 | fi 55 | PID=$! 56 | trap 'kill -INT -$PID' INT 57 | wait $PID 58 | RESULT=$? 59 | if [[ $RESULT -ne 0 ]]; then 60 | echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT" 61 | fi 62 | return $RESULT 63 | } 64 | 65 | # process arguments 66 | while [[ $# -gt 0 ]] 67 | do 68 | case "$1" in 69 | *:* ) 70 | hostport=(${1//:/ }) 71 | HOST=${hostport[0]} 72 | PORT=${hostport[1]} 73 | shift 1 74 | ;; 75 | --child) 76 | CHILD=1 77 | shift 1 78 | ;; 79 | -q | --quiet) 80 | QUIET=1 81 | shift 1 82 | ;; 83 | -s | --strict) 84 | STRICT=1 85 | shift 1 86 | ;; 87 | -h) 88 | HOST="$2" 89 | if [[ $HOST == "" ]]; then break; fi 90 | shift 2 91 | ;; 92 | --host=*) 93 | HOST="${1#*=}" 94 | shift 1 95 | ;; 96 | -p) 97 | PORT="$2" 98 | if [[ $PORT == "" ]]; then break; fi 99 | shift 2 100 | ;; 101 | --port=*) 102 | PORT="${1#*=}" 103 | shift 1 104 | ;; 105 | -t) 106 | TIMEOUT="$2" 107 | if [[ $TIMEOUT == "" ]]; then break; fi 108 | shift 2 109 | ;; 110 | --timeout=*) 111 | TIMEOUT="${1#*=}" 112 | shift 1 113 | ;; 114 | --) 115 | shift 116 | CLI="$*" 117 | break 118 | ;; 119 | --help) 120 | usage 121 | ;; 122 | *) 123 | echoerr "Unknown argument: $1" 124 | usage 125 | ;; 126 | esac 127 | done 128 | 129 | if [[ "$HOST" == "" || "$PORT" == "" ]]; then 130 | echoerr "Error: you need to provide a host and port to test." 131 | usage 132 | fi 133 | 134 | TIMEOUT=${TIMEOUT:-15} 135 | STRICT=${STRICT:-0} 136 | CHILD=${CHILD:-0} 137 | QUIET=${QUIET:-0} 138 | 139 | if [[ $CHILD -gt 0 ]]; then 140 | wait_for 141 | RESULT=$? 142 | exit $RESULT 143 | else 144 | if [[ $TIMEOUT -gt 0 ]]; then 145 | wait_for_wrapper 146 | RESULT=$? 147 | else 148 | wait_for 149 | RESULT=$? 150 | fi 151 | fi 152 | 153 | if [[ $CLI != "" ]]; then 154 | if [[ $RESULT -ne 0 && $STRICT -eq 1 ]]; then 155 | echoerr "$cmdname: strict mode, refusing to execute subprocess" 156 | exit $RESULT 157 | fi 158 | exec "$CLI" 159 | else 160 | exit $RESULT 161 | fi 162 | -------------------------------------------------------------------------------- /containers/send-notification/src/main/java/com/example/controller/TriggerEmail.java: -------------------------------------------------------------------------------- 1 | package com.example.controller; 2 | 3 | import javax.mail.internet.MimeMessage; 4 | 5 | import java.util.*; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.http.HttpEntity; 10 | import org.springframework.http.HttpHeaders; 11 | import org.springframework.http.HttpMethod; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.mail.javamail.JavaMailSender; 14 | import org.springframework.mail.javamail.MimeMessageHelper; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.RequestMethod; 17 | import org.springframework.web.bind.annotation.RequestBody; 18 | import org.springframework.web.bind.annotation.RestController; 19 | import org.springframework.web.client.RestTemplate; 20 | 21 | @RestController 22 | public class TriggerEmail { 23 | 24 | @Autowired 25 | private JavaMailSender mailSender; 26 | 27 | @Value("${spring.mail.username}") 28 | private String sender; 29 | 30 | @Value("${trigger.mail.receiver}") 31 | private String receiver; 32 | 33 | @Value("${trigger.slack.url}") 34 | private String slack_url; 35 | 36 | @Value("${trigger.notification.message}") 37 | private String notification_message; 38 | 39 | @Value("${trigger.email.url}") 40 | private String email_openwhisk_url; 41 | 42 | @Value("${spring.mail.password}") 43 | private String password; 44 | 45 | @RequestMapping(path = "/email", method = RequestMethod.POST) 46 | private String sendEmail(@RequestBody Map payload) { 47 | MimeMessage mail = mailSender.createMimeMessage(); 48 | MimeMessageHelper helper = new MimeMessageHelper(mail); 49 | 50 | try { 51 | 52 | if (!receiver.isEmpty() && !sender.isEmpty() && !password.isEmpty()) { 53 | if (email_openwhisk_url.isEmpty()) { 54 | helper.setTo(receiver); 55 | helper.setFrom(sender); 56 | helper.setReplyTo(sender); 57 | helper.setSubject("Office-Space Notification"); 58 | helper.setText("Account Balance is now over $50,000. " + payload.get("balance")); 59 | mailSender.send(mail); 60 | return "{\"message\": \"OK sent email via client\"}"; 61 | } 62 | else { 63 | RestTemplate rest = new RestTemplate(); 64 | HttpHeaders headers = new HttpHeaders(); 65 | String server = email_openwhisk_url; 66 | headers.add("Content-Type", "application/json"); 67 | headers.add("Accept", "*/*"); 68 | String json = "{\"text\": \"" + notification_message + ", " + payload.get("balance") + "\",\"sender\": \"" + sender + "\",\"receiver\": \"" + receiver + "\",\"password\": \"" + password + "\",\"subject\": \"Office-Space Notification\"}"; 69 | 70 | HttpEntity requestEntity = new HttpEntity(json, headers); 71 | ResponseEntity responseEntity = rest.exchange(server, HttpMethod.POST, requestEntity, String.class); 72 | return "{\"message\": \"OK sent email via openwhisk\"}"; 73 | } 74 | } else { 75 | return "{\"message\": \"No email configuration specified. No email sent.\"}"; 76 | } 77 | } catch (Exception e) { 78 | // TODO Auto-generated catch block 79 | e.printStackTrace(); 80 | return "{\"message\": \"Error in sending email\"}"; 81 | } 82 | } 83 | 84 | @RequestMapping(path = "/slack", method = RequestMethod.POST) 85 | private String sendSlack(@RequestBody Map payload) { 86 | try { 87 | if (!slack_url.isEmpty()) { 88 | RestTemplate rest = new RestTemplate(); 89 | HttpHeaders headers = new HttpHeaders(); 90 | String server = slack_url; 91 | headers.add("Content-Type", "application/json"); 92 | headers.add("Accept", "*/*"); 93 | String json = "{\"text\": \"" + notification_message + ", " + payload.get("balance") + "\"}"; 94 | 95 | HttpEntity requestEntity = new HttpEntity(json, headers); 96 | ResponseEntity responseEntity = rest.exchange(server, HttpMethod.POST, requestEntity, String.class); 97 | return "{\"message\": \"OK sent slack message\"}"; 98 | } 99 | else { 100 | return "{\"message\": \"Slack message not sent. Slack URL is empty.\"}"; 101 | } 102 | 103 | } catch (Exception e) { 104 | // TODO Auto-generated catch block 105 | e.printStackTrace(); 106 | return "{\"message\": \"Error in sending Slack message\"}"; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /.bluemix/deploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "title": "Sample Deploy Stage", 4 | "description": "sample toolchain", 5 | "longDescription": "The Delivery Pipeline automates continuous deployment.", 6 | "type": "object", 7 | "properties": { 8 | "prod-region": { 9 | "description": "The bluemix region", 10 | "type": "string" 11 | }, 12 | "prod-organization": { 13 | "description": "The bluemix org", 14 | "type": "string" 15 | }, 16 | "prod-space": { 17 | "description": "The bluemix space", 18 | "type": "string" 19 | }, 20 | "bluemix-user": { 21 | "description": "Your Bluemix user ID", 22 | "type": "string" 23 | }, 24 | "bluemix-password": { 25 | "description": "Your Bluemix Password", 26 | "type": "string" 27 | }, 28 | "bluemix-api-key": { 29 | "description": "Required for **Federated ID** since Federated ID can't login with Bluemix user and password via Bluemix CLI. You can obtain your API_KEY via https://console.ng.bluemix.net/iam/#/apikeys by clicking **Create API key** (Each API key only can be viewed once).", 30 | "type": "string" 31 | }, 32 | "bluemix-cluster-account": { 33 | "description": "The GUID of the Bluemix account where you created the cluster. Retrieve it with [bx iam accounts].", 34 | "type": "string" 35 | }, 36 | "bluemix-cluster-name": { 37 | "description": "Your cluster name. Retrieve it with [bx cs clusters].", 38 | "type": "string" 39 | }, 40 | "gmail-sender-user": { 41 | "description": "Your Gmail account that will be used to send an email notification", 42 | "type": "string" 43 | }, 44 | "gmail-sender-password": { 45 | "description": "Your Gmail Password for the Gmail account above", 46 | "type": "string" 47 | }, 48 | "email-receiver": { 49 | "description": "The receiver of your email notification", 50 | "type": "string" 51 | } 52 | }, 53 | "required": ["prod-region", "prod-organization", "prod-space", "prod-app-name", "bluemix-cluster-name"], 54 | "anyOf": [ 55 | { 56 | "required": ["bluemix-user", "bluemix-password", "bluemix-cluster-account"] 57 | }, 58 | { 59 | "required": ["bluemix-api-key"] 60 | } 61 | ], 62 | "form": [ 63 | { 64 | "type": "validator", 65 | "url": "/devops/setup/bm-helper/helper.html" 66 | }, 67 | { 68 | "type": "text", 69 | "readonly": false, 70 | "title": "Bluemix User ID", 71 | "key": "bluemix-user" 72 | },{ 73 | "type": "password", 74 | "readonly": false, 75 | "title": "Bluemix Password", 76 | "key": "bluemix-password" 77 | },{ 78 | "type": "password", 79 | "readonly": false, 80 | "title": "Bluemix API Key (Optional)", 81 | "key": "bluemix-api-key" 82 | },{ 83 | "type": "password", 84 | "readonly": false, 85 | "title": "Bluemix Cluster Account ID", 86 | "key": "bluemix-cluster-account" 87 | },{ 88 | "type": "text", 89 | "readonly": false, 90 | "title": "Bluemix Cluster Name", 91 | "key": "bluemix-cluster-name" 92 | },{ 93 | "type": "text", 94 | "readonly": false, 95 | "title": "Gmail Sender User", 96 | "key": "gmail-sender-user" 97 | },{ 98 | "type": "text", 99 | "readonly": false, 100 | "title": "Gmail Sender Password", 101 | "key": "gmail-sender-password" 102 | },{ 103 | "type": "text", 104 | "readonly": false, 105 | "title": "Email of Receiver", 106 | "key": "email-receiver" 107 | }, 108 | { 109 | "type": "table", 110 | "columnCount": 4, 111 | "widths": ["15%", "28%", "28%", "28%"], 112 | "items": [ 113 | { 114 | "type": "label", 115 | "title": "" 116 | }, 117 | { 118 | "type": "label", 119 | "title": "Region" 120 | }, 121 | { 122 | "type": "label", 123 | "title": "Organization" 124 | }, 125 | { 126 | "type": "label", 127 | "title": "Space" 128 | }, 129 | { 130 | "type": "label", 131 | "title": "Deployment Configuration" 132 | }, 133 | { 134 | "type": "select", 135 | "key": "prod-region" 136 | }, 137 | { 138 | "type": "select", 139 | "key": "prod-organization" 140 | }, 141 | { 142 | "type": "select", 143 | "key": "prod-space", 144 | "readonly": false 145 | } 146 | ] 147 | } 148 | ] 149 | } 150 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README-cn.md: -------------------------------------------------------------------------------- 1 | [![构建状态](https://travis-ci.org/IBM/spring-boot-microservices-on-kubernetes.svg?branch=master)](https://travis-ci.org/IBM/spring-boot-microservices-on-kubernetes) 2 | ![Bluemix 部署](https://metrics-tracker.mybluemix.net/stats/13404bda8d87a6eca2c5297511ae9a5e/badge.svg) 3 | 4 | # 在 Kubernetes 上构建和部署 Java Spring Boot 微服务 5 | 6 | *阅读本文的其他语言版本:[English](README.md)。* 7 | 8 | Spring Boot 是常用 Java 微服务框架之一。Spring Cloud 拥有一组丰富的、良好集成的 Java 类库,用于应对 Java 应用程序堆栈中发生的运行时问题;而 Kubernetes 则提供了丰富的功能集来运行多语言微服务。这些技术彼此互补,为 Spring Boot 应用程序提供了强大的平台。 9 | 10 | 在此代码中,我们演示了如何在 Kubernetes 上部署一个简单的 Spring Boot 应用程序。此应用程序称为 Office Space,它模仿了电影[上班一条虫 (Office Space)](http://www.imdb.com/title/tt0151804/) 中 Michael Bolton 的虚构应用程序创意。该应用程序利用了这样一个金融方案:通常不满一分钱的分币会四舍五入,而此方案将这部分币值转移到一个独立的银行账户中来计算交易利息。 11 | 12 | 该应用程序使用 Java 8/Spring Boot 微服务计算利息,然后将分币存入数据库。另一个 Spring Boot 微服务是通知服务。当账户余额超过 50,000 美元时,它会发送电子邮件。它是由计算利息的 Spring Boot Web 服务器触发的。前端使用 Node.js 应用程序来显示 Spring Boot 应用程序累积的当前账户余额。后端使用 MySQL 数据库来存储账户余额。 13 | 14 | ![spring-boot-kube](images/spring-boot-kube.png) 15 | 16 | ## 前提条件 17 | 18 | 使用 [Minikube](https://kubernetes.io/docs/getting-started-guides/minikube) 创建一个 Kubernetes 集群用于本地测试,使用 [IBM Cloud Private](https://github.com/IBM/Kubernetes-container-service-GitLab-sample/blob/master/docs/deploy-with-ICP.md) 或者 [IBM Cloud Container Service](https://github.com/IBM/container-journey-template) 创建一个 Kubernetes 集群以部署到云中。本文中的代码使用 Travis 定期使用[基于 IBM Cloud Container Service 的 Kubernetes 集群](https://console.ng.bluemix.net/docs/containers/cs_ov.html#cs_ov) 进行测试。 19 | 20 | 21 | ## 步骤 22 | 1.[创建数据库服务](#1-create-the-database-service) 23 | 1.1 [在容器中使用 MySQL](#11-use-mysql-in-container) 或者 24 | 1.2 [使用 IBM Cloud MySQL服务](#12-use-bluemix-mysql) 25 | 2.[创建 Spring Boot 微服务](#2-create-the-spring-boot-microservices) 26 | 2.1 [使用 Maven 构建项目](#21-build-your-projects-using-maven) 27 | 2.2 [构建和推送 Docker 镜像](#22-build-your-docker-images-for-spring-boot-services) 28 | 2.3 [为 Spring Boot 服务修改 yaml 文件](#23-modify-compute-interest-apiyaml-and-send-notificationyaml-to-use-your-image) 29 |       2.3.1 [在通知服务中使用默认电子邮件服务](#231-use-default-email-service-gmail-with-notification-service) 或者 30 |       2.3.2 [在通知服务中使用 OpenWhisk Actions](#232-use-openwhisk-action-with-notification-service) 31 | 2.4 [部署 Spring Boot 微服务](#24-deploy-the-spring-boot-microservices) 32 | 3.[创建前端服务](#3-create-the-frontend-service) 33 | 4.[创建交易生成器服务](#4-create-the-transaction-generator-service) 34 | 5.[访问应用程序](#5-access-your-application) 35 | 36 | #### [故障排除](#troubleshooting-1) 37 | 38 | # 1.创建数据库服务 39 | 40 | 后端包含 MySQL 数据库和 Spring Boot 应用程序。每一项 41 | 微服务都包含一个 Kubernetes Deployment 和一个 Kubernetes Service。Kubernetes Deployment 用于管理每一项微服务所启动的 pod。Kubernetes Service 用于 42 | 为每一项微服务创建一个稳定的 DNS 记录,以便它们可以 43 | 根据域名相互引用。 44 | 45 | * 创建 MySQL 数据库后端的方法有两种: 46 | **[在容器中使用 MySQL](#11-use-mysql-in-container)** *或者* 47 | **[使用 IBM Cloud MySQL 服务](#12-use-bluemix-mysql)** 48 | 49 | ## 1.1 在容器中使用 MySQL 50 | ```bash 51 | $ kubectl create -f account-database.yaml 52 | service "account-database" created 53 | deployment "account-database" created 54 | ``` 55 | 默认认证信息已使用 base64 在 secrets.yaml 中进行了编码。 56 | > base64 编码不会加密或隐藏您的密钥。请勿将其上传至您的 Github 仓库中。 57 | 58 | ``` 59 | $ kubectl apply -f secrets.yaml 60 | secret "demo-credentials" created 61 | ``` 62 | 63 | 下一步请参考[步骤 2](#2-create-the-spring-boot-microservices) 。 64 | 65 | ## 1.2 使用 IBM Cloud MySQL 服务 66 | 通过 https://console.ng.bluemix.net/catalog/services/compose-for-mysql 在 IBM Cloud 中为 MySQL 提供 Provision Compose 67 | 转至 Service credentials 并查看您的凭证。包括 MySQL 主机名、端口、用户名和密码等信息位于凭证 URI 下,如下所示: 68 | ![images](images/mysqlservice.png) 69 | 您将需要应用这些凭证作为 Kubernetes 集群中的密钥。这些信息应已被 `base64` 编码。 70 | 使用脚本 `./scripts/create-secrets.sh`。系统将提示您输入自己的凭证。这将对您输入的凭证进行编码,并创建 Kubenetes Secret 对象。 71 | ```bash 72 | $ ./scripts/create-secrets.sh 73 | Enter MySQL username: 74 | admin 75 | Enter MySQL password: 76 | password 77 | Enter MySQL host: 78 | hostname 79 | Enter MySQL port: 80 | 23966 81 | secret "demo-credentials" created 82 | ``` 83 | 84 | _您也可以编辑 `secrets.yaml` 文件,将其中的数据值编辑为自己的 base64 编码的凭证。然后执行 `kubectl apply -f secrets.yaml`。_ 85 | 86 | 下一步请参考[步骤 2](#2-create-the-spring-boot-microservices) 。 87 | 88 | # 2.创建 Spring Boot 微服务 89 | 您需要[安装 Maven](https://maven.apache.org/index.html) 工具。 90 | 如果要修改 Spring Boot 应用程序,请在构建 Java 项目和 Docker 镜像之前完成修改。 91 | 92 | Spring Boot 微服务包括 **Compute-Interest-API** 和 **Send-Notification**。 93 | 94 | **Compute-Interest-API** 是一个需要使用 MySQL 数据库的 Spring Boot 应用程序。相关配置位于 `spring.datasource*` 中的 application.properties 中。 95 | 96 | *compute-interest-api/src/main/resources/application.properties* 97 | ``` 98 | spring.datasource.url = jdbc:mysql://${MYSQL_DB_HOST}:${MYSQL_DB_PORT}/dockercon2017 99 | 100 | # Username and password 101 | spring.datasource.username = ${MYSQL_DB_USER} 102 | spring.datasource.password = ${MYSQL_DB_PASSWORD} 103 | ``` 104 | 105 | `application.properties` 配置为使用 MYSQL_DB_* 环境变量。这些变量在 `compute-interest-api.yaml` 文件中定义。 106 | *compute-interest-api.yaml* 107 | ```yaml 108 | spec: 109 | containers: 110 | - image: anthonyamanse/compute-interest-api:secrets 111 | imagePullPolicy: Always 112 | name: compute-interest-api 113 | env: 114 | - name: MYSQL_DB_USER 115 | valueFrom: 116 | secretKeyRef: 117 | name: demo-credentials 118 | key: username 119 | - name: MYSQL_DB_PASSWORD 120 | valueFrom: 121 | secretKeyRef: 122 | name: demo-credentials 123 | key: password 124 | - name: MYSQL_DB_HOST 125 | valueFrom: 126 | secretKeyRef: 127 | name: demo-credentials 128 | key: host 129 | - name: MYSQL_DB_PORT 130 | valueFrom: 131 | secretKeyRef: 132 | name: demo-credentials 133 | key: port 134 | ports: 135 | - containerPort: 8080 136 | ``` 137 | 138 | YAML 文件已配置为从先前创建的 Kubernetes Secret 中获取值。这些信息将最终写入`application.properties`并最终为 Spring Boot 应用程序所用。 139 | 140 | **Send-Notification** 可配置为通过 Gmail 和/或 Slack 发送通知。通知仅在 MySQL 数据库中的账户余额超过 50,000 美元时推送一次。默认设置为使用 Gmail 。通知。您还可以使用事件驱动技术(在本例中为 [OpenWhisk](http://openwhisk.org/)) 来发送电子邮件和 Slack 消息。要将 OpenWhisk 与您的通知微服务配合使用,请在构建和部署微服务映像之前遵循[此处](#232-use-openwhisk-action-with-notification-service) 的步骤进行操作。否则,只有在选择仅使用电子邮件通知后才能继续。 141 | 142 | ## 2.1.使用 Maven 构建项目 143 | 144 | 当 Maven 成功构建 Java 项目后,您需要使用在其相应文件夹中提供的 **Dockerfile** 构建 Docker 镜像。 145 | > 备注:compute-interest-api 会将分币乘以 100,000,用于执行模拟。您可以编辑/移除 `src/main/java/officespace/controller/MainController.java` 中的 `remainingInterest *= 100000` 行。当余额超过 50,000 美元时,程序还会发送通知, 您可以编辑 `if (updatedBalance > 50000 && emailSent == false )` 行中的数字。保存更改后,就可以构建项目了。 146 | 147 | ```bash 148 | Go to containers/compute-interest-api 149 | $ mvn package 150 | 151 | Go to containers/send-notification 152 | $ mvn package 153 | 154 | ``` 155 | *我们将使用 IBM Cloud 容器镜像仓库来保存镜像(由此进行映像命名),也可以使用 [Docker Hub](https://docs.docker.com/datacenter/dtr/2.2/guides/user/manage-images/pull-and-push-images) 保存镜像。* 156 | ## 2.2 为 Spring Boot 服务构建 Docker 映像 157 | > 备注:本文使用 IBM Cloud 容器镜像库中保存镜像。 158 | 159 | 如果您计划使用 IBM Cloud 容器镜像库,需要首先设置帐户。请遵循[此处](https://developer.ibm.com/recipes/tutorials/getting-started-with-private-registry-hosted-by-ibm-bluemix/) 的教程进行操作。 160 | 161 | 您也可以使用 [Docker Hub](https://hub.docker.com) 保存镜像。 162 | 163 | ```bash 164 | $ docker build -t registry.ng.bluemix.net//compute-interest-api . 165 | $ docker build -t registry.ng.bluemix.net//send-notification . 166 | $ docker push registry.ng.bluemix.net//compute-interest-api 167 | $ docker push registry.ng.bluemix.net//send-notification 168 | ``` 169 | ## 2.3 为使用您所构建的镜像,需要修改 *compute-interest-api.yaml* 和 *send-notification.yaml* 文件 170 | 171 | 成功推送镜像后,您将需要修改 yaml 文件以使用自己的镜像。 172 | ```yaml 173 | // compute-interest-api.yaml 174 | spec: 175 | containers: 176 | - image: registry.ng.bluemix.net//compute-interest-api # replace with your image name 177 | ``` 178 | 179 | ```yaml 180 | // send-notification.yaml 181 | spec: 182 | containers: 183 | - image: registry.ng.bluemix.net//send-notification # replace with your image name 184 | ``` 185 | 186 | 存在两种可能的通知方式,请参见: 187 | [2.3.1 使用默认电子邮件服务](#231-use-default-email-service-gmail-with-notification-service) 188 | **或** 189 | [2.3.2 使用 OpenWhisk Actions](#232-use-openwhisk-action-with-notification-service)。 190 | 191 | ### 2.3.1 使用默认电子邮件服务 (Gmail) 来处理通知服务 192 | 193 | 194 | 您将需要修改 `send-notification.yaml` 中的 **环境变量**: 195 | ```yaml 196 | env: 197 | - name: GMAIL_SENDER_USER 198 | value: 'username@gmail.com' # change this to the gmail that will send the email 199 | - name: GMAIL_SENDER_PASSWORD 200 | value: 'password' # change this to the the password of the gmail above 201 | - name: EMAIL_RECEIVER 202 | value: 'sendTo@gmail.com' # change this to the email of the receiver 203 | ``` 204 | 205 | 现在,您可以继续执行[步骤 2.4](#24-deploy-the-spring-boot-microservices)。 206 | 207 | ### 2.3.2 使用 OpenWhisk Action 来处理通知服务 208 | 本部分的要求: 209 | * 您的 Slack 团队中具有 [Slack Incoming Webhook](https://api.slack.com/incoming-webhooks)。 210 | * **IBM Cloud帐户**,以便使用 [OpenWhisk CLI](https://console.ng.bluemix.net/openwhisk/)。 211 | 212 | 213 | #### 2.3.2.1 创建 Actions 214 | 本代码库的根目录中包含您创建 OpenWhisk Actions 时所需的代码。 215 | 如果您尚未安装 OpenWhisk CLI,请转至[此处](https://console.ng.bluemix.net/openwhisk/)。 216 | 您可以使用 `wsk` 命令来创建 OpenWhisk Actions。创建操作使用以下语法:`wsk action create < action_name > < source code for action> [add --param for optional Default parameters]` 217 | * 创建用于发送 **Slack 通知** 的 Action 218 | ```bash 219 | $ wsk action create sendSlackNotification sendSlack.js --param url https://hooks.slack.com/services/XXXX/YYYY/ZZZZ 220 | Replace the url with your Slack team's incoming webhook url. 221 | ``` 222 | * 创建用于发送 **Gmail 通知** 的 Action 223 | ```bash 224 | $ wsk action create sendEmailNotification sendEmail.js 225 | ``` 226 | 227 | #### 2.3.2.2 测试 Actions 228 | 您可以使用 `wsk action invoke [action name] [add --param to pass parameters]` 测试 OpenWhisk Actions 229 | * 调用 Slack 通知 230 | ```bash 231 | $ wsk action invoke sendSlackNotification --param text "Hello from OpenWhisk" 232 | ``` 233 | * 调用电子邮件通知 234 | ```bash 235 | $ wsk action invoke sendEmailNotification --param sender [sender's email] --param password [sender's password]--param receiver [receiver's email] --param subject [Email subject] --param text [Email Body] 236 | ``` 237 | 至此,您应分别收到一条 Slack 消息和一封电子邮件。 238 | 239 | #### 2.3.2.3 为 Actions 创建 REST API 240 | 您可以使用 `wsk api create` 为创建的 Action 映射 REST API 端点。其语法为 `wsk api create [base-path] [api-path] [verb (GET PUT POST etc)] [action name]` 241 | * 创建用于 **Slack 通知** 的 REST API 端点 242 | ```bash 243 | $ wsk api create /v1 /slack POST sendSlackNotification 244 | ok: created API /v1/email POST for action /_/sendEmailNotification 245 | https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/slack 246 | ``` 247 | * 创建用于 **Gmail 通知** 的 REST API 端点 248 | ```bash 249 | $ wsk api create /v1 /email POST sendEmailNotification 250 | ok: created API /v1/email POST for action /_/sendEmailNotification 251 | https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/email 252 | ``` 253 | 254 | 您可以使用以下命令查看 API 列表: 255 | ```bash 256 | $ wsk api list 257 | ok: APIs 258 | Action Verb API Name URL 259 | /Anthony.Amanse_dev/sendEmailNotificatio post /v1 https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/email 260 | /Anthony.Amanse_dev/testDefault post /v1 https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/slack 261 | ``` 262 | 263 | 请记录 这些 API URL,稍后我们将使用它们 。 264 | 265 | #### 2.3.2.4 测试 REST API URL 266 | 267 | * 测试用于 **Slack 通知** 的 REST API 端点。这里请使用您自己的 API URL。 268 | ```bash 269 | $ curl -X POST -d '{ "text": "Hello from OpenWhisk" }' https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/slack 270 | ``` 271 | ![Slack 通知](images/slackNotif.png) 272 | * 测试用于 **Gmail 通知** 的 REST API 端点。这里请使用您自己的 API URL。将参数 **sender、password、receiver 和 subject** 的值替换为您自己的值。 273 | ```bash 274 | $ curl -X POST -d '{ "text": "Hello from OpenWhisk", "subject": "Email Notification", "sender": "testemail@gmail.com", "password": "passwordOfSender", "receiver": "receiversEmail" }' https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/email 275 | ``` 276 | ![电子邮件通知](images/emailNotif.png) 277 | 278 | #### 2.3.2.5 将 REST API URL 添加到 yaml 文件中 279 | 一旦确认您的 API 运行正常,就可以将这些 URL 放入 `send-notification.yaml` 文件中了 280 | ```yaml 281 | env: 282 | - name: GMAIL_SENDER_USER 283 | value: 'username@gmail.com' # the sender's email 284 | - name: GMAIL_SENDER_PASSWORD 285 | value: 'password' # the sender's password 286 | - name: EMAIL_RECEIVER 287 | value: 'sendTo@gmail.com' # the receiver's email 288 | - name: OPENWHISK_API_URL_SLACK 289 | value: 'https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/slack' # your API endpoint for slack notifications 290 | - name: SLACK_MESSAGE 291 | value: 'Your balance is over $50,000.00' # your custom message 292 | - name: OPENWHISK_API_URL_EMAIL 293 | value: 'https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/email' # your API endpoint for email notifications 294 | ``` 295 | 296 | 297 | 298 | 299 | ## 2.4 部署 Spring Boot 微服务 300 | ```bash 301 | $ kubectl create -f compute-interest-api.yaml 302 | service "compute-interest-api" created 303 | deployment "compute-interest-api" created 304 | ``` 305 | ```bash 306 | $ kubectl create -f send-notification.yaml 307 | service "send-notification" created 308 | deployment "send-notification" created 309 | ``` 310 | 311 | # 3.创建前端服务 312 | 此用户界面是 Node.js 应用程序,可显示账户总余额。 313 | **如果您在 IBM Cloud 中使用 MySQL 数据库,请记得填充 `account-summary.yaml` 文件中环境变量的值,否则请将其留空。这是在[步骤 1](#1-create-the-database-service) 中执行的操作。** 314 | 315 | 316 | * 创建 **Node.js** 前端: 317 | ```bash 318 | $ kubectl create -f account-summary.yaml 319 | service "account-summary" created 320 | deployment "account-summary" created 321 | ``` 322 | 323 | # 4.创建交易生成器服务 324 | 交易生成器是 Python 应用程序,可使用累积利息生成随机交易。 325 | * 创建交易生成器 **Python** 应用程序: 326 | ```bash 327 | $ kubectl create -f transaction-generator.yaml 328 | service "transaction-generator" created 329 | deployment "transaction-generator" created 330 | ``` 331 | 332 | # 5.访问应用程序 333 | 您可以通过 Kubernetes Cluster IP 和 NodePort 访问应用程序。NodePort 应为 **30080**。 334 | 335 | * 要查找 Cluster IP,请执行以下命令: 336 | ```bash 337 | $ bx cs workers 338 | ID Public IP Private IP Machine Type State Status 339 | kube-dal10-paac005a5fa6c44786b5dfb3ed8728548f-w1 169.47.241.213 10.177.155.13 free normal Ready 340 | ``` 341 | 342 | * 要查找账户摘要 (account-summary) 服务的 NodePort,请执行以下命令: 343 | ```bash 344 | $ kubectl get svc 345 | NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE 346 | ... 347 | account-summary 10.10.10.74 80:30080/TCP 2d 348 | ... 349 | ``` 350 | * 在您的浏览器上,转至 `http://:30080` 351 | ![账户余额](images/balance.png) 352 | 353 | ## 故障排除 354 | * 要从头开始,请删除所有内容:`kubectl delete svc,deploy -l app=office-space` 355 | 356 | 357 | ## 参考资料 358 | * [John Zaccone](https://github.com/jzaccone) - [通过 Docker 部署的 Office Space 应用程序](https://github.com/jzaccone/office-space-dockercon2017) 的原始作者。 359 | * Office Space 应用程序是以 1999 年运用此理念的同名电影为基础编写的。 360 | 361 | ## 许可 362 | [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0) 363 | 364 | # 隐私声明 365 | 366 | 可以配置包含这个包的样本 Kubernetes Yaml 文件,以跟踪对 [IBM Cloud ](https://www.bluemix.net/) 和其他 Kubernetes 平台的部署。每次部署时,都会将以下信息发送到 [Deployment Tracker](https://github.com/IBM/metrics-collector-service) 服务: 367 | 368 | * Kubernetes 集群提供者(`IBM Cloud、Minikube 等`) 369 | * Kubernetes 机器 ID (`MachineID`) 370 | * 这个 Kubernetes 作业中的环境变量。 371 | 372 | 此数据是从样本应用程序的 yaml 文件中的 Kubernetes Job 收集而来。IBM 使用此数据来跟踪将样本应用程序部署到 IBM Cloud 相关的指标,以度量我们的示例的实用性,从而让我们能够持续改进为您提供的内容。仅跟踪那些包含代码以对 Deployment Tracker 服务执行 ping 操作的样本应用程序的部署过程。 373 | 374 | ## 禁用部署跟踪 375 | 376 | 请注释掉/移除 `account-summary.yaml` 文件末尾的 Kubernetes Job 部分。 377 | -------------------------------------------------------------------------------- /README-ko.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/IBM/spring-boot-microservices-on-kubernetes.svg?branch=master)](https://travis-ci.org/IBM/spring-boot-microservices-on-kubernetes) 2 | ![Bluemix Deployments](https://metrics-tracker.mybluemix.net/stats/13404bda8d87a6eca2c5297511ae9a5e/badge.svg) 3 | 4 | # 쿠버네티스에 자바 Spring Boot 애플리케이션 빌드 및 배포하기 5 | 6 | *Read this in other languages: [English](README.md).* 7 | 8 | Spring Boot은 널리 사용되는 자바 마이크로서비스 프레임워크 입니다. Spring Cloud는 Java 애플리케이션 스택의 일부로 런타임 문제를 해결할 수 있는 풍부한 통합 라이브러리를 갖고 있으며 쿠버네티스는 polyglot 마이크로서비스를 실행할 수 있는 풍부한 기능을 제공합니다. 이 두 기술은 서로를 보완하고 있으며 Spring Boot 애플리케이션을 위한 훌륭한 플랫폼을 만듭니다. 9 | 10 | 이 코드에서는 쿠버네티스 위에 간단한 Spring Boot 애플리케이션을 배포하는 방법을 보여줍니다. 이 애플리케이션인 Office Space는 영화 [Office Space](http://www.imdb.com/title/tt0151804/)에 등장하는 Michael Bolton의 가상의 앱 아이디어를 모방한 것입니다. 이 앱은 일반적으로 1센트 이하의 금액을 반올림 하는 방식 대신에 버림하여 별도의 은행 계좌에 저장하는 방식으로 이득을 취합니다. 11 | 12 | 이 애플리케이션은 Java 8/Spring Boot 마이크로서비스를 사용하여 이자를 계산하고 소숫점 이하의 금액은 데이터베이스에 저장합니다. 또 다른 Spring Boot 마이크로서비스인 알림 서비스는 계좌의 잔액이 $50,000 이상이 되면 이메일을 전송합니다. 이는 이자를 계산하는 Spring Boot 웹서버에 의해 트리거 됩니다. 앱의 Frontend는 Node.js 기반으로 만들어졌고 Spring Boot 앱에 의해 쌓인 현재 계좌의 잔액을 보여줍니다. Backend는 MySQL 데이터 베이스를 사용하여 계좌 잔액을 저장합니다. 13 | 14 | ![spring-boot-kube](images/spring-boot-kube.png) 15 | 16 | ## 사전 준비 사항 17 | 18 | 쿠버네티스 클러스터를 생성합니다. 로컬에서 테스트 하려면 [Minikube](https://kubernetes.io/docs/getting-started-guides/minikube)에서, 클라우드에 배포하려면 [IBM Bluemix Container Service](https://github.com/IBM/container-journey-template)에서 생성합니다. 이 코드는 Travis를 사용하여 [Kubernetes Cluster from Bluemix Container Service](https://console.ng.bluemix.net/docs/containers/cs_ov.html#cs_ov)에서 정기적으로 테스트 합니다. 19 | 20 | ## Steps 21 | 1. [데이터베이스 서비스 생성](#1-데이터베이스-서비스-생성) 22 | 1.1 [컨테이너에서 MySQL 사용](#11-컨테이너에서-mysql-사용) 또는 23 | 1.2 [Bluemix MySQL 사용](#12-bluemix-mysql-사용) 24 | 2. [Spring Boot 마이크로서비스 생성](#2-spring-boot-마이크로서비스-생성) 25 | 2.1 [Maven으로 프로젝트 빌드](#21-maven으로-프로젝트-빌드) 26 | 2.2 [닥커 이미지 빌드 및 푸시](#22-닥커-이미지-빌드-및-푸시) 27 | 2.3 [Spring Boot 서비스를 위한 yaml 파일 수정](#23-compute-interest-apiyaml-및-send-notificationyaml-수정) 28 |       2.3.1 [알림 서비스로 기본 이메일 서비스 사용](#231-알림-서비스로-기본-이메일-서비스-사용-gmail) 또는 29 |       2.3.2 [알림 서비스로 OpenWhisk Actions 사용](#232-알림-서비스로-openwhisk-actions-사용) 30 | 2.4 [Spring Boot 마이크로서비스 배포](#24-spring-boot-마이크로서비스-배포) 31 | 3. [Frontend 서비스 작성](#3-frontend-서비스-작성) 32 | 4. [트랜잭션 생성 서비스 작성](#4-트랜잭션-생성-서비스-작성) 33 | 5. [애플리케이션 접근](#5-애플리케이션-접근) 34 | 35 | #### [문제 해결](#문제-해결) 36 | 37 | # 1. 데이터베이스 서비스 생성 38 | 39 | 백엔드 시스템은 MySQL 데이터베이스와 Spring Boot 애플리케이션으로 구성되어 있습니다. 각 마이크로서비스는 Deployment와 Service를 갖고 있습니다. Deployment는 각 마이크로서비스에 대해 시작된 Pod를 관리합니다. 서비스는 각 마이크로서비스에 대해 이름으로 dependency를 참조하도록 안정적인 DNS를 생성합니다. 40 | 41 | * MySQL 데이터베이스를 생성하는 방법은 두가지가 있습니다.: 42 | **[컨테이너에서 MySQL 사용](#11-컨테이너에서-mysql-사용)** *또는* 43 | **[Bluemix MySQL 사용](#12-bluemix-mysql-사용)** 44 | 45 | ## 1.1 컨테이너에서 MySQL 사용 46 | ```bash 47 | $ kubectl create -f account-database.yaml 48 | service "account-database" created 49 | deployment "account-database" created 50 | ``` 51 | 52 | 기본 신임 정보는 이미 secrets.yaml에 base64로 인코딩 되어 있습니다. 53 | > base64 인코딩은 신임 정보를 암호화하지는 않으므로 이 정보를 Github에 업로드 하지 마십시오. 54 | 55 | ``` 56 | $ kubectl apply -f secrets.yaml 57 | secret "demo-credentials" created 58 | ``` 59 | 60 | [Step 2](#2-spring-boot-마이크로서비스-생성)에서 계속 진행 하십시오. 61 | 62 | ## 1.2 Bluemix MySQL 사용 63 | https://console.ng.bluemix.net/catalog/services/compose-for-mysql를 통해 Bluemix에서 Compose for MySQL을 작성하십시오. 64 | Service Credentials로 가서 신임 정보를 확인하십시오. MySQL의 Hostname, port, user 그리고 password가 신임 정보의 url에 있습니다. 다음의 형식으로 되어 있습니다. 65 | ![images](images/mysqlservice.png) 66 | 이 신임정보를 큐버네티스 클러스터에 Secret으로 적용해야 합니다. 이 값은 반드시 `base64`로 인코딩 되어 있어야 합니다. `./scripts/create-secrets.sh`를 사용하십시오. 신임 정보를 넣도록 입력을 받을 것입니다. 이 스크립트를 통해 신임 정보를 인코딩 하고 클러스터에 Secret으로 적용할 수 있습니다. 67 | ```bash 68 | $ ./scripts/create-secrets.sh 69 | Enter MySQL username: 70 | admin 71 | Enter MySQL password: 72 | password 73 | Enter MySQL host: 74 | hostname 75 | Enter MySQL port: 76 | 23966 77 | secret "demo-credentials" created 78 | ``` 79 | 80 | _또한 `secrets.yaml` 파일을 수정하여 base64 인코딩된 신임정보를 직접 넣는 방법을 사용할 수 있습니다. 이 경우에는 `kubectl apply -f secrets.yaml` 를 수행하십시오._ 81 | 82 | [Step 2](#2-spring-boot-마이크로서비스-생성)에서 계속 진행 하십시오. 83 | 84 | # 2. Spring Boot 마이크로서비스 생성 85 | [Maven이 설치되어 있어야 합니다.](https://maven.apache.org/index.html). 86 | Spring Boot 애플리케이션들을 수정하려면 Java 프로젝트와 닥커 이미지를 빌드하기 전에 해야 합니다. 87 | 88 | Spring Boot 마이크로서비스는 **Compute-Interest-API** 와 **Send-Notification** 입니다. 89 | 90 | **Compute-Interest-API**은 MySQL 데이터베이스를 사용하기위해 구성된 Spring Boot 앱입니다. 관련한 구성은 `spring.datasource.*`에 있는 application.properties에 위치해 있습니다. 91 | 92 | *compute-interest-api/src/main/resources/application.properties* 93 | ``` 94 | spring.datasource.url = jdbc:mysql://${MYSQL_DB_HOST}:${MYSQL_DB_PORT}/dockercon2017 95 | 96 | # Username과 password 97 | spring.datasource.username = ${MYSQL_DB_USER} 98 | spring.datasource.password = ${MYSQL_DB_PASSWORD} 99 | ``` 100 | 101 | `application.properties`는 MYSQL_DB_* 환경 변수를 사용하기 위해 구성됐습니다. 이는 `compute-interest-api.yaml` 파일에 정의되어 있습니다. 102 | *compute-interest-api.yaml* 103 | ```yaml 104 | spec: 105 | containers: 106 | - image: anthonyamanse/compute-interest-api:secrets 107 | imagePullPolicy: Always 108 | name: compute-interest-api 109 | env: 110 | - name: MYSQL_DB_USER 111 | valueFrom: 112 | secretKeyRef: 113 | name: demo-credentials 114 | key: username 115 | - name: MYSQL_DB_PASSWORD 116 | valueFrom: 117 | secretKeyRef: 118 | name: demo-credentials 119 | key: password 120 | - name: MYSQL_DB_HOST 121 | valueFrom: 122 | secretKeyRef: 123 | name: demo-credentials 124 | key: host 125 | - name: MYSQL_DB_PORT 126 | valueFrom: 127 | secretKeyRef: 128 | name: demo-credentials 129 | key: port 130 | ports: 131 | - containerPort: 8080 132 | ``` 133 | 134 | 이 YAML 파일은 이미 이전 단계에서 생성한 쿠버네티스 Secret으로부터 값을 얻도록 구성되어 있습니다. 이는 `application.properties`에 있는 Spring Boot 애플리케이션에서 사용될 것입니다. 135 | 136 | **Send-Notification** 은 gmail 그리고/또는 Slack을 통해 알림을 줄 수 있도록 구성되어 있습니다. 알림은 MySQL 데이터베이스의 계좌 잔액이 $50,000을 넘었을 때에 한번만 발송됩니다. 기본적으로는 gmail 옵션을 사용합니다. Event driven 기술을 사용할 수도 있습니다. 여기서는 [OpenWhisk](http://openwhisk.org/)를 사용하여 이메일과 슬랙 메세지를 보냅니다. 알림 마이크로서비스로 OpenWhisk를 사용하려면 마이크로서비스 이미지를 빌드 및 배포 하기 전에 [여기](#232-알림-서비스로-openwhisk-actions-사용) 스텝을 따르십시오. 이메일 알림 설정만 사용하려면 그냥 진행하십시오. 137 | 138 | ## 2.1. Maven으로 프로젝트 빌드 139 | 140 | Maven이 Java 프로젝트를 성공적으로 빌드하면, 각 폴더에 제공된 **Dockerfile**로 닥커 이미지를 빌드해야 합니다. 141 | > Note: compute-interest-api는 시뮬레이션 목적으로 나머지 금액에 x100,000을 곱합니다. `src/main/java/officespace/controller/MainController.java`에서 `remainingInterest *= 100000`라인을 수정/제거할 수 있습니다. 또한 잔액이 $50,000을 넘길 때 알림을 전송하도록 되어 있습니다. `if (updatedBalance > 50000 && emailSent == false )` 라인에서 이 숫자를 변경하여 사용하십시오. 변경 사항을 저장하면 이제 프로젝트를 빌드하십시오. 142 | 143 | ```bash 144 | Go to containers/compute-interest-api 145 | $ mvn package 146 | 147 | Go to containers/send-notification 148 | $ mvn package 149 | 150 | ``` 151 | *이 과정에서는 Bluemix 컨테이너 레지스트리를 사용합니다. [닥커 허브](https://docs.docker.com/datacenter/dtr/2.2/guides/user/manage-images/pull-and-push-images)를 사용할 수도 있습니다.* 152 | 153 | ## 2.2 닥커 이미지 빌드 및 푸시 154 | > Note: Bluemix 컨테이너 레지스트리로 이미지를 푸시합니다. 155 | 156 | Bluemix 컨테이너 레지스트리를 사용하려면 먼저 계정을 구성해야 합니다. [이 튜토리얼](https://developer.ibm.com/recipes/tutorials/getting-started-with-private-registry-hosted-by-ibm-bluemix/)을 따르십시오. 157 | 158 | [Docker Hub](https://hub.docker.com)를 사용할 수도 있습니다. 159 | 160 | ```bash 161 | $ docker build -t registry.ng.bluemix.net//compute-interest-api . 162 | $ docker build -t registry.ng.bluemix.net//send-notification . 163 | $ docker push registry.ng.bluemix.net//compute-interest-api 164 | $ docker push registry.ng.bluemix.net//send-notification 165 | ``` 166 | ## 2.3 *compute-interest-api.yaml* 및 *send-notification.yaml* 수정 167 | 168 | 이미지를 성공적으로 푸시했으면 yaml 파일을 수정하여 푸시한 이미지를 사용하도록 변경합니다. 169 | ```yaml 170 | // compute-interest-api.yaml 171 | spec: 172 | containers: 173 | - image: registry.ng.bluemix.net//compute-interest-api # 푸시한 이미지 이름으로 대체하십시오. 174 | ``` 175 | 176 | ```yaml 177 | // send-notification.yaml 178 | spec: 179 | containers: 180 | - image: registry.ng.bluemix.net//send-notification # 푸시한 이미지 이름으로 대체하십시오. 181 | ``` 182 | 183 | 두가지 타입의 알림이 가능합니다. 184 | [2.3.1 Use default email service](#231-알림-서비스로-기본-이메일-서비스-사용-gmail) 185 | **또는** 186 | [2.3.2 Use OpenWhisk Actions](#232-알림-서비스로-openwhisk-actions-사용). 187 | 188 | ### 2.3.1 알림 서비스로 기본 이메일 서비스 사용 (gmail) 189 | 190 | `send-notification.yaml` 에서 **환경 변수**를 수정하십시오.: 191 | ```yaml 192 | env: 193 | - name: GMAIL_SENDER_USER 194 | value: 'username@gmail.com' # 이메일을 전송할 gmail로 변경하십시오. 195 | - name: GMAIL_SENDER_PASSWORD 196 | value: 'password' # 위에 입력한 gmail의 패스워드로 변경하십시오. 197 | - name: EMAIL_RECEIVER 198 | value: 'sendTo@gmail.com' # 수신자의 이메일 주소로 변경하십시오. 199 | ``` 200 | 201 | 이제 [Step 2.4](#24-spring-boot-마이크로서비스-배포)를 진행하십시오. 202 | 203 | ### 2.3.2 알림 서비스로 OpenWhisk Actions 사용 204 | 이 섹션에서 필요한 것들: 205 | * Slack 팀의 [Slack Incoming Webhook](https://api.slack.com/incoming-webhooks). 206 | * [OpenWhisk CLI](https://console.ng.bluemix.net/openwhisk/)를 사용하기 위해 **Bluemix 계정**이 필요합니다. 207 | 208 | 209 | #### 2.3.2.1 Actions 생성 210 | 이 리파지토리의 최상위 디렉토리에는 OpenWhisk Actions를 생성하는데 필요한 코드가 있습니다. 아직 OpenWhisk CLI를 설치하지 않았다면 먼저 [여기](https://console.ng.bluemix.net/openwhisk/)에서 설치하십시오. 211 | 212 | `wsk` 명령을 사용해서 OpenWhisk Actions를 새엇ㅇ할 수 있습니다. Action을 생성하는 문법은 `wsk action create < action_name > < source code for action> [add --param for optional Default parameters]` 입니다. 213 | 214 | * **Slack 알림**을 보내기 위한 Action 생성 215 | ```bash 216 | $ wsk action create sendSlackNotification sendSlack.js --param url https://hooks.slack.com/services/XXXX/YYYY/ZZZZ 217 | Replace the url with your Slack team's incoming webhook url. 218 | ``` 219 | * **Gmail 알림**을 전송하기 위한 Action 생성 220 | ```bash 221 | $ wsk action create sendEmailNotification sendEmail.js 222 | ``` 223 | 224 | #### 2.3.2.2 Actions 테스트 225 | 226 | 다음을 사용하여 OpenWhisk Actions를 테스트할 수 있습니다. 227 | `wsk action invoke [action name] [add --param to pass parameters]` 228 | 229 | * 슬랙 알림 호출하기 230 | ```bash 231 | $ wsk action invoke sendSlackNotification --param text "Hello from OpenWhisk" 232 | ``` 233 | * 이메일 알림 호출하기 234 | ```bash 235 | $ wsk action invoke sendEmailNotification --param sender [sender's email] --param password [sender's password]--param receiver [receiver's email] --param subject [Email subject] --param text [Email Body] 236 | ``` 237 | 각각의 명령을 통해 슬랙 메세지와 이메일을 받을 수 있습니다. 238 | 239 | #### 2.3.2.3 Actions에 대한 REST API 생성 240 | `wsk api create` 명령을 사용하여 REST API 엔드포인트와 생성된 Actions를 맵핑할 수 있습니다. 이 명령을 사용하는 문법은 `wsk api create [base-path] [api-path] [verb (GET PUT POST etc)] [action name]` 입니다. 241 | 242 | * **Slack 알림**에 대한 엔드포인트 생성 243 | ```bash 244 | $ wsk api create /v1 /slack POST sendSlackNotification 245 | ok: created API /v1/email POST for action /_/sendEmailNotification 246 | https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/slack 247 | ``` 248 | * **Gmail 알림**에 대한 엔드포인트 생성 249 | ```bash 250 | $ wsk api create /v1 /email POST sendEmailNotification 251 | ok: created API /v1/email POST for action /_/sendEmailNotification 252 | https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/email 253 | ``` 254 | 255 | 다음 명령으로 생성한 API 목록을 확인할 수 있습니다.: 256 | ```bash 257 | $ wsk api list 258 | ok: APIs 259 | Action Verb API Name URL 260 | /Anthony.Amanse_dev/sendEmailNotificatio post /v1 https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/email 261 | /Anthony.Amanse_dev/testDefault post /v1 https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/slack 262 | ``` 263 | 264 | 각자의 API URL을 편한 방법으로 기록해 둡니다. 이 URL은 다음 단계에서 다시 사용됩니다. 265 | 266 | #### 2.3.2.4 REST API Url 테스트 267 | 268 | * **Slack 알림**의 엔드포인트를 테스트 합니다. URL 부분을 각자의 API URL로 대체하십시오. 269 | ```bash 270 | $ curl -X POST -d '{ "text": "Hello from OpenWhisk" }' https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/slack 271 | ``` 272 | ![Slack Notification](images/slackNotif.png) 273 | * **Gmail 알림**의 엔드포인트를 테스트 합니다. URL 부분을 각자의 API URL로 대체하십시오. **sender, password, receiver, subject** 파라미터의 값을 각자의 값으로 대체하십시오. 274 | ```bash 275 | $ curl -X POST -d '{ "text": "Hello from OpenWhisk", "subject": "Email Notification", "sender": "testemail@gmail.com", "password": "passwordOfSender", "receiver": "receiversEmail" }' https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/email 276 | ``` 277 | ![Email Notification](images/emailNotif.png) 278 | 279 | #### 2.3.2.5 REST API Url을 yaml 파일에 추가 280 | API가 잘 작동하는 것을 테스트 한 후에는 이 URL을 `send-notification.yaml` file 에 입력합니다. 281 | ```yaml 282 | env: 283 | - name: GMAIL_SENDER_USER 284 | value: 'username@gmail.com' # 발신자의 이메일 285 | - name: GMAIL_SENDER_PASSWORD 286 | value: 'password' # 발신자 이메일의 password 287 | - name: EMAIL_RECEIVER 288 | value: 'sendTo@gmail.com' # 수신자의 이메일 289 | - name: OPENWHISK_API_URL_SLACK 290 | value: 'https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/slack' # Slack 알림의 API 엔드포인트 URL 291 | - name: SLACK_MESSAGE 292 | value: 'Your balance is over $50,000.00' # 메세지 293 | - name: OPENWHISK_API_URL_EMAIL 294 | value: 'https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/email' # 이메일 알림의 API 엔드포인트 URL 295 | ``` 296 | 297 | 298 | 299 | 300 | ## 2.4 Spring Boot 마이크로서비스 배포 301 | ```bash 302 | $ kubectl create -f compute-interest-api.yaml 303 | service "compute-interest-api" created 304 | deployment "compute-interest-api" created 305 | ``` 306 | ```bash 307 | $ kubectl create -f send-notification.yaml 308 | service "send-notification" created 309 | deployment "send-notification" created 310 | ``` 311 | 312 | # 3. Frontend 서비스 작성 313 | UI는 Node.js 앱으로 전체 계좌의 잔액을 보여줍니다. 314 | **블루믹스의 MySQL 데이터베이스를 사용중이면, 환경 변수를 `account-summary.yaml` 파일에 넣어야 합니다. 블루믹스의 MySQL을 사용하지 않는다면 빈 상태로 두십시오. 이는 [Step 1](#1-데이터베이스-서비스-생성)에서 이미 했습니다.** 315 | 316 | 317 | * *Node.js**기반 Frontend UI 생성: 318 | ```bash 319 | $ kubectl create -f account-summary.yaml 320 | service "account-summary" created 321 | deployment "account-summary" created 322 | ``` 323 | 324 | # 4. 트랜잭션 생성 서비스 작성 325 | 트랜잭션 생성 서비스는 Python 앱으로 축적된 이자로 랜덤한 트랜잭션 생성합니다. 326 | * 트랜잭션 생성을 위한 **Python** 앱 작성: 327 | ```bash 328 | $ kubectl create -f transaction-generator.yaml 329 | service "transaction-generator" created 330 | deployment "transaction-generator" created 331 | ``` 332 | 333 | # 5. 애플리케이션 접근 334 | Cluster IP와 NodePort를 통해 애플리케이션에 퍼블릭 네트워크로 접근할 수 있습니다. NodePort는 **30080**입니다. 335 | 336 | * IP 찾기: 337 | ```bash 338 | $ bx cs workers 339 | ID Public IP Private IP Machine Type State Status 340 | kube-dal10-paac005a5fa6c44786b5dfb3ed8728548f-w1 169.47.241.213 10.177.155.13 free normal Ready 341 | ``` 342 | 343 | * account-summary 서비스의 NodePort 찾기: 344 | ```bash 345 | $ kubectl get svc 346 | NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE 347 | ... 348 | account-summary 10.10.10.74 80:30080/TCP 2d 349 | ... 350 | ``` 351 | * 브라우저에서, `http://:30080`에 접속하십시오. 352 | ![Account-balance](images/balance.png) 353 | 354 | ## 문제 해결 355 | * 다시 시작하려면 모두 삭제하십시오. `kubectl delete svc,deploy -l app=office-space` 356 | 357 | 358 | ## 참조 359 | * [John Zaccone](https://github.com/jzaccone) - [office space app deployed via Docker](https://github.com/jzaccone/office-space-dockercon2017)의 원 저자. 360 | * Office Space 앱은 1999년도 영화 Office Space의 컨셉에 기반했습니다. 361 | 362 | ## 라이센스 363 | [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0) 364 | 365 | # 정보 사용 안내 366 | 367 | 이 패키지가 포함 된 샘플 쿠버네티스 Yaml 파일은 [IBM Cloud](https://www.bluemix.net/) 및 기타 쿠버네티스 플랫폼에 대한 배치를 추적하도록 구성 될 수 있습니다. 다음 정보는 배포시마다 [배치 추적 서비스](https://github.com/IBM/metrics-collector-service)로 전송됩니다.: 368 | 369 | * 쿠버네티스 클러스터 제공자 (`Bluemix,Minikube 등`) 370 | * 쿠버네티스 Machine ID (`MachineID`) 371 | * 이 쿠버네티스 Job의 환경 변수 372 | 373 | 이 데이터는 샘플 애플리케이션의 yaml 파일의 쿠버네티스 Job으로부터 수집됩니다. 이 데이터는 IBM에서 지속적으로 더 나은 컨텐츠를 제공하기 위해 사용됩니다. 예제의 유용성을 측정하기 위해 IBM Cloud로 배포되는 샘플 애플리케이션의 배포를 관련된 측정 항목을 추적하는데에 사용합니다.배포 추적 서비스를 핑하는 코드가 포함 된 샘플 응용 프로그램의 배포 만 추적됩니다. 374 | 375 | ## 배포 추적 비활성화 376 | 377 | `account-summary.yaml` 파일의 끝 부분에 있는 쿠버네티스 Job 부분을 주석 처리 하거나 삭제하십시오. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/IBM/spring-boot-microservices-on-kubernetes.svg?branch=master)](https://travis-ci.org/IBM/spring-boot-microservices-on-kubernetes) 2 | 3 | # Build and deploy Java Spring Boot microservices on Kubernetes 4 | 5 | *Read this in other languages: [한국어](README-ko.md)、[中国](README-cn.md).* 6 | 7 | Spring Boot is one of the popular Java microservices framework. Spring Cloud has a rich set of well integrated Java libraries to address runtime concerns as part of the Java application stack, and Kubernetes provides a rich featureset to run polyglot microservices. Together these technologies complement each other and make a great platform for Spring Boot applications. 8 | 9 | In this code we demonstrate how a simple Spring Boot application can be deployed on top of Kubernetes. This application, Office Space, mimicks the fictitious app idea from Michael Bolton in the movie [Office Space](http://www.imdb.com/title/tt0151804/). The app takes advantage of a financial program that computes interest for transactions by diverting fractions of a cent that are usually rounded off into a seperate bank account. 10 | 11 | The application uses a Java 8/Spring Boot microservice that computes the interest then takes the fraction of the pennies to a database. Another Spring Boot microservice is the notification service. It sends email when the account balance reach more than $50,000. It is triggered by the Spring Boot webserver that computes the interest. The frontend uses a Node.js app that shows the current account balance accumulated by the Spring Boot app. The backend uses a MySQL database to store the account balance. 12 | 13 | ## Flow 14 | 15 | ![spring-boot-kube](images/architecture.png) 16 | 17 | 1. The Transaction Generator service written in Python simulates transactions and pushes them to the Compute Interest microservice. 18 | 2. The Compute Interest microservice computes the interest and then moves the fraction of pennies to the MySQL database to be stored. The database can be running within a container in the same deployment or on a public cloud such as IBM Cloud. 19 | 3. The Compute Interest microservice then calls the notification service to notify the user if an amount has been deposited in the user’s account. 20 | 4. The Notification service uses IBM Cloud Function to send an email message to the user. 21 | 5. Additionally, an IBM Cloud Function to send messages to Slack can also be invoked. 22 | 6. The user retrieves the account balance by visiting the Node.js web interface. 23 | 24 | ## Included Components 25 | 26 | * [IBM Cloud Kubernetes Service](https://console.bluemix.net/docs/containers/container_index.html): IBM Bluemix Container Service manages highly available apps inside Docker containers and Kubernetes clusters on the IBM Cloud. 27 | * [Compose for MySQL](https://console.ng.bluemix.net/catalog/services/compose-for-mysql): Probably the most popular open source relational database in the world. 28 | * [IBM Cloud Functions](https://console.ng.bluemix.net/openwhisk): Execute code on demand in a highly scalable, serverless environment. 29 | 30 | ## Featured Technologies 31 | 32 | * [Container Orchestration](https://www.ibm.com/cloud/container-service): Automating the deployment, scaling and management of containerized applications. 33 | * [Databases](https://en.wikipedia.org/wiki/IBM_Information_Management_System#.22Full_Function.22_databases): Repository for storing and managing collections of data. 34 | * [Serverless](https://www.ibm.com/cloud/functions): An event-action platform that allows you to execute code in response to an event. 35 | 36 | # Prerequisite 37 | 38 | * Create a Kubernetes cluster with either [Minikube](https://kubernetes.io/docs/getting-started-guides/minikube) for local testing, [IBM Cloud Private](https://github.com/IBM/Kubernetes-container-service-GitLab-sample/blob/master/docs/deploy-with-ICP.md), or with [IBM Cloud Kubernetes Service](https://github.com/IBM/container-journey-template) to deploy in cloud. The code here is regularly tested against [Kubernetes Cluster from IBM Cloud](https://console.ng.bluemix.net/docs/containers/cs_ov.html#cs_ov) using Travis. 39 | 40 | * [Slack Incoming Webhook](https://api.slack.com/incoming-webhooks) in your Slack team. _(If you want to receive a test notification in Step 4.)_ 41 | 42 | * [IBM Cloud Function CLI](https://console.bluemix.net/openwhisk/learn/cli/) to create IBM Cloud Functions. _(If you want to do Step 4.)_ 43 | 44 | 45 | # Steps 46 | 1. [Clone the repo](#1-clone-the-repo) 47 | 2. [Create the Database service](#2-create-the-database-service) 48 | 3. [Create the Spring Boot Microservices](#3-create-the-spring-boot-microservices) 49 | 4. [Use IBM Cloud Functions with Notification service *(Optional)*](#4-use-ibm-cloud-functions-with-notification-service) 50 | 5. [Deploy the Microservices](#5-deploy-the-microservices) 51 | 6. [Access Your Application](#6-access-your-application) 52 | 53 | ### 1. Clone the repo 54 | 55 | Clone this repository. In a terminal, run: 56 | 57 | ``` 58 | $ git clone https://github.com/IBM/spring-boot-microservices-on-kubernetes 59 | ``` 60 | 61 | ### 2. Create the Database service 62 | 63 | The backend consists of a MySQL database and the Spring Boot app. Each 64 | microservice has a Deployment and a Service. The deployment manages 65 | the pods started for each microservice. The Service creates a stable 66 | DNS entry for each microservice so they can reference their 67 | dependencies by name. 68 | 69 | * There are two ways to create the MySQL database backend: 70 | **Use MySQL in container** *OR* 71 | **Use IBM Cloud Compose for MySQL** 72 | 73 | * Use MySQL in container _(Option 1)_ 74 | 75 | ```bash 76 | $ kubectl create -f account-database.yaml 77 | service "account-database" created 78 | deployment "account-database" created 79 | ``` 80 | Default credentials are already encoded in base64 in secrets.yaml. 81 | > Encoding in base64 does not encrypt or hide your secrets. Do not put this in your Github. 82 | 83 | ``` 84 | $ kubectl apply -f secrets.yaml 85 | secret "demo-credentials" created 86 | ``` 87 | 88 | Continue on in [Step 3](#3-create-the-spring-boot-microservices). 89 | 90 | * Use IBM Cloud Compose for MySQL _(Option 2)_ 91 | 92 | Provision [IBM Cloud Compose for MySQL](https://console.ng.bluemix.net/catalog/services/compose-for-mysql). Go to Service credentials and view your credentials. Your MySQL hostname, port, user, and password are under your credential uri and it should look like this 93 | ![images](images/mysqlservice.png) 94 | You will need to apply these credentials as a Secret in your Kubernetes cluster. It should be `base64` encoded. 95 | Use the script `./scripts/create-secrets.sh`. You will be prompted to enter your credentials. This will encode the credentials you input and apply them in your cluster as Secrets. 96 | 97 | ```bash 98 | $ ./scripts/create-secrets.sh 99 | Enter MySQL username: 100 | admin 101 | Enter MySQL password: 102 | password 103 | Enter MySQL host: 104 | hostname 105 | Enter MySQL port: 106 | 23966 107 | secret "demo-credentials" created 108 | ``` 109 | 110 | _You can also use the `secrets.yaml` file and edit the data values in it to your own base64 encoded credentials. Then do `kubectl apply -f secrets.yaml`._ 111 | 112 | ### 3. Create the Spring Boot Microservices 113 | You will need to have [Maven installed in your environment](https://maven.apache.org/index.html). 114 | If you want to modify the Spring Boot apps, you will need to do it before building the Java project and the docker image. 115 | 116 | The Spring Boot Microservices are the **Compute-Interest-API** and the **Send-Notification**. 117 | 118 | **Compute-Interest-API** is a Spring Boot app configured to use a MySQL database. The configuration is located in `compute-interest-api/src/main/resources/application.properties` in `spring.datasource.*` 119 | 120 | The `application.properties` is configured to use MYSQL_DB_* environment variables. These are defined in the `compute-interest-api.yaml` file. It is already configured to get the values from the Kubernetes Secrets that was created earlier. 121 | 122 | The **Send-Notification** can be configured to send notification through gmail and/or Slack. The notification is sent when the account balance on the MySQL database goes over $50,000. 123 | 124 | * Build your projects using Maven 125 | 126 | After Maven has successfully built the Java project, you will need to build the Docker image using the provided `Dockerfile` in their respective folders. 127 | > Note: The compute-interest-api multiplies the fraction of the pennies to x100,000 for simulation purposes. 128 | 129 | ```bash 130 | Go to containers/compute-interest-api 131 | $ mvn package 132 | 133 | Go to containers/send-notification 134 | $ mvn package 135 | 136 | ``` 137 | 138 | * Build your Docker images for Spring Boot services 139 | > Note: This is being pushed in the IBM Cloud Container Registry. 140 | 141 | If you plan to use IBM Cloud Container Registry, you will need to setup your account first. Follow the tutorial [here](https://developer.ibm.com/recipes/tutorials/getting-started-with-private-registry-hosted-by-ibm-bluemix/). 142 | 143 | *We will be using IBM Cloud container registry to push images (hence the image naming), but the images [can be pushed in Docker hub](https://docs.docker.com/datacenter/dtr/2.2/guides/user/manage-images/pull-and-push-images) as well.* 144 | 145 | ```bash 146 | $ docker build -t registry.ng.bluemix.net//compute-interest-api . 147 | $ docker build -t registry.ng.bluemix.net//send-notification . 148 | $ docker push registry.ng.bluemix.net//compute-interest-api 149 | $ docker push registry.ng.bluemix.net//send-notification 150 | ``` 151 | 152 | * Modify *compute-interest-api.yaml* and *send-notification.yaml* to use your image 153 | 154 | Once you have successfully pushed your images, you will need to modify the yaml files to use your images. 155 | ```yaml 156 | # compute-interest-api.yaml 157 | spec: 158 | containers: 159 | - image: registry.ng.bluemix.net//compute-interest-api # replace with your image name 160 | ``` 161 | 162 | ```yaml 163 | # send-notification.yaml 164 | spec: 165 | containers: 166 | - image: registry.ng.bluemix.net//send-notification # replace with your image name 167 | ``` 168 | 169 | There are two types of notifications possible, either `Using default email service with Notification service` or `Use IBM Cloud Functions with Notification Service` 170 | 171 | * Using default email service (gmail) with Notification service 172 | 173 | You will need to modify the **environment variables** in the `send-notification.yaml`: 174 | ```yaml 175 | env: 176 | - name: GMAIL_SENDER_USER 177 | value: 'username@gmail.com' # change this to the gmail that will send the email 178 | - name: GMAIL_SENDER_PASSWORD 179 | value: 'password' # change this to the the password of the gmail above 180 | - name: EMAIL_RECEIVER 181 | value: 'sendTo@gmail.com' # change this to the email of the receiver 182 | ``` 183 | 184 | You may now proceed to [Step 5](#5-deploy-the-microservices) if you don't want to use IBM Cloud Functions. 185 | 186 | ### 4. Use IBM Cloud Functions with Notification service 187 | > This is an optional step if you want to try IBM Cloud Functions 188 | 189 | * Create Actions 190 | The root directory of this repository contains the required code for you to create IBM Cloud Functions. You can create Actions using the `ibmcloud wsk` or `wsk` command. 191 | 192 | Create action for sending **Slack Notification** 193 | ```bash 194 | $ wsk action create sendSlackNotification sendSlack.js --param url https://hooks.slack.com/services/XXXX/YYYY/ZZZZ --web true 195 | # Replace the url with your Slack team's incoming webhook url. 196 | ``` 197 | 198 | Create action for sending **Gmail Notification** 199 | ```bash 200 | $ wsk action create sendEmailNotification sendEmail.js --web true 201 | ``` 202 | 203 | * Test Actions 204 | 205 | You can test your IBM Cloud Function Actions using `wsk action invoke [action name] [add --param to pass parameters]` 206 | 207 | Invoke Slack Notification 208 | ```bash 209 | $ wsk action invoke sendSlackNotification --param text "Hello from OpenWhisk" 210 | ``` 211 | 212 | Invoke Email Notification 213 | ```bash 214 | $ wsk action invoke sendEmailNotification --param sender [sender email] --param password [sender password]--param receiver [receiver email] --param subject [Email subject] --param text [Email Body] 215 | ``` 216 | You should receive a slack message and receive an email respectively. 217 | 218 | * Create REST API for Actions 219 | 220 | You can map REST API endpoints for your created actions using `wsk api create`. The syntax for it is `wsk api create [base-path] [api-path] [verb (GET PUT POST etc)] [action name]` 221 | 222 | Create endpoint for **Slack Notification** 223 | 224 | ```bash 225 | $ wsk api create /v1 /slack POST sendSlackNotification 226 | 227 | ok: created API /v1/slack POST for action /_/sendEmailNotification 228 | https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/slack 229 | ``` 230 | 231 | Create endpoint for **Gmail Notification** 232 | ```bash 233 | $ wsk api create /v1 /email POST sendEmailNotification 234 | ok: created API /v1/email POST for action /_/sendEmailNotification 235 | https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/email 236 | ``` 237 | 238 | You can view a list of your APIs with this command: 239 | 240 | ```bash 241 | $ wsk api list 242 | 243 | ok: APIs 244 | Action Verb API Name URL 245 | /Anthony.Amanse_dev/sendEmailNotificatio post /v1 https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/email 246 | /Anthony.Amanse_dev/testDefault post /v1 https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/slack 247 | ``` 248 | 249 | Take note of your API URLs. You are going to use them later. 250 | 251 | * Test REST API Url 252 | 253 | Test endpoint for **Slack Notification**. Replace the URL with your own API URL. 254 | 255 | ```bash 256 | $ curl -X POST -H 'Content-type: application/json' -d '{ "text": "Hello from OpenWhisk" }' https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/slack 257 | ``` 258 | 259 | ![Slack Notification](images/slackNotif.png) 260 | 261 | Test endpoint for **Gmail Notification**. Replace the URL with your own API URL. Replace the value of the parameters **sender, password, receiver, subject** with your own. 262 | 263 | ```bash 264 | $ curl -X POST -H 'Content-type: application/json' -d '{ "text": "Hello from OpenWhisk", "subject": "Email Notification", "sender": "testemail@gmail.com", "password": "passwordOfSender", "receiver": "receiversEmail" }' https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/email 265 | ``` 266 | ![Email Notification](images/emailNotif.png) 267 | 268 | * Add REST API Url to yaml files 269 | 270 | Once you have confirmed that your APIs are working, put the URLs in your `send-notification.yaml` file 271 | ```yaml 272 | env: 273 | - name: GMAIL_SENDER_USER 274 | value: 'username@gmail.com' # the sender's email 275 | - name: GMAIL_SENDER_PASSWORD 276 | value: 'password' # the sender's password 277 | - name: EMAIL_RECEIVER 278 | value: 'sendTo@gmail.com' # the receiver's email 279 | - name: OPENWHISK_API_URL_SLACK 280 | value: 'https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/slack' # your API endpoint for slack notifications 281 | - name: SLACK_MESSAGE 282 | value: 'Your balance is over $50,000.00' # your custom message 283 | - name: OPENWHISK_API_URL_EMAIL 284 | value: 'https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/.../v1/email' # your API endpoint for email notifications 285 | ``` 286 | 287 | ### 5. Deploy the Microservices 288 | 289 | * Deploy Spring Boot Microservices 290 | 291 | ```bash 292 | $ kubectl apply -f compute-interest-api.yaml 293 | service "compute-interest-api" created 294 | deployment "compute-interest-api" created 295 | ``` 296 | 297 | ```bash 298 | $ kubectl apply -f send-notification.yaml 299 | service "send-notification" created 300 | deployment "send-notification" created 301 | ``` 302 | 303 | * Deploy the Frontend service 304 | 305 | The UI is a Node.js app serving static files (HTML, CSS, JavaScript) that shows the total account balance. 306 | 307 | ```bash 308 | $ kubectl apply -f account-summary.yaml 309 | service "account-summary" created 310 | deployment "account-summary" created 311 | ``` 312 | 313 | * Deploy the Transaction Generator service 314 | The transaction generator is a Python app that generates random transactions with accumulated interest. 315 | 316 | Create the transaction generator **Python** app: 317 | ```bash 318 | $ kubectl apply -f transaction-generator.yaml 319 | service "transaction-generator" created 320 | deployment "transaction-generator" created 321 | ``` 322 | 323 | ### 6. Access Your Application 324 | You can access your app publicly through your Cluster IP and the NodePort. The NodePort should be **30080**. 325 | 326 | * To find your IP: 327 | ```bash 328 | $ ibmcloud cs workers 329 | ID Public IP Private IP Machine Type State Status 330 | kube-dal10-paac005a5fa6c44786b5dfb3ed8728548f-w1 169.47.241.213 10.177.155.13 free normal Ready 331 | ``` 332 | 333 | * To find the NodePort of the account-summary service: 334 | ```bash 335 | $ kubectl get svc 336 | NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE 337 | ... 338 | account-summary 10.10.10.74 80:30080/TCP 2d 339 | ... 340 | ``` 341 | * On your browser, go to `http://:30080` 342 | ![Account-balance](images/balance.png) 343 | 344 | ## Troubleshooting 345 | * To start over, delete everything: `kubectl delete svc,deploy -l app=office-space` 346 | 347 | 348 | ## References 349 | * [John Zaccone](https://github.com/jzaccone) - The original author of the [office space app deployed via Docker](https://github.com/jzaccone/office-space-dockercon2017). 350 | * The Office Space app is based on the 1999 film that used that concept. 351 | 352 | ## License 353 | This code pattern is licensed under the Apache Software License, Version 2. Separate third party code objects invoked within this code pattern are licensed by their respective providers pursuant to their own separate licenses. Contributions are subject to the [Developer Certificate of Origin, Version 1.1 (DCO)](https://developercertificate.org/) and the [Apache Software License, Version 2](http://www.apache.org/licenses/LICENSE-2.0.txt). 354 | 355 | [Apache Software License (ASL) FAQ](http://www.apache.org/foundation/license-faq.html#WhatDoesItMEAN) 356 | -------------------------------------------------------------------------------- /.bluemix/toolchain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 36 | 37 | background 38 | 39 | 40 | 41 | Layer 1 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 83 | 84 | 85 | 86 | 88 | 89 | 90 | 91 | 92 | 93 | 96 | 97 | 98 | 99 | 101 | 102 | 103 | 104 | 105 | 107 | 108 | 109 | 110 | 112 | 116 | 118 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 135 | 138 | 139 | 140 | 142 | 143 | 144 | 145 | 148 | 151 | 152 | 168 | 169 | 170 | 173 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 203 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 225 | 226 | 228 | 231 | 232 | 233 | 235 | 236 | 237 | 238 | 241 | 244 | 245 | 261 | 262 | 263 | 266 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 279 | 280 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | ISSUE TRACKER 317 | GitHub 318 | THINK 319 | CODE 320 | DELIVER 321 | RUN 322 | REPOSITORY 323 | GitHub 324 | PIPELINE 325 | BLUEMIX 326 | WEB IDE 327 | 328 | 329 | --------------------------------------------------------------------------------