├── featureflags
├── src
│ ├── test
│ │ ├── resources
│ │ │ ├── application.properties
│ │ │ └── data-postgresql.sql
│ │ └── java
│ │ │ └── com
│ │ │ └── github
│ │ │ └── quiram
│ │ │ └── shopping
│ │ │ └── featureflags
│ │ │ ├── model
│ │ │ └── FlagTest.java
│ │ │ └── repositories
│ │ │ └── FlagRepositoryIT.java
│ └── main
│ │ ├── resources
│ │ ├── application.properties
│ │ ├── data-postgresql.sql
│ │ ├── application-prod.properties
│ │ └── application-test.properties
│ │ └── java
│ │ └── com
│ │ └── github
│ │ └── quiram
│ │ └── shopping
│ │ └── featureflags
│ │ ├── exceptions
│ │ ├── FlagCreatedWithIdException.java
│ │ ├── FlagNameAlreadyExistsException.java
│ │ ├── FlagWithoutIdException.java
│ │ └── FlagNotFoundException.java
│ │ ├── repositories
│ │ └── FlagRepository.java
│ │ ├── FeatureFlagsApplication.java
│ │ ├── model
│ │ └── Flag.java
│ │ ├── services
│ │ └── FlagService.java
│ │ └── resources
│ │ └── FlagResource.java
└── Dockerfile
├── stockmanager
├── src
│ ├── main
│ │ ├── resources
│ │ │ └── application.properties
│ │ └── java
│ │ │ └── uk
│ │ │ └── co
│ │ │ └── danielbryant
│ │ │ └── shopping
│ │ │ └── stockmanager
│ │ │ ├── repositories
│ │ │ └── StockRepository.java
│ │ │ ├── StockManagerApplication.java
│ │ │ ├── exceptions
│ │ │ └── StockNotFoundException.java
│ │ │ ├── model
│ │ │ ├── v1
│ │ │ │ └── Stock.java
│ │ │ └── v2
│ │ │ │ ├── AmountAvailable.java
│ │ │ │ └── Stock.java
│ │ │ ├── services
│ │ │ └── StockService.java
│ │ │ ├── config
│ │ │ └── DataGenerator.java
│ │ │ └── resources
│ │ │ └── StockResource.java
│ └── test
│ │ ├── resources
│ │ └── contracts
│ │ │ └── stockmanager
│ │ │ ├── getOneStock.groovy
│ │ │ ├── getOneStock-v2.groovy
│ │ │ ├── getAllStock.groovy
│ │ │ └── getAllStock-v2.groovy
│ │ └── java
│ │ └── uk
│ │ └── co
│ │ └── danielbryant
│ │ └── shopping
│ │ └── stockmanager
│ │ ├── model
│ │ ├── v1
│ │ │ └── StockTest.java
│ │ └── v2
│ │ │ ├── AmountAvailableTest.java
│ │ │ └── StockTest.java
│ │ ├── StockManagerCDCBase.java
│ │ └── StockManagerApplicationCT.java
└── Dockerfile
├── fake-adaptive-pricing
└── Dockerfile
├── external-adaptive-pricing
├── src
│ ├── main
│ │ ├── resources
│ │ │ └── application.properties
│ │ └── java
│ │ │ └── com
│ │ │ └── github
│ │ │ └── quiram
│ │ │ └── shopping
│ │ │ └── adaptive
│ │ │ ├── model
│ │ │ └── Price.java
│ │ │ ├── AdaptivePricingApplication.java
│ │ │ └── resources
│ │ │ └── PriceResource.java
│ └── test
│ │ └── java
│ │ └── com
│ │ └── github
│ │ └── quiram
│ │ └── shopping
│ │ └── adaptive
│ │ └── AdaptivePricingApplicationCT.java
├── Dockerfile
└── pom.xml
├── jenkins-base
├── push.sh
├── mvn
│ ├── jenkins.mvn.GlobalMavenConfig.xml
│ └── hudson.tasks.Maven.xml
├── build.sh
├── docker-compose.yml
├── docker
│ └── org.jenkinsci.plugins.docker.commons.tools.DockerTool.xml
├── jobs
│ ├── deploy
│ │ └── config.xml
│ ├── test-featureflags-db
│ │ └── config.xml
│ ├── fake-adaptive-pricing
│ │ └── config.xml
│ ├── featureflags-db
│ │ └── config.xml
│ ├── featureflags
│ │ └── config.xml
│ ├── stockmanager
│ │ └── config.xml
│ ├── productcatalogue
│ │ └── config.xml
│ ├── external-adaptive-pricing
│ │ └── config.xml
│ └── acceptance-tests
│ │ └── config.xml
├── plugins
│ └── plugins-list
├── Dockerfile
└── README.md
├── acceptance-tests
└── src
│ └── test
│ ├── resources
│ ├── docker-compose-ci-network.yml
│ ├── logback-test.xml
│ └── docker-compose.yml
│ └── java
│ └── com
│ └── github
│ └── quiram
│ └── shopping
│ └── acceptancetests
│ ├── steps
│ ├── StepsBase.java
│ ├── FeatureFlagsSteps.java
│ └── ShopfrontSteps.java
│ ├── entities
│ └── Flag.java
│ ├── pages
│ └── ShopfrontHomePage.java
│ └── ShoppingAT.java
├── featureflags-db
├── Dockerfile
└── README.md
├── shopfront
├── Dockerfile
├── src
│ ├── test
│ │ ├── resources
│ │ │ └── application.properties
│ │ └── java
│ │ │ └── uk
│ │ │ └── co
│ │ │ └── danielbryant
│ │ │ └── shopping
│ │ │ └── shopfront
│ │ │ ├── StockManagerCDC.java
│ │ │ └── services
│ │ │ ├── dto
│ │ │ └── SmartThrottleFlagTest.java
│ │ │ └── FeatureFlagsServiceTest.java
│ └── main
│ │ ├── resources
│ │ └── application.properties
│ │ └── java
│ │ └── uk
│ │ └── co
│ │ └── danielbryant
│ │ └── shopping
│ │ └── shopfront
│ │ ├── model
│ │ ├── Constants.java
│ │ ├── CircuitBreaker.java
│ │ └── Product.java
│ │ ├── services
│ │ ├── dto
│ │ │ ├── PriceDTO.java
│ │ │ ├── FlagDTO.java
│ │ │ ├── ProductDTO.java
│ │ │ ├── StockDTO.java
│ │ │ └── SmartThrottleFlag.java
│ │ ├── FeatureFlagsService.java
│ │ └── ProductService.java
│ │ ├── controllers
│ │ └── HomeController.java
│ │ ├── ShopfrontApplication.java
│ │ ├── resources
│ │ ├── ProductResource.java
│ │ └── InternalResource.java
│ │ └── repo
│ │ ├── CircuitBreakerRepo.java
│ │ ├── ProductRepo.java
│ │ ├── AdaptivePricingRepo.java
│ │ ├── FeatureFlagsRepo.java
│ │ └── StockRepo.java
└── pom.xml
├── test-featureflags-db
└── Dockerfile
├── productcatalogue
├── product-catalogue.yml
├── src
│ ├── test
│ │ ├── resources
│ │ │ └── product-catalogue.yml
│ │ └── java
│ │ │ └── uk
│ │ │ └── co
│ │ │ └── danielbryant
│ │ │ └── shopping
│ │ │ └── productcatalogue
│ │ │ ├── model
│ │ │ ├── v2
│ │ │ │ ├── BulkPriceTest.java
│ │ │ │ ├── UnitPriceTest.java
│ │ │ │ ├── PriceTest.java
│ │ │ │ └── ProductTest.java
│ │ │ └── v1
│ │ │ │ └── ProductTest.java
│ │ │ └── ProductServiceApplicationCT.java
│ └── main
│ │ └── java
│ │ └── uk
│ │ └── co
│ │ └── danielbryant
│ │ └── shopping
│ │ └── productcatalogue
│ │ ├── healthchecks
│ │ └── BasicHealthCheck.java
│ │ ├── configuration
│ │ └── ProductServiceConfiguration.java
│ │ ├── model
│ │ ├── v2
│ │ │ ├── UnitPrice.java
│ │ │ ├── BulkPrice.java
│ │ │ ├── Product.java
│ │ │ └── Price.java
│ │ └── v1
│ │ │ └── Product.java
│ │ ├── services
│ │ └── ProductService.java
│ │ ├── resources
│ │ ├── v2
│ │ │ └── ProductResource.java
│ │ └── v1
│ │ │ └── ProductResource.java
│ │ └── ProductServiceApplication.java
├── README.md
└── Dockerfile
├── jenkins-kubernetes
├── expose-services.sh
├── hard-rebuild.sh
├── Dockerfile
├── kubernetes
│ └── kubeconfig_template
├── docker-compose.yml
├── service-definitions
│ ├── featureflags-db-service.yaml
│ ├── shopfront-service.yaml
│ ├── featureflags-service.yaml
│ ├── stockmanager-service.yaml
│ ├── productcatalogue-service.yaml
│ └── external-adaptive-pricing-service.yaml
├── credentials
│ └── credentials.xml
├── build.sh
├── README.md
└── jobs
│ └── deploy
│ └── config.xml
├── jenkins-aws-ecs
├── task-definitions
│ ├── featureflags-db-task.json
│ ├── shopfront-task.json
│ ├── featureflags-task.json
│ ├── stockmanager-task.json
│ ├── productcatalogue-task.json
│ └── external-adaptive-pricing-task.json
├── Dockerfile
├── hard-rebuild.sh
├── docker-compose.yml
├── build.sh
├── jobs
│ └── deploy
│ │ └── config.xml
├── deploy-to-aws-ecs.sh
├── setup
│ ├── delete-env.sh
│ ├── create-env.sh
│ └── constants.sh
└── README.md
├── .gitignore
├── non-spring-boot-master-pom.xml
├── README.md
└── spring-boot-master-pom.xml
/featureflags/src/test/resources/application.properties:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/stockmanager/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | server.port=8030
--------------------------------------------------------------------------------
/fake-adaptive-pricing/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM quiram/external-adaptive-pricing
2 |
--------------------------------------------------------------------------------
/external-adaptive-pricing/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | server.port=8050
--------------------------------------------------------------------------------
/featureflags/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.profiles.active=prod
--------------------------------------------------------------------------------
/jenkins-base/push.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | docker push quiram/jenkins-base
4 |
--------------------------------------------------------------------------------
/acceptance-tests/src/test/resources/docker-compose-ci-network.yml:
--------------------------------------------------------------------------------
1 |
2 | networks:
3 | default:
4 | external:
5 | name: %network_placeholder%
--------------------------------------------------------------------------------
/featureflags-db/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM centos/postgresql-96-centos7
2 | ENV POSTGRESQL_USER featureflags
3 | ENV POSTGRESQL_PASSWORD secret-password
4 | ENV POSTGRESQL_DATABASE featureflags
5 |
--------------------------------------------------------------------------------
/shopfront/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM openjdk:8-jre
2 | ADD target/shopfront-0.0.1-SNAPSHOT.jar app.jar
3 | EXPOSE 8010
4 | ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
5 |
--------------------------------------------------------------------------------
/featureflags/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM openjdk:8-jre
2 | ADD target/featureflags-0.0.1-SNAPSHOT.jar app.jar
3 | EXPOSE 8040
4 | ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
5 |
--------------------------------------------------------------------------------
/stockmanager/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM openjdk:8-jre
2 | ADD target/stockmanager-0.0.1-SNAPSHOT.jar app.jar
3 | EXPOSE 8030
4 | ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
5 |
--------------------------------------------------------------------------------
/test-featureflags-db/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM centos/postgresql-96-centos7
2 | ENV POSTGRESQL_USER featureflagstest
3 | ENV POSTGRESQL_PASSWORD secret-password-test
4 | ENV POSTGRESQL_DATABASE featureflags
5 |
--------------------------------------------------------------------------------
/productcatalogue/product-catalogue.yml:
--------------------------------------------------------------------------------
1 | version: 1.0-SNAPSHOT
2 |
3 |
4 | server:
5 | applicationConnectors:
6 | - type: http
7 | port: 8020
8 | adminConnectors:
9 | - type: http
10 | port: 8025
--------------------------------------------------------------------------------
/external-adaptive-pricing/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM openjdk:8-jre
2 | ADD target/external-adaptive-pricing-0.0.1-SNAPSHOT.jar app.jar
3 | EXPOSE 8050
4 | ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
5 |
--------------------------------------------------------------------------------
/featureflags/src/main/resources/data-postgresql.sql:
--------------------------------------------------------------------------------
1 | -- Ensure the Adaptive Pricing Flag is added when the schema is created
2 | INSERT INTO flag(flag_id, name, portion_in, sticky) VALUES (1, 'adaptive-pricing', 50, TRUE)
3 |
--------------------------------------------------------------------------------
/productcatalogue/src/test/resources/product-catalogue.yml:
--------------------------------------------------------------------------------
1 | version: 1.0-SNAPSHOT
2 |
3 |
4 | server:
5 | applicationConnectors:
6 | - type: http
7 | port: 8020
8 | adminConnectors:
9 | - type: http
10 | port: 8025
--------------------------------------------------------------------------------
/productcatalogue/README.md:
--------------------------------------------------------------------------------
1 | product-catalogue
2 | =================
3 |
4 | java -jar target/product-1.0-SNAPSHOT.jar server product-catalogue.yml
5 |
6 | docker build -t quiram/product .
7 | docker run -p 9010:9010 -p 9011:9011 -d quiram/product
--------------------------------------------------------------------------------
/productcatalogue/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM openjdk:8-jre
2 | ADD target/productcatalogue-0.0.1-SNAPSHOT.jar app.jar
3 | ADD product-catalogue.yml app-config.yml
4 | EXPOSE 8020
5 | ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","app.jar", "server", "app-config.yml"]
6 |
--------------------------------------------------------------------------------
/jenkins-base/mvn/jenkins.mvn.GlobalMavenConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/featureflags/src/main/resources/application-prod.properties:
--------------------------------------------------------------------------------
1 | server.port=8040
2 | spring.datasource.url= jdbc:postgresql://featureflags-db:5432/featureflags
3 | spring.datasource.platform=postgresql
4 | spring.datasource.username=featureflags
5 | spring.datasource.password=secret-password
6 | spring.jpa.hibernate.ddl-auto=create
7 |
--------------------------------------------------------------------------------
/featureflags/src/main/java/com/github/quiram/shopping/featureflags/exceptions/FlagCreatedWithIdException.java:
--------------------------------------------------------------------------------
1 | package com.github.quiram.shopping.featureflags.exceptions;
2 |
3 | public class FlagCreatedWithIdException extends Exception {
4 | public FlagCreatedWithIdException(String message) {
5 | super(message);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/featureflags/src/main/resources/application-test.properties:
--------------------------------------------------------------------------------
1 | server.port=8040
2 | spring.datasource.url= jdbc:postgresql://test-featureflags-db:5432/featureflags
3 | spring.datasource.platform=postgresql
4 | spring.datasource.username=featureflagstest
5 | spring.datasource.password=secret-password-test
6 | spring.jpa.hibernate.ddl-auto=create
7 |
--------------------------------------------------------------------------------
/featureflags/src/main/java/com/github/quiram/shopping/featureflags/exceptions/FlagNameAlreadyExistsException.java:
--------------------------------------------------------------------------------
1 | package com.github.quiram.shopping.featureflags.exceptions;
2 |
3 | public class FlagNameAlreadyExistsException extends Exception {
4 | public FlagNameAlreadyExistsException(String message) {
5 | super(message);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/shopfront/src/test/resources/application.properties:
--------------------------------------------------------------------------------
1 | productCatalogueUri=http://localhost:8020
2 | stockManagerUri=http://localhost:8030
3 | featureFlagsUri=http://localhost:8040
4 | adaptivePricingUri=http://localhost:8050
5 | # Disable discovery (this just creates a lot of noise in the logs)
6 | spring.cloud.service-registry.auto-registration.enabled=false
--------------------------------------------------------------------------------
/stockmanager/src/main/java/uk/co/danielbryant/shopping/stockmanager/repositories/StockRepository.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.stockmanager.repositories;
2 |
3 | import org.springframework.data.repository.CrudRepository;
4 | import uk.co.danielbryant.shopping.stockmanager.model.v2.Stock;
5 |
6 | public interface StockRepository extends CrudRepository {
7 | }
8 |
--------------------------------------------------------------------------------
/featureflags/src/test/resources/data-postgresql.sql:
--------------------------------------------------------------------------------
1 | -- Some data for tests
2 | DELETE FROM flag
3 | INSERT INTO flag(flag_id, name, portion_in, sticky) VALUES (1, 'half-way-feature', 50, TRUE)
4 | INSERT INTO flag(flag_id, name, portion_in, sticky) VALUES (2, 'disabled-feature', 0, FALSE)
5 | INSERT INTO flag(flag_id, name, portion_in, sticky) VALUES (3, 'fully-enabled-feature', 100, TRUE)
6 |
7 |
--------------------------------------------------------------------------------
/shopfront/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | server.port=8010
2 | productCatalogueUri=http://productcatalogue:8020
3 | stockManagerUri=http://stockmanager:8030
4 | featureFlagsUri=http://featureflags:8040
5 | adaptivePricingUri=http://external-adaptive-pricing:8050
6 | # Disable discovery (this just creates a lot of noise in the logs)
7 | spring.cloud.service-registry.auto-registration.enabled=false
--------------------------------------------------------------------------------
/jenkins-kubernetes/expose-services.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | services="shopfront stockmanager productcatalogue featureflags featureflags-db external-adaptive-pricing"
4 |
5 | ip=$(minikube ip)
6 | for service in ${services}; do
7 | port=$(kubectl get services/${service} -o go-template='{{(index .spec.ports 0).nodePort}}')
8 | echo "${service} --> http://${ip}:${port}/"
9 | done
10 |
--------------------------------------------------------------------------------
/featureflags/src/main/java/com/github/quiram/shopping/featureflags/repositories/FlagRepository.java:
--------------------------------------------------------------------------------
1 | package com.github.quiram.shopping.featureflags.repositories;
2 |
3 | import com.github.quiram.shopping.featureflags.model.Flag;
4 | import org.springframework.data.repository.CrudRepository;
5 |
6 | public interface FlagRepository extends CrudRepository {
7 | Flag findByName(String name);
8 | }
9 |
--------------------------------------------------------------------------------
/featureflags/src/main/java/com/github/quiram/shopping/featureflags/exceptions/FlagWithoutIdException.java:
--------------------------------------------------------------------------------
1 | package com.github.quiram.shopping.featureflags.exceptions;
2 |
3 | import static java.lang.String.format;
4 |
5 | public class FlagWithoutIdException extends Exception {
6 | public FlagWithoutIdException(String flagName) {
7 | super(format("Flag with name '%s' does not have a flagId", flagName));
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/jenkins-aws-ecs/task-definitions/featureflags-db-task.json:
--------------------------------------------------------------------------------
1 | {
2 | "family": "featureflags-db",
3 | "containerDefinitions": [
4 | {
5 | "image": "quiram/featureflags-db",
6 | "name": "featureflags-db",
7 | "cpu": 10,
8 | "memory": 300,
9 | "essential": true,
10 | "portMappings": [
11 | {
12 | "containerPort": 5432,
13 | "hostPort": 5432
14 | }
15 | ]
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/jenkins-aws-ecs/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM quiram/jenkins-base
2 |
3 | USER root
4 |
5 | # Install PIP
6 | RUN curl -O https://bootstrap.pypa.io/get-pip.py
7 | RUN python get-pip.py
8 |
9 | # Install latest AWS Client
10 | RUN pip install awscli --upgrade
11 |
12 | # Install jq (used by deployment script)
13 | RUN apt-get update && sudo apt-get install -y jq
14 |
15 | # Override deploy job to use AWS ECS
16 | COPY --chown=jenkins:jenkins jobs/ /var/jenkins_home/jobs/
17 |
18 | USER jenkins
19 |
--------------------------------------------------------------------------------
/featureflags/src/main/java/com/github/quiram/shopping/featureflags/FeatureFlagsApplication.java:
--------------------------------------------------------------------------------
1 | package com.github.quiram.shopping.featureflags;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class FeatureFlagsApplication {
8 | public static void main(String[] args) {
9 | SpringApplication.run(FeatureFlagsApplication.class, args);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/stockmanager/src/main/java/uk/co/danielbryant/shopping/stockmanager/StockManagerApplication.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.stockmanager;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class StockManagerApplication {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(StockManagerApplication.class, args);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/external-adaptive-pricing/src/main/java/com/github/quiram/shopping/adaptive/model/Price.java:
--------------------------------------------------------------------------------
1 | package com.github.quiram.shopping.adaptive.model;
2 |
3 | import java.math.BigDecimal;
4 |
5 | public class Price {
6 | private BigDecimal price;
7 |
8 | public Price() {
9 | // Needed by Spring
10 | }
11 |
12 | public Price(BigDecimal price) {
13 | this.price = price;
14 | }
15 |
16 | public BigDecimal getPrice() {
17 | return price;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/external-adaptive-pricing/src/main/java/com/github/quiram/shopping/adaptive/AdaptivePricingApplication.java:
--------------------------------------------------------------------------------
1 | package com.github.quiram.shopping.adaptive;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class AdaptivePricingApplication {
8 | public static void main(String[] args) {
9 | SpringApplication.run(AdaptivePricingApplication.class, args);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Maven template
2 | target/
3 | build/
4 | release.properties
5 | buildNumber.properties
6 |
7 | ### Java template
8 | *.class
9 | .idea
10 | **.iml
11 |
12 | # Package Files #
13 | *.jar
14 | *.war
15 | *.ear
16 |
17 | # Vagrant
18 | .vagrant/
19 | # Mac
20 | .DS_Store
21 |
22 | #retry
23 | *.retry
24 |
25 | #logs
26 | *.log
27 |
28 | # Generated files
29 | jenkins-kubernetes/kubernetes/secrets/
30 | jenkins-kubernetes/kubernetes/kubeconfig
31 |
32 | # Private keys for AWS access
33 | *.pem
--------------------------------------------------------------------------------
/jenkins-aws-ecs/hard-rebuild.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
4 |
5 | pushd ${SCRIPT_DIR}
6 |
7 | graceful_exit() {
8 | eval "popd; exit $1" # This ensures that popd is executed without affecting the result code of the script
9 | }
10 |
11 |
12 | docker system prune && docker volume rm $(docker volume ls -qf dangling=true)
13 |
14 | pushd ../jenkins-base
15 | ./build.sh && ./push.sh
16 | popd
17 | ./build.sh
18 |
19 | graceful_exit $?
20 |
--------------------------------------------------------------------------------
/jenkins-kubernetes/hard-rebuild.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
4 |
5 | pushd ${SCRIPT_DIR}
6 |
7 | graceful_exit() {
8 | eval "popd; exit $1" # This ensures that popd is executed without affecting the result code of the script
9 | }
10 |
11 |
12 | docker system prune && docker volume rm $(docker volume ls -qf dangling=true)
13 |
14 | pushd ../jenkins-base
15 | ./build.sh && ./push.sh
16 | popd
17 | ./build.sh
18 |
19 | graceful_exit $?
20 |
--------------------------------------------------------------------------------
/featureflags/src/main/java/com/github/quiram/shopping/featureflags/exceptions/FlagNotFoundException.java:
--------------------------------------------------------------------------------
1 | package com.github.quiram.shopping.featureflags.exceptions;
2 |
3 | public class FlagNotFoundException extends Exception {
4 |
5 | public FlagNotFoundException() {
6 | }
7 |
8 | public FlagNotFoundException(String message) {
9 | super(message);
10 | }
11 |
12 | public FlagNotFoundException(String message, Throwable cause) {
13 | super(message, cause);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/shopfront/src/main/java/uk/co/danielbryant/shopping/shopfront/model/Constants.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.shopfront.model;
2 |
3 | import java.util.List;
4 |
5 | import static java.util.Collections.singletonList;
6 |
7 | public class Constants {
8 | public static final long ADAPTIVE_PRICING_FLAG_ID = 1L;
9 | public static final String ADAPTIVE_PRICING_COMMAND_KEY = "adaptivePricing";
10 | public static final List COMMAND_KEYS = singletonList(ADAPTIVE_PRICING_COMMAND_KEY);
11 | }
12 |
--------------------------------------------------------------------------------
/stockmanager/src/main/java/uk/co/danielbryant/shopping/stockmanager/exceptions/StockNotFoundException.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.stockmanager.exceptions;
2 |
3 | public class StockNotFoundException extends Exception {
4 |
5 | public StockNotFoundException() {
6 | }
7 |
8 | public StockNotFoundException(String message) {
9 | super(message);
10 | }
11 |
12 | public StockNotFoundException(String message, Throwable cause) {
13 | super(message, cause);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/jenkins-base/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
4 |
5 | pushd ${SCRIPT_DIR}
6 |
7 | graceful_exit() {
8 | eval "popd; exit $1" # This ensures that popd is executed without affecting the result code of the script
9 | }
10 |
11 | # build image
12 | docker build -t quiram/jenkins-base .
13 | exit_code=$?
14 |
15 | # show message
16 | echo
17 | echo "Remember to rebuild all other jenkins images based on this template container."
18 | echo
19 |
20 | graceful_exit ${exit_code}
21 |
--------------------------------------------------------------------------------
/stockmanager/src/test/resources/contracts/stockmanager/getOneStock.groovy:
--------------------------------------------------------------------------------
1 | package contracts.stockmanager
2 |
3 | org.springframework.cloud.contract.spec.Contract.make {
4 | request {
5 | method 'GET'
6 | url '/stocks/123'
7 | }
8 | response {
9 | status 200
10 | body(
11 | productId: "123",
12 | sku: "sku-123",
13 | amountAvailable: 10
14 | )
15 | headers {
16 | contentType('application/json')
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/jenkins-kubernetes/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM quiram/jenkins-base
2 |
3 | USER root
4 |
5 | # Load plugin for kubernetes deployment
6 | RUN /usr/local/bin/install-plugins.sh kubernetes-cd
7 |
8 | # Override deploy job to use Kubernetes
9 | COPY --chown=jenkins:jenkins jobs/ /var/jenkins_home/jobs/
10 |
11 | # Configure kubernetes client
12 | RUN mkdir /var/jenkins_home/kubernetes
13 | COPY --chown=jenkins:jenkins kubernetes/ /var/jenkins_home/kubernetes/
14 | COPY --chown=jenkins:jenkins credentials/credentials.xml /var/jenkins_home/
15 |
16 | USER jenkins
17 |
18 |
--------------------------------------------------------------------------------
/featureflags-db/README.md:
--------------------------------------------------------------------------------
1 | Feature Flags Database
2 | ======================
3 |
4 | This is a dockerised MySQL database in lieu of a real production database for Feature Flags Service to connect in production.
5 |
6 | This is done this way to simplify the demonstration of the different deployment environments: while different technologies
7 | will handle databases differently, docker containers are handled the same way. This way we can connect the Feature Flags
8 | to a "real" MySQL database without having to deal with the different ways in which databases are managed in Kubernetes,
9 | AWS, etc.
--------------------------------------------------------------------------------
/jenkins-base/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | jenkins:
4 | image: quiram/jenkins-base
5 | ports:
6 | - "8080:8080"
7 | - "50000:50000"
8 | volumes:
9 | - "jenkins_base_home:/var/jenkins_home"
10 | - "/var/run/docker.sock:/var/run/docker.sock"
11 | networks:
12 | - ci_network
13 | environment:
14 | - docker_network_name=jenkinsbase_ci_network
15 |
16 | volumes:
17 | jenkins_base_home:
18 |
19 | networks:
20 | ci_network:
21 | driver: bridge
22 | ipam:
23 | config:
24 | - subnet: 172.19.0.0/16
--------------------------------------------------------------------------------
/jenkins-kubernetes/kubernetes/kubeconfig_template:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | clusters:
3 | - cluster:
4 | certificate-authority: /var/jenkins_home/kubernetes/secrets/ca.crt
5 | server: https://%MINIKUBE_IP%:8443
6 | name: minikube
7 | contexts:
8 | - context:
9 | cluster: minikube
10 | user: minikube
11 | name: minikube
12 | current-context: minikube
13 | kind: Config
14 | preferences: {}
15 | users:
16 | - name: minikube
17 | user:
18 | client-certificate: /var/jenkins_home/kubernetes/secrets/client.crt
19 | client-key: /var/jenkins_home/kubernetes/secrets/client.key
--------------------------------------------------------------------------------
/productcatalogue/src/main/java/uk/co/danielbryant/shopping/productcatalogue/healthchecks/BasicHealthCheck.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.productcatalogue.healthchecks;
2 |
3 | import com.codahale.metrics.health.HealthCheck;
4 |
5 | public class BasicHealthCheck extends HealthCheck {
6 |
7 | private final String version;
8 |
9 | public BasicHealthCheck(String version) {
10 | this.version = version;
11 | }
12 |
13 | @Override
14 | protected Result check() throws Exception {
15 | return Result.healthy("Ok with version: " + version);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/jenkins-aws-ecs/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | jenkins:
4 | image: quiram/jenkins-aws-ecs
5 | ports:
6 | - "8080:8080"
7 | - "50000:50000"
8 | volumes:
9 | - "jenkins_aws_ecs_home:/var/jenkins_home"
10 | - "/var/run/docker.sock:/var/run/docker.sock"
11 | networks:
12 | - ci_network
13 | environment:
14 | - docker_network_name=jenkins-aws-ecs_ci_network
15 |
16 | volumes:
17 | jenkins_aws_ecs_home:
18 |
19 | networks:
20 | ci_network:
21 | driver: bridge
22 | ipam:
23 | config:
24 | - subnet: 172.19.0.0/16
--------------------------------------------------------------------------------
/shopfront/src/main/java/uk/co/danielbryant/shopping/shopfront/services/dto/PriceDTO.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.shopfront.services.dto;
2 |
3 | import java.math.BigDecimal;
4 |
5 | public class PriceDTO {
6 | private BigDecimal price;
7 |
8 | public PriceDTO() {
9 | // Needed by Spring
10 | }
11 |
12 | public PriceDTO(BigDecimal price) {
13 | this.price = price;
14 | }
15 |
16 | public BigDecimal getPrice() {
17 | return price;
18 | }
19 |
20 | public void setPrice(BigDecimal price) {
21 | this.price = price;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/jenkins-kubernetes/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | jenkins:
4 | image: quiram/jenkins-kubernetes
5 | ports:
6 | - "8080:8080"
7 | - "50000:50000"
8 | volumes:
9 | - "jenkins_kubernetes_home:/var/jenkins_home"
10 | - "/var/run/docker.sock:/var/run/docker.sock"
11 | networks:
12 | - ci_network
13 | environment:
14 | - docker_network_name=jenkins-kubernetes_ci_network
15 |
16 | volumes:
17 | jenkins_kubernetes_home:
18 |
19 | networks:
20 | ci_network:
21 | driver: bridge
22 | ipam:
23 | config:
24 | - subnet: 172.19.0.0/16
--------------------------------------------------------------------------------
/jenkins-base/mvn/hudson.tasks.Maven.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Default
6 |
7 |
8 |
9 |
10 | 3.5.3
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/jenkins-aws-ecs/task-definitions/shopfront-task.json:
--------------------------------------------------------------------------------
1 | {
2 | "family": "shopfront",
3 | "containerDefinitions": [
4 | {
5 | "image": "quiram/shopfront",
6 | "name": "shopfront",
7 | "cpu": 10,
8 | "memory": 300,
9 | "essential": true,
10 | "portMappings": [
11 | {
12 | "containerPort": 8010,
13 | "hostPort": 8010
14 | }
15 | ],
16 | "healthCheck": {
17 | "command": [ "CMD-SHELL", "curl -f http://localhost:8010/health || exit 1" ],
18 | "interval": 10,
19 | "timeout": 2,
20 | "retries": 3,
21 | "startPeriod": 30
22 | }
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/jenkins-aws-ecs/task-definitions/featureflags-task.json:
--------------------------------------------------------------------------------
1 | {
2 | "family": "featureflags",
3 | "containerDefinitions": [
4 | {
5 | "image": "quiram/featureflags",
6 | "name": "featureflags",
7 | "cpu": 10,
8 | "memory": 300,
9 | "essential": true,
10 | "portMappings": [
11 | {
12 | "containerPort": 8040,
13 | "hostPort": 8040
14 | }
15 | ],
16 | "healthCheck": {
17 | "command": [ "CMD-SHELL", "curl -f http://localhost:8040/health || exit 1" ],
18 | "interval": 10,
19 | "timeout": 2,
20 | "retries": 3,
21 | "startPeriod": 30
22 | }
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/jenkins-aws-ecs/task-definitions/stockmanager-task.json:
--------------------------------------------------------------------------------
1 | {
2 | "family": "stockmanager",
3 | "containerDefinitions": [
4 | {
5 | "image": "quiram/stockmanager",
6 | "name": "stockmanager",
7 | "cpu": 10,
8 | "memory": 300,
9 | "essential": true,
10 | "portMappings": [
11 | {
12 | "containerPort": 8030,
13 | "hostPort": 8030
14 | }
15 | ],
16 | "healthCheck": {
17 | "command": [ "CMD-SHELL", "curl -f http://localhost:8030/health || exit 1" ],
18 | "interval": 10,
19 | "timeout": 2,
20 | "retries": 3,
21 | "startPeriod": 30
22 | }
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/jenkins-aws-ecs/task-definitions/productcatalogue-task.json:
--------------------------------------------------------------------------------
1 | {
2 | "family": "productcatalogue",
3 | "containerDefinitions": [
4 | {
5 | "image": "quiram/productcatalogue",
6 | "name": "productcatalogue",
7 | "cpu": 10,
8 | "memory": 300,
9 | "essential": true,
10 | "portMappings": [
11 | {
12 | "containerPort": 8020,
13 | "hostPort": 8020
14 | }
15 | ],
16 | "healthCheck": {
17 | "command": [ "CMD-SHELL", "curl -f http://localhost:8025/healthcheck || exit 1" ],
18 | "interval": 10,
19 | "timeout": 2,
20 | "retries": 3,
21 | "startPeriod": 30
22 | }
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/productcatalogue/src/main/java/uk/co/danielbryant/shopping/productcatalogue/configuration/ProductServiceConfiguration.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.productcatalogue.configuration;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 | import io.dropwizard.Configuration;
5 | import org.hibernate.validator.constraints.NotEmpty;
6 |
7 | public class ProductServiceConfiguration extends Configuration {
8 |
9 | @NotEmpty
10 | private String version;
11 |
12 | @JsonProperty
13 | public String getVersion() {
14 | return version;
15 | }
16 |
17 | @JsonProperty
18 | public void setVersion(String version) {
19 | this.version = version;
20 | }
21 | }
--------------------------------------------------------------------------------
/jenkins-aws-ecs/task-definitions/external-adaptive-pricing-task.json:
--------------------------------------------------------------------------------
1 | {
2 | "family": "external-adaptive-pricing",
3 | "containerDefinitions": [
4 | {
5 | "image": "quiram/external-adaptive-pricing",
6 | "name": "external-adaptive-pricing",
7 | "cpu": 10,
8 | "memory": 300,
9 | "essential": true,
10 | "portMappings": [
11 | {
12 | "containerPort": 8050,
13 | "hostPort": 8050
14 | }
15 | ],
16 | "healthCheck": {
17 | "command": [ "CMD-SHELL", "curl -f http://localhost:8050/health || exit 1" ],
18 | "interval": 10,
19 | "timeout": 2,
20 | "retries": 3,
21 | "startPeriod": 30
22 | }
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/jenkins-aws-ecs/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
4 |
5 | pushd ${SCRIPT_DIR}
6 |
7 | graceful_exit() {
8 | eval "popd; exit $1" # This ensures that popd is executed without affecting the result code of the script
9 | }
10 |
11 | no_file_error_message() {
12 | file_name=$1
13 |
14 | echo "${file_name} file not found"
15 | echo "You need to run minikube first to generate secret files."
16 |
17 | graceful_exit -1
18 | }
19 |
20 | ensure_file() {
21 | file_name=$1
22 |
23 | [ -f ${file_name} ] || no_file_error_message ${file_name}
24 | }
25 |
26 | # build image
27 | docker build -t quiram/jenkins-aws-ecs .
28 | exit_code=$?
29 |
30 | graceful_exit ${exit_code}
31 |
--------------------------------------------------------------------------------
/shopfront/src/main/java/uk/co/danielbryant/shopping/shopfront/controllers/HomeController.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.shopfront.controllers;
2 |
3 | import org.springframework.beans.factory.annotation.Autowired;
4 | import org.springframework.stereotype.Controller;
5 | import org.springframework.ui.Model;
6 | import org.springframework.web.bind.annotation.RequestMapping;
7 | import uk.co.danielbryant.shopping.shopfront.services.ProductService;
8 |
9 | @Controller
10 | public class HomeController {
11 |
12 | @Autowired
13 | private ProductService productService;
14 |
15 | @RequestMapping("/")
16 | public String index(Model model) {
17 | model.addAttribute("products", productService.getProducts());
18 | return "index";
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/shopfront/src/main/java/uk/co/danielbryant/shopping/shopfront/services/dto/FlagDTO.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.shopfront.services.dto;
2 |
3 | public class FlagDTO {
4 |
5 | private Long flagId;
6 | private String name;
7 | private int portionIn;
8 |
9 | private FlagDTO() {
10 | // Needed by Spring
11 | }
12 |
13 | public FlagDTO(Long flagId, String name, int portionIn) {
14 | this.flagId = flagId;
15 | this.name = name;
16 | this.portionIn = portionIn;
17 | }
18 |
19 | public Long getFlagId() {
20 | return flagId;
21 | }
22 |
23 | public String getName() {
24 | return name;
25 | }
26 |
27 | public int getPortionIn() {
28 | return portionIn;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/stockmanager/src/test/resources/contracts/stockmanager/getOneStock-v2.groovy:
--------------------------------------------------------------------------------
1 | package contracts.stockmanager
2 |
3 | import org.springframework.cloud.contract.spec.Contract
4 |
5 | Contract.make {
6 | request {
7 | headers {
8 | accept("application/vnd.stock.v2+json")
9 | }
10 | method 'GET'
11 | url '/stocks/123'
12 | }
13 | response {
14 | status 200
15 | body(
16 | productId: "123",
17 | sku: "sku-123",
18 | amountAvailable: [
19 | total : 10,
20 | perPurchase: 5
21 | ]
22 | )
23 | headers {
24 | contentType('application/vnd.stock.v2+json')
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/shopfront/src/main/java/uk/co/danielbryant/shopping/shopfront/model/CircuitBreaker.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.shopfront.model;
2 |
3 | public class CircuitBreaker {
4 | private String name;
5 | private boolean isOpen;
6 |
7 | public CircuitBreaker() {
8 | // Needed by Spring
9 | }
10 |
11 | public CircuitBreaker(String name, boolean isOpen) {
12 | this.name = name;
13 | this.isOpen = isOpen;
14 | }
15 |
16 | public boolean isOpen() {
17 | return isOpen;
18 | }
19 |
20 | public String getName() {
21 | return name;
22 | }
23 |
24 | public void setName(String name) {
25 | this.name = name;
26 | }
27 |
28 | public void setOpen(boolean open) {
29 | isOpen = open;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/shopfront/src/main/java/uk/co/danielbryant/shopping/shopfront/ShopfrontApplication.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.shopfront;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 | import org.springframework.cloud.netflix.hystrix.EnableHystrix;
6 | import org.springframework.context.annotation.Bean;
7 | import org.springframework.web.client.RestTemplate;
8 |
9 | @SpringBootApplication
10 | @EnableHystrix
11 | public class ShopfrontApplication {
12 |
13 | public static void main(String[] args) {
14 | SpringApplication.run(ShopfrontApplication.class, args);
15 | }
16 |
17 | @Bean(name = "stdRestTemplate")
18 | public RestTemplate getRestTemplate() {
19 | return new RestTemplate();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/shopfront/src/main/java/uk/co/danielbryant/shopping/shopfront/resources/ProductResource.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.shopfront.resources;
2 |
3 | import org.springframework.beans.factory.annotation.Autowired;
4 | import org.springframework.web.bind.annotation.RequestMapping;
5 | import org.springframework.web.bind.annotation.RestController;
6 | import uk.co.danielbryant.shopping.shopfront.model.Product;
7 | import uk.co.danielbryant.shopping.shopfront.services.ProductService;
8 |
9 | import java.util.List;
10 |
11 | @RestController
12 | @RequestMapping("/products")
13 | public class ProductResource {
14 |
15 | @Autowired
16 | private ProductService productService;
17 |
18 | @RequestMapping()
19 | public List getProducts() {
20 | return productService.getProducts();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/jenkins-kubernetes/service-definitions/featureflags-db-service.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: Service
4 | metadata:
5 | name: featureflags-db
6 | labels:
7 | app: featureflags-db
8 | spec:
9 | type: NodePort
10 | selector:
11 | app: featureflags-db
12 | ports:
13 | - protocol: TCP
14 | port: 5432
15 | name: jdbc-postgresql
16 |
17 | ---
18 | apiVersion: apps/v1beta2
19 | kind: Deployment
20 | metadata:
21 | name: featureflags-db
22 | labels:
23 | app: featureflags-db
24 | spec:
25 | replicas: 1
26 | selector:
27 | matchLabels:
28 | app: featureflags-db
29 | template:
30 | metadata:
31 | labels:
32 | app: featureflags-db
33 | spec:
34 | containers:
35 | - name: featureflags-db
36 | image: quiram/featureflags-db
37 | ports:
38 | - containerPort: 5432
39 |
--------------------------------------------------------------------------------
/shopfront/src/main/java/uk/co/danielbryant/shopping/shopfront/resources/InternalResource.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.shopfront.resources;
2 |
3 | import org.springframework.beans.factory.annotation.Autowired;
4 | import org.springframework.web.bind.annotation.RequestMapping;
5 | import org.springframework.web.bind.annotation.RestController;
6 | import uk.co.danielbryant.shopping.shopfront.model.CircuitBreaker;
7 | import uk.co.danielbryant.shopping.shopfront.repo.CircuitBreakerRepo;
8 |
9 | import java.util.List;
10 |
11 | @RestController
12 | @RequestMapping("/internal")
13 | public class InternalResource {
14 |
15 | @Autowired
16 | private CircuitBreakerRepo circuitBreakerRepo;
17 |
18 | @RequestMapping("/circuit-breakers")
19 | public List getCircuitBreakers() {
20 | return circuitBreakerRepo.getAll();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/productcatalogue/src/test/java/uk/co/danielbryant/shopping/productcatalogue/model/v2/BulkPriceTest.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.productcatalogue.model.v2;
2 |
3 | import org.junit.Rule;
4 | import org.junit.Test;
5 | import org.junit.rules.ExpectedException;
6 |
7 | public class BulkPriceTest {
8 | @Rule
9 | public ExpectedException onBadData = ExpectedException.none();
10 |
11 | @Test
12 | public void priceMustBePresent() {
13 | onBadData.expect(IllegalArgumentException.class);
14 | onBadData.expectMessage("unit price");
15 | new BulkPrice(null, 2);
16 | }
17 |
18 | @Test
19 | public void minimumMustBeAtLeastTwo() {
20 | // no "bulk" if we can buy single items
21 | onBadData.expect(IllegalArgumentException.class);
22 | onBadData.expectMessage("minimum amount");
23 | new BulkPrice(new UnitPrice(1), 1);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/shopfront/src/main/java/uk/co/danielbryant/shopping/shopfront/services/dto/ProductDTO.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.shopfront.services.dto;
2 |
3 | import java.math.BigDecimal;
4 |
5 | public class ProductDTO {
6 | private String id;
7 | private String name;
8 | private String description;
9 | private BigDecimal price;
10 |
11 | public ProductDTO() {
12 | }
13 |
14 | public ProductDTO(String id, String name, String description, BigDecimal price) {
15 | this.id = id;
16 | this.name = name;
17 | this.description = description;
18 | this.price = price;
19 | }
20 |
21 | public String getId() {
22 | return id;
23 | }
24 |
25 | public String getName() {
26 | return name;
27 | }
28 |
29 | public String getDescription() {
30 | return description;
31 | }
32 |
33 | public BigDecimal getPrice() {
34 | return price;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/jenkins-kubernetes/service-definitions/shopfront-service.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: Service
4 | metadata:
5 | name: shopfront
6 | labels:
7 | app: shopfront
8 | spec:
9 | type: NodePort
10 | selector:
11 | app: shopfront
12 | ports:
13 | - protocol: TCP
14 | port: 8010
15 | name: http
16 |
17 | ---
18 | apiVersion: apps/v1beta2
19 | kind: Deployment
20 | metadata:
21 | name: shopfront
22 | labels:
23 | app: shopfront
24 | spec:
25 | replicas: 1
26 | selector:
27 | matchLabels:
28 | app: shopfront
29 | template:
30 | metadata:
31 | labels:
32 | app: shopfront
33 | spec:
34 | containers:
35 | - name: shopfront
36 | image: quiram/shopfront
37 | ports:
38 | - containerPort: 8010
39 | livenessProbe:
40 | httpGet:
41 | path: /health
42 | port: 8010
43 | initialDelaySeconds: 30
44 | timeoutSeconds: 1
--------------------------------------------------------------------------------
/jenkins-base/docker/org.jenkinsci.plugins.docker.commons.tools.DockerTool.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Default
6 |
7 |
8 |
9 |
10 |
11 | latest
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/acceptance-tests/src/test/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/jenkins-kubernetes/service-definitions/featureflags-service.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: Service
4 | metadata:
5 | name: featureflags
6 | labels:
7 | app: featureflags
8 | spec:
9 | type: NodePort
10 | selector:
11 | app: featureflags
12 | ports:
13 | - protocol: TCP
14 | port: 8040
15 | name: http
16 |
17 | ---
18 | apiVersion: apps/v1beta2
19 | kind: Deployment
20 | metadata:
21 | name: featureflags
22 | labels:
23 | app: featureflags
24 | spec:
25 | replicas: 1
26 | selector:
27 | matchLabels:
28 | app: featureflags
29 | template:
30 | metadata:
31 | labels:
32 | app: featureflags
33 | spec:
34 | containers:
35 | - name: featureflags
36 | image: quiram/featureflags
37 | ports:
38 | - containerPort: 8040
39 | livenessProbe:
40 | httpGet:
41 | path: /health
42 | port: 8040
43 | initialDelaySeconds: 30
44 | timeoutSeconds: 1
45 |
--------------------------------------------------------------------------------
/jenkins-kubernetes/service-definitions/stockmanager-service.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: Service
4 | metadata:
5 | name: stockmanager
6 | labels:
7 | app: stockmanager
8 | spec:
9 | type: NodePort
10 | selector:
11 | app: stockmanager
12 | ports:
13 | - protocol: TCP
14 | port: 8030
15 | name: http
16 |
17 | ---
18 | apiVersion: apps/v1beta2
19 | kind: Deployment
20 | metadata:
21 | name: stockmanager
22 | labels:
23 | app: stockmanager
24 | spec:
25 | replicas: 1
26 | selector:
27 | matchLabels:
28 | app: stockmanager
29 | template:
30 | metadata:
31 | labels:
32 | app: stockmanager
33 | spec:
34 | containers:
35 | - name: stockmanager
36 | image: quiram/stockmanager
37 | ports:
38 | - containerPort: 8030
39 | livenessProbe:
40 | httpGet:
41 | path: /health
42 | port: 8030
43 | initialDelaySeconds: 30
44 | timeoutSeconds: 1
45 |
--------------------------------------------------------------------------------
/shopfront/src/main/java/uk/co/danielbryant/shopping/shopfront/services/dto/StockDTO.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.shopfront.services.dto;
2 |
3 | import com.github.quiram.utils.ReflectiveToStringCompareEquals;
4 |
5 | public class StockDTO extends ReflectiveToStringCompareEquals {
6 | private String productId;
7 | private String sku;
8 | private int amountAvailable;
9 |
10 | public static final StockDTO DEFAULT_STOCK_DTO = new StockDTO("", "default", 999);
11 |
12 | public StockDTO() {
13 | }
14 |
15 | public StockDTO(String productId, String sku, int amountAvailable) {
16 | this.productId = productId;
17 | this.sku = sku;
18 | this.amountAvailable = amountAvailable;
19 | }
20 |
21 | public String getProductId() {
22 | return productId;
23 | }
24 |
25 | public String getSku() {
26 | return sku;
27 | }
28 |
29 | public int getAmountAvailable() {
30 | return amountAvailable;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/acceptance-tests/src/test/java/com/github/quiram/shopping/acceptancetests/steps/StepsBase.java:
--------------------------------------------------------------------------------
1 | package com.github.quiram.shopping.acceptancetests.steps;
2 |
3 | import io.restassured.response.Response;
4 |
5 | import static io.restassured.RestAssured.given;
6 | import static java.util.concurrent.TimeUnit.MINUTES;
7 | import static org.apache.http.HttpStatus.SC_OK;
8 | import static org.awaitility.Awaitility.await;
9 |
10 | abstract class StepsBase {
11 | private static boolean serviceIsReady(String serviceUrl) {
12 | final Response response;
13 |
14 | try {
15 | response = given()
16 | .when()
17 | .get(serviceUrl)
18 | .thenReturn();
19 | } catch (Exception e) {
20 | return false;
21 | }
22 |
23 | return response.statusCode() == SC_OK;
24 | }
25 |
26 | void waitForService(String serviceUrl) {
27 | await().atMost(3, MINUTES).until(() -> serviceIsReady(serviceUrl));
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/jenkins-kubernetes/service-definitions/productcatalogue-service.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: Service
4 | metadata:
5 | name: productcatalogue
6 | labels:
7 | app: productcatalogue
8 | spec:
9 | type: NodePort
10 | selector:
11 | app: productcatalogue
12 | ports:
13 | - protocol: TCP
14 | port: 8020
15 | name: http
16 |
17 | ---
18 | apiVersion: apps/v1beta2
19 | kind: Deployment
20 | metadata:
21 | name: productcatalogue
22 | labels:
23 | app: productcatalogue
24 | spec:
25 | replicas: 1
26 | selector:
27 | matchLabels:
28 | app: productcatalogue
29 | template:
30 | metadata:
31 | labels:
32 | app: productcatalogue
33 | spec:
34 | containers:
35 | - name: productcatalogue
36 | image: quiram/productcatalogue
37 | ports:
38 | - containerPort: 8020
39 | livenessProbe:
40 | httpGet:
41 | path: /healthcheck
42 | port: 8025
43 | initialDelaySeconds: 30
44 | timeoutSeconds: 1
45 |
--------------------------------------------------------------------------------
/shopfront/src/main/java/uk/co/danielbryant/shopping/shopfront/repo/CircuitBreakerRepo.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.shopfront.repo;
2 |
3 | import com.netflix.hystrix.HystrixCircuitBreaker;
4 | import com.netflix.hystrix.HystrixCommandKey;
5 | import org.springframework.stereotype.Component;
6 | import uk.co.danielbryant.shopping.shopfront.model.CircuitBreaker;
7 |
8 | import java.util.List;
9 | import java.util.Optional;
10 |
11 | import static com.github.quiram.utils.Collections.map;
12 | import static uk.co.danielbryant.shopping.shopfront.model.Constants.COMMAND_KEYS;
13 |
14 | @Component
15 | public class CircuitBreakerRepo {
16 | public List getAll() {
17 | return map(COMMAND_KEYS, (String key) -> {
18 | final HystrixCircuitBreaker circuitBreaker = HystrixCircuitBreaker.Factory.getInstance(HystrixCommandKey.Factory.asKey(key));
19 | return new CircuitBreaker(key, Optional.ofNullable(circuitBreaker).map(HystrixCircuitBreaker::isOpen).orElse(false));
20 | });
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/productcatalogue/src/main/java/uk/co/danielbryant/shopping/productcatalogue/model/v2/UnitPrice.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.productcatalogue.model.v2;
2 |
3 | import com.github.quiram.utils.ReflectiveToStringCompareEquals;
4 |
5 | import java.math.BigDecimal;
6 |
7 | import static com.github.quiram.utils.ArgumentChecks.ensureGreaterThanZero;
8 | import static com.github.quiram.utils.ArgumentChecks.ensureNotNull;
9 |
10 | public class UnitPrice extends ReflectiveToStringCompareEquals {
11 | private BigDecimal value;
12 |
13 | public UnitPrice() {
14 |
15 | }
16 |
17 | public UnitPrice(BigDecimal value) {
18 | ensureNotNull(value, "price");
19 | ensureGreaterThanZero(value.intValue(), "price");
20 | this.value = value;
21 | }
22 |
23 | public UnitPrice(int value) {
24 | this(new BigDecimal(value));
25 | }
26 |
27 | public BigDecimal getValue() {
28 | return value;
29 | }
30 |
31 | public void setValue(BigDecimal value) {
32 | this.value = value;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/productcatalogue/src/test/java/uk/co/danielbryant/shopping/productcatalogue/model/v2/UnitPriceTest.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.productcatalogue.model.v2;
2 |
3 | import org.junit.Rule;
4 | import org.junit.Test;
5 | import org.junit.rules.ExpectedException;
6 |
7 | import java.math.BigDecimal;
8 |
9 | public class UnitPriceTest {
10 | @Rule
11 | public ExpectedException onBadData = ExpectedException.none();
12 |
13 | @Test
14 | public void productCannotBeFree() {
15 | onBadData.expect(IllegalArgumentException.class);
16 | onBadData.expectMessage("price");
17 | new UnitPrice(new BigDecimal(0));
18 | }
19 |
20 | @Test
21 | public void priceCannotBeNegative() {
22 | onBadData.expect(IllegalArgumentException.class);
23 | onBadData.expectMessage("price");
24 | new UnitPrice(new BigDecimal(-1));
25 | }
26 |
27 | @Test
28 | public void priceCannotBeNull() {
29 | onBadData.expect(IllegalArgumentException.class);
30 | onBadData.expectMessage("price");
31 | new UnitPrice(null);
32 | }
33 | }
--------------------------------------------------------------------------------
/shopfront/src/main/java/uk/co/danielbryant/shopping/shopfront/services/dto/SmartThrottleFlag.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.shopfront.services.dto;
2 |
3 | import java.time.LocalDate;
4 |
5 | import static java.time.temporal.ChronoUnit.DAYS;
6 |
7 | public class SmartThrottleFlag {
8 | private int initialPortionIn;
9 | private LocalDate startDate;
10 | private int dailyIncrement;
11 |
12 | public SmartThrottleFlag(int initialPortionIn, LocalDate startDate, int dailyIncrement) {
13 | this.initialPortionIn = initialPortionIn;
14 | this.startDate = startDate;
15 | this.dailyIncrement = dailyIncrement;
16 | }
17 |
18 | public int getPortionIn() {
19 | final LocalDate now = LocalDate.now();
20 | if (startDate.isAfter(now)) {
21 | return 0;
22 | }
23 |
24 | long daysPast = DAYS.between(startDate, now);
25 | long totalIncrement = daysPast * dailyIncrement;
26 | long currentPortionIn = initialPortionIn + totalIncrement;
27 | return Math.min((int) currentPortionIn, 100);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/acceptance-tests/src/test/resources/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | services:
3 | shopfront:
4 | image: quiram/shopfront
5 | ports:
6 | - "8010:8010"
7 | depends_on:
8 | - productcatalogue
9 | - stockmanager
10 | - featureflags
11 | - external-adaptive-pricing
12 | links:
13 | - productcatalogue
14 | - stockmanager
15 | - featureflags
16 | - external-adaptive-pricing
17 | productcatalogue:
18 | image: quiram/productcatalogue
19 | ports:
20 | - "8020:8020"
21 | stockmanager:
22 | image: quiram/stockmanager
23 | ports:
24 | - "8030:8030"
25 | external-adaptive-pricing:
26 | image: quiram/fake-adaptive-pricing
27 | ports:
28 | - "8050:8050"
29 | featureflags:
30 | image: quiram/featureflags
31 | ports:
32 | - "8040:8040"
33 | depends_on:
34 | - test-featureflags-db
35 | links:
36 | - test-featureflags-db
37 | environment:
38 | - spring.profiles.active=test
39 | test-featureflags-db:
40 | image: quiram/test-featureflags-db
41 | ports:
42 | - "5432:5432"
43 |
--------------------------------------------------------------------------------
/jenkins-kubernetes/service-definitions/external-adaptive-pricing-service.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: Service
4 | metadata:
5 | name: external-adaptive-pricing
6 | labels:
7 | app: external-adaptive-pricing
8 | spec:
9 | type: NodePort
10 | selector:
11 | app: external-adaptive-pricing
12 | ports:
13 | - protocol: TCP
14 | port: 8050
15 | name: http
16 |
17 | ---
18 | apiVersion: apps/v1beta2
19 | kind: Deployment
20 | metadata:
21 | name: external-adaptive-pricing
22 | labels:
23 | app: external-adaptive-pricing
24 | spec:
25 | replicas: 1
26 | selector:
27 | matchLabels:
28 | app: external-adaptive-pricing
29 | template:
30 | metadata:
31 | labels:
32 | app: external-adaptive-pricing
33 | spec:
34 | containers:
35 | - name: external-adaptive-pricing
36 | image: quiram/external-adaptive-pricing
37 | ports:
38 | - containerPort: 8050
39 | livenessProbe:
40 | httpGet:
41 | path: /health
42 | port: 8050
43 | initialDelaySeconds: 30
44 | timeoutSeconds: 1
45 |
--------------------------------------------------------------------------------
/external-adaptive-pricing/src/test/java/com/github/quiram/shopping/adaptive/AdaptivePricingApplicationCT.java:
--------------------------------------------------------------------------------
1 | package com.github.quiram.shopping.adaptive;
2 |
3 | import com.github.quiram.shopping.adaptive.model.Price;
4 | import org.junit.Test;
5 | import org.junit.runner.RunWith;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.boot.test.context.SpringBootTest;
8 | import org.springframework.boot.test.web.client.TestRestTemplate;
9 | import org.springframework.test.context.junit4.SpringRunner;
10 |
11 | import static org.junit.Assert.assertNotNull;
12 |
13 | @RunWith(SpringRunner.class)
14 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = AdaptivePricingApplication.class)
15 | public class AdaptivePricingApplicationCT {
16 | @Autowired
17 | private TestRestTemplate restTemplate;
18 |
19 |
20 | @Test
21 | public void getAllFlags() {
22 | final Price price = restTemplate.getForObject("/price?productName=lala", Price.class);
23 | assertNotNull(price);
24 | assertNotNull(price.getPrice());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/acceptance-tests/src/test/java/com/github/quiram/shopping/acceptancetests/entities/Flag.java:
--------------------------------------------------------------------------------
1 | package com.github.quiram.shopping.acceptancetests.entities;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 |
5 | @JsonIgnoreProperties(ignoreUnknown = true)
6 | public class Flag {
7 | private Long flagId;
8 | private String name;
9 | private int portionIn;
10 |
11 | public Flag() {
12 |
13 | }
14 |
15 | public Flag(Long flagId, String name, int portionIn) {
16 | this.flagId = flagId;
17 | this.name = name;
18 | this.portionIn = portionIn;
19 | }
20 |
21 | public Long getFlagId() {
22 | return flagId;
23 | }
24 |
25 | public void setFlagId(Long flagId) {
26 | this.flagId = flagId;
27 | }
28 |
29 | public String getName() {
30 | return name;
31 | }
32 |
33 | public void setName(String name) {
34 | this.name = name;
35 | }
36 |
37 | public int getPortionIn() {
38 | return portionIn;
39 | }
40 |
41 | public void setPortionIn(int portionIn) {
42 | this.portionIn = portionIn;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/productcatalogue/src/main/java/uk/co/danielbryant/shopping/productcatalogue/model/v2/BulkPrice.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.productcatalogue.model.v2;
2 |
3 | import com.github.quiram.utils.ReflectiveToStringCompareEquals;
4 |
5 | import static com.github.quiram.utils.ArgumentChecks.ensure;
6 | import static com.github.quiram.utils.ArgumentChecks.ensureGreaterThan;
7 | import static com.github.quiram.utils.ArgumentChecks.ensureNotNull;
8 |
9 | public class BulkPrice extends ReflectiveToStringCompareEquals {
10 | private UnitPrice unit;
11 | private int min;
12 |
13 | public BulkPrice() {
14 |
15 | }
16 |
17 | public BulkPrice(UnitPrice unit, int min) {
18 | ensureNotNull(unit, "unit price");
19 | ensureGreaterThan(1, min, "minimum amount");
20 | this.unit = unit;
21 | this.min = min;
22 | }
23 |
24 | public UnitPrice getUnit() {
25 | return unit;
26 | }
27 |
28 | public void setUnit(UnitPrice unit) {
29 | this.unit = unit;
30 | }
31 |
32 | public int getMin() {
33 | return min;
34 | }
35 |
36 | public void setMin(int min) {
37 | this.min = min;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/stockmanager/src/test/resources/contracts/stockmanager/getAllStock.groovy:
--------------------------------------------------------------------------------
1 | package contracts.stockmanager
2 |
3 | import org.springframework.cloud.contract.spec.Contract
4 |
5 | Contract.make {
6 | request {
7 | method 'GET'
8 | url '/stocks'
9 | }
10 | response {
11 | status 200
12 | body(
13 | [
14 | [
15 | productId : "1",
16 | sku : "sku-1",
17 | amountAvailable: 10
18 | ],
19 | [
20 | productId : "2",
21 | sku : "sku-2",
22 | amountAvailable: 20
23 | ],
24 | [
25 | productId : "3",
26 | sku : "sku-3",
27 | amountAvailable: 30
28 | ]
29 | ]
30 | )
31 | headers {
32 | contentType('application/json')
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/stockmanager/src/main/java/uk/co/danielbryant/shopping/stockmanager/model/v1/Stock.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.stockmanager.model.v1;
2 |
3 | import com.github.quiram.utils.ReflectiveToStringCompareEquals;
4 |
5 | import static com.github.quiram.utils.ArgumentChecks.ensureNotBlank;
6 | import static com.github.quiram.utils.ArgumentChecks.ensureNotNegative;
7 |
8 | public class Stock extends ReflectiveToStringCompareEquals {
9 |
10 | private String productId;
11 | private String sku;
12 | private int amountAvailable;
13 |
14 | @SuppressWarnings("unused")
15 | private Stock() {
16 | // Needed by Spring
17 | }
18 |
19 | public Stock(String productId, String sku, int amountAvailable) {
20 | ensureNotNegative(amountAvailable, "amountAvailable");
21 | ensureNotBlank(productId, "productId");
22 | ensureNotBlank(sku, "sku");
23 |
24 | this.productId = productId;
25 | this.sku = sku;
26 | this.amountAvailable = amountAvailable;
27 | }
28 |
29 | public String getProductId() {
30 | return productId;
31 | }
32 |
33 | public String getSku() {
34 | return sku;
35 | }
36 |
37 | public int getAmountAvailable() {
38 | return amountAvailable;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/jenkins-kubernetes/credentials/credentials.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | GLOBAL
11 | minikube
12 |
13 |
14 | /var/jenkins_home/kubernetes/kubeconfig
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/external-adaptive-pricing/src/main/java/com/github/quiram/shopping/adaptive/resources/PriceResource.java:
--------------------------------------------------------------------------------
1 | package com.github.quiram.shopping.adaptive.resources;
2 |
3 | import com.github.quiram.shopping.adaptive.model.Price;
4 | import org.slf4j.Logger;
5 | import org.slf4j.LoggerFactory;
6 | import org.springframework.web.bind.annotation.*;
7 |
8 | import java.math.BigDecimal;
9 |
10 | import static com.github.quiram.utils.ArgumentChecks.ensureNotBlank;
11 | import static com.github.quiram.utils.Random.randomDouble;
12 | import static org.springframework.http.HttpStatus.BAD_REQUEST;
13 | import static org.springframework.web.bind.annotation.RequestMethod.GET;
14 |
15 | @RestController
16 | @RequestMapping("/price")
17 | public class PriceResource {
18 |
19 | private static final Logger LOGGER = LoggerFactory.getLogger(PriceResource.class);
20 |
21 | @RequestMapping(method = GET)
22 | public Price getPrice(@RequestParam(name = "productName") String productName) {
23 | LOGGER.info("getPriceFor '{}'", productName);
24 | ensureNotBlank(productName, "productName");
25 | return new Price(new BigDecimal(Double.toString(randomDouble(100, 2))));
26 | }
27 |
28 | @ExceptionHandler
29 | @ResponseStatus(BAD_REQUEST)
30 | public void handleMissingParameter(IllegalArgumentException e) {
31 | }
32 |
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/productcatalogue/src/test/java/uk/co/danielbryant/shopping/productcatalogue/model/v2/PriceTest.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.productcatalogue.model.v2;
2 |
3 | import org.junit.Rule;
4 | import org.junit.Test;
5 | import org.junit.rules.ExpectedException;
6 |
7 | public class PriceTest {
8 | @Rule
9 | public ExpectedException onBadData = ExpectedException.none();
10 |
11 |
12 | @Test
13 | public void priceCannotBeNull() {
14 | onBadData.expect(IllegalArgumentException.class);
15 | onBadData.expectMessage("single price");
16 | new Price(null, null);
17 | }
18 |
19 | @Test
20 | public void bulkPriceCanBeNull() {
21 | new Price(new UnitPrice(1), null);
22 | }
23 |
24 | @Test
25 | public void bulkPriceCannotBeSameAsSinglePrice() {
26 | onBadData.expect(IllegalArgumentException.class);
27 | onBadData.expectMessage("single price");
28 | onBadData.expectMessage("bulk price");
29 | new Price(new UnitPrice(1), new BulkPrice(new UnitPrice(1), 2));
30 | }
31 |
32 | @Test
33 | public void bulkPriceCannotBeHigherThanSinglePrice() {
34 | onBadData.expect(IllegalArgumentException.class);
35 | onBadData.expectMessage("single price");
36 | onBadData.expectMessage("bulk price");
37 | new Price(new UnitPrice(1), new BulkPrice(new UnitPrice(2), 2));
38 | }
39 |
40 | }
--------------------------------------------------------------------------------
/acceptance-tests/src/test/java/com/github/quiram/shopping/acceptancetests/steps/FeatureFlagsSteps.java:
--------------------------------------------------------------------------------
1 | package com.github.quiram.shopping.acceptancetests.steps;
2 |
3 | import com.github.quiram.shopping.acceptancetests.entities.Flag;
4 | import net.thucydides.core.annotations.Step;
5 |
6 | import static io.restassured.RestAssured.given;
7 | import static io.restassured.http.ContentType.JSON;
8 | import static java.lang.String.format;
9 |
10 | public class FeatureFlagsSteps extends StepsBase {
11 | private static final String FEATURE_FLAGS_IP = System.getenv("feature_flags_ip");
12 | private static final String FEATURE_FLAGS_URL = format("http://%s:8040/flags/", FEATURE_FLAGS_IP);
13 |
14 | @Step
15 | public void feature_flags_service_is_ready() {
16 | waitForService(FEATURE_FLAGS_URL);
17 | }
18 |
19 | @Step("Admin sets the adaptive pricing feature flag to {0}%")
20 | public void admin_sets_the_adaptive_pricing_feature_flag_to(int portionIn) {
21 | final Flag currentFlag = given().contentType(JSON).get(FEATURE_FLAGS_URL + "1").body().as(Flag.class);
22 | final Flag newFlag = new Flag(currentFlag.getFlagId(), currentFlag.getName(), portionIn);
23 |
24 | given()
25 | .contentType(JSON)
26 | .body(newFlag)
27 | .when()
28 | .put(FEATURE_FLAGS_URL + "1")
29 | .thenReturn();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/shopfront/src/main/java/uk/co/danielbryant/shopping/shopfront/services/FeatureFlagsService.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.shopfront.services;
2 |
3 | import org.springframework.beans.factory.annotation.Autowired;
4 | import org.springframework.beans.factory.annotation.Value;
5 | import org.springframework.stereotype.Service;
6 | import uk.co.danielbryant.shopping.shopfront.repo.FeatureFlagsRepo;
7 | import uk.co.danielbryant.shopping.shopfront.services.dto.FlagDTO;
8 |
9 | import java.util.Optional;
10 | import java.util.Random;
11 |
12 | @Service
13 | public class FeatureFlagsService {
14 | private final FeatureFlagsRepo featureFlagsRepo;
15 | private final Random random;
16 |
17 | @Autowired
18 | public FeatureFlagsService(FeatureFlagsRepo featureFlagsRepo, @Value("#{new java.util.Random()}") Random random) {
19 | this.featureFlagsRepo = featureFlagsRepo;
20 | this.random = random;
21 | }
22 |
23 | // Get value of flag and check if a randomly generated value falls within
24 | public boolean shouldApplyFeatureWithFlag(long flagId) {
25 | final Optional flag = featureFlagsRepo.getFlag(flagId);
26 | return flag.map(FlagDTO::getPortionIn).map(this::randomWithinPortion)
27 | .orElse(false);
28 | }
29 |
30 | private boolean randomWithinPortion(int portionIn) {
31 | return random.nextInt(100) < portionIn;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/jenkins-base/jobs/deploy/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | false
5 |
6 |
7 |
8 |
9 | project_name
10 |
11 | none
12 | true
13 |
14 |
15 |
16 |
17 |
18 | true
19 | false
20 | false
21 | false
22 |
23 | false
24 |
25 |
26 | echo "This is where we would deploy service ${project_name}; the actual deployment is performed in different Jenkins instances according to different strategies."
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/productcatalogue/src/main/java/uk/co/danielbryant/shopping/productcatalogue/services/ProductService.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.productcatalogue.services;
2 |
3 | import uk.co.danielbryant.shopping.productcatalogue.model.v2.Product;
4 |
5 | import java.util.*;
6 |
7 | import static uk.co.danielbryant.shopping.productcatalogue.model.v2.Price.complexPrice;
8 | import static uk.co.danielbryant.shopping.productcatalogue.model.v2.Price.singlePrice;
9 |
10 | public class ProductService {
11 |
12 | //{productId, Product}
13 | private Map fakeProductDAO = new HashMap<>();
14 |
15 | public ProductService() {
16 | fakeProductDAO.put("1", new Product("1", "Widget", "Premium ACME Widgets", complexPrice("1.20", "1.00", 5)));
17 | fakeProductDAO.put("2", new Product("2", "Sprocket", "Grade B sprockets", singlePrice("4.10")));
18 | fakeProductDAO.put("3", new Product("3", "Anvil", "Large Anvils", complexPrice("45.50", "45.00", 10)));
19 | fakeProductDAO.put("4", new Product("4", "Cogs", "Grade Y cogs", singlePrice("1.80")));
20 | fakeProductDAO.put("5", new Product("5", "Multitool", "Multitools", singlePrice("154.10")));
21 | }
22 |
23 | public List getAllProducts() {
24 | return new ArrayList<>(fakeProductDAO.values());
25 | }
26 |
27 | public Optional getProduct(String id) {
28 | return Optional.ofNullable(fakeProductDAO.get(id));
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/featureflags/src/main/java/com/github/quiram/shopping/featureflags/model/Flag.java:
--------------------------------------------------------------------------------
1 | package com.github.quiram.shopping.featureflags.model;
2 |
3 | import com.github.quiram.utils.ReflectiveToStringCompareEquals;
4 |
5 | import javax.persistence.Entity;
6 | import javax.persistence.GeneratedValue;
7 | import javax.persistence.Id;
8 |
9 | import static com.github.quiram.utils.ArgumentChecks.*;
10 |
11 | @Entity
12 | public class Flag extends ReflectiveToStringCompareEquals {
13 |
14 | @Id
15 | @GeneratedValue
16 | private Long flagId;
17 | private String name;
18 | private int portionIn;
19 | private boolean sticky;
20 |
21 | private Flag() {
22 | // Needed by Spring
23 | }
24 |
25 | public Flag(Long flagId, String name, int portionIn, boolean sticky) {
26 | ensureInRange(0, 100, portionIn, "portionIn");
27 | ensureNotBlank(name, "name");
28 |
29 | this.flagId = flagId;
30 | this.name = name;
31 | this.portionIn = portionIn;
32 | this.sticky = sticky;
33 | }
34 |
35 | public Long getFlagId() {
36 | return flagId;
37 | }
38 |
39 | public String getName() {
40 | return name;
41 | }
42 |
43 | public int getPortionIn() {
44 | return portionIn;
45 | }
46 |
47 | public boolean isSticky() {
48 | return sticky;
49 | }
50 |
51 | public void setSticky(boolean sticky) {
52 | this.sticky = sticky;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/stockmanager/src/main/java/uk/co/danielbryant/shopping/stockmanager/services/StockService.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.stockmanager.services;
2 |
3 | import org.springframework.beans.factory.annotation.Autowired;
4 | import org.springframework.stereotype.Service;
5 | import uk.co.danielbryant.shopping.stockmanager.exceptions.StockNotFoundException;
6 | import uk.co.danielbryant.shopping.stockmanager.model.v2.Stock;
7 | import uk.co.danielbryant.shopping.stockmanager.repositories.StockRepository;
8 |
9 | import java.util.List;
10 | import java.util.Optional;
11 | import java.util.stream.Collectors;
12 | import java.util.stream.StreamSupport;
13 |
14 | @Service
15 | public class StockService {
16 |
17 | @Autowired
18 | private StockRepository stockRepository;
19 |
20 | public StockService() {
21 |
22 | }
23 |
24 | @SuppressWarnings("unused")
25 | public StockService(StockRepository stockRepository) {
26 | this.stockRepository = stockRepository;
27 | }
28 |
29 | public List getStocks() {
30 | return StreamSupport.stream(stockRepository.findAll().spliterator(), false)
31 | .collect(Collectors.toList());
32 | }
33 |
34 | public Stock getStock(String productId) throws StockNotFoundException {
35 | return Optional.ofNullable(stockRepository.findOne(productId))
36 | .orElseThrow(() -> new StockNotFoundException("Stock not found with productId: " + productId));
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/stockmanager/src/main/java/uk/co/danielbryant/shopping/stockmanager/model/v2/AmountAvailable.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.stockmanager.model.v2;
2 |
3 | import com.github.quiram.utils.ArgumentChecks;
4 | import com.github.quiram.utils.ReflectiveToStringCompareEquals;
5 |
6 | import javax.persistence.Embeddable;
7 |
8 | import static com.github.quiram.utils.ArgumentChecks.ensure;
9 | import static com.github.quiram.utils.ArgumentChecks.ensureNotNegative;
10 |
11 | @Embeddable
12 | public class AmountAvailable extends ReflectiveToStringCompareEquals {
13 | private int total;
14 | private int perPurchase;
15 |
16 | @SuppressWarnings("unused")
17 | public AmountAvailable() {
18 | // Needed for Spring
19 | }
20 |
21 | public AmountAvailable(int total, int perPurchase) {
22 | ensureNotNegative(total, "total");
23 | ensureNotNegative(perPurchase, "perPurchase");
24 | ensure(() -> perPurchase <= total, "perPurchase must not be higher than total");
25 |
26 | this.total = total;
27 | this.perPurchase = perPurchase;
28 | }
29 |
30 | public int getPerPurchase() {
31 | return perPurchase;
32 | }
33 |
34 | public void setPerPurchase(int perPurchase) {
35 | this.perPurchase = perPurchase;
36 | }
37 |
38 | public int getTotal() {
39 | return total;
40 | }
41 |
42 | public void setTotal(int total) {
43 | this.total = total;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/jenkins-base/plugins/plugins-list:
--------------------------------------------------------------------------------
1 | ace-editor
2 | ant
3 | antisamy-markup-formatter
4 | apache-httpcomponents-client-4-api
5 | authentication-tokens
6 | bouncycastle-api
7 | branch-api
8 | build-timeout
9 | cloudbees-folder
10 | command-launcher
11 | credentials
12 | credentials-binding
13 | display-url-api
14 | docker-build-publish
15 | docker-commons
16 | docker-workflow
17 | durable-task
18 | email-ext
19 | git
20 | git-client
21 | git-server
22 | github
23 | github-api
24 | github-branch-source
25 | gradle
26 | handlebars
27 | htmlpublisher
28 | jackson2-api
29 | jdk-tool
30 | jquery-detached
31 | jsch
32 | junit
33 | ldap
34 | mailer
35 | mapdb-api
36 | matrix-auth
37 | matrix-project
38 | momentjs
39 | pam-auth
40 | parameterized-trigger
41 | pipeline-build-step
42 | pipeline-github-lib
43 | pipeline-graph-analysis
44 | pipeline-input-step
45 | pipeline-milestone-step
46 | pipeline-model-api
47 | pipeline-model-declarative-agent
48 | pipeline-model-definition
49 | pipeline-model-extensions
50 | pipeline-rest-api
51 | pipeline-stage-step
52 | pipeline-stage-tags-metadata
53 | pipeline-stage-view
54 | plain-credentials
55 | resource-disposer
56 | scm-api
57 | script-security
58 | ssh-credentials
59 | ssh-slaves
60 | structs
61 | subversion
62 | timestamper
63 | token-macro
64 | workflow-aggregator
65 | workflow-api
66 | workflow-basic-steps
67 | workflow-cps
68 | workflow-cps-global-lib
69 | workflow-durable-task-step
70 | workflow-job
71 | workflow-multibranch
72 | workflow-scm-step
73 | workflow-step-api
74 | workflow-support
75 | ws-cleanup
--------------------------------------------------------------------------------
/productcatalogue/src/main/java/uk/co/danielbryant/shopping/productcatalogue/model/v1/Product.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.productcatalogue.model.v1;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 |
5 | import java.math.BigDecimal;
6 |
7 | import static com.github.quiram.utils.ArgumentChecks.*;
8 |
9 | public class Product {
10 | private String id;
11 | private String name;
12 | private String description;
13 | private BigDecimal price;
14 |
15 | private Product() {
16 | // Needed for Jackson deserialization
17 | }
18 |
19 | public Product(String id, String name, String description, BigDecimal price) {
20 | ensureNotBlank(id, "id");
21 | ensureNotBlank(name, "name");
22 | ensureNotBlank(description, "description");
23 | ensurePrice(price);
24 |
25 | this.id = id;
26 | this.name = name;
27 | this.description = description;
28 | this.price = price;
29 | }
30 |
31 | private void ensurePrice(BigDecimal price) {
32 | ensureNotNull(price, "price");
33 | ensureGreaterThanZero(price.intValue(), "price");
34 | }
35 |
36 | @JsonProperty
37 | public String getId() {
38 | return id;
39 | }
40 |
41 | @JsonProperty
42 | public String getName() {
43 | return name;
44 | }
45 |
46 | @JsonProperty
47 | public String getDescription() {
48 | return description;
49 | }
50 |
51 | @JsonProperty
52 | public BigDecimal getPrice() {
53 | return price;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/shopfront/src/main/java/uk/co/danielbryant/shopping/shopfront/repo/ProductRepo.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.shopfront.repo;
2 |
3 | import org.springframework.beans.factory.annotation.Autowired;
4 | import org.springframework.beans.factory.annotation.Qualifier;
5 | import org.springframework.beans.factory.annotation.Value;
6 | import org.springframework.core.ParameterizedTypeReference;
7 | import org.springframework.http.HttpMethod;
8 | import org.springframework.http.ResponseEntity;
9 | import org.springframework.stereotype.Component;
10 | import org.springframework.web.client.RestTemplate;
11 | import uk.co.danielbryant.shopping.shopfront.services.dto.ProductDTO;
12 |
13 | import java.util.List;
14 | import java.util.Map;
15 | import java.util.function.Function;
16 | import java.util.stream.Collectors;
17 |
18 | @Component
19 | public class ProductRepo {
20 |
21 | @Value("${productCatalogueUri}")
22 | private String productCatalogueUri;
23 |
24 | @Autowired
25 | @Qualifier(value = "stdRestTemplate")
26 | private RestTemplate restTemplate;
27 |
28 |
29 | public Map getProductDTOs() {
30 | ResponseEntity> productCatalogueResponse =
31 | restTemplate.exchange(productCatalogueUri + "/products",
32 | HttpMethod.GET, null, new ParameterizedTypeReference>() {
33 | });
34 | List productDTOs = productCatalogueResponse.getBody();
35 |
36 | return productDTOs.stream()
37 | .collect(Collectors.toMap(ProductDTO::getId, Function.identity()));
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/stockmanager/src/main/java/uk/co/danielbryant/shopping/stockmanager/model/v2/Stock.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.stockmanager.model.v2;
2 |
3 | import com.github.quiram.utils.ReflectiveToStringCompareEquals;
4 |
5 | import javax.persistence.Embedded;
6 | import javax.persistence.Entity;
7 | import javax.persistence.Id;
8 |
9 | import static com.github.quiram.utils.ArgumentChecks.ensureNotBlank;
10 | import static com.github.quiram.utils.ArgumentChecks.ensureNotNull;
11 |
12 | @Entity
13 | public class Stock extends ReflectiveToStringCompareEquals {
14 |
15 | @Id
16 | private String productId;
17 | private String sku;
18 | @Embedded
19 | private AmountAvailable amountAvailable;
20 |
21 | private Stock() {
22 | // Needed by Spring
23 | }
24 |
25 | public Stock(String productId, String sku, AmountAvailable amountAvailable) {
26 | ensureNotNull(amountAvailable, "amountAvailable");
27 | ensureNotBlank(productId, "productId");
28 | ensureNotBlank(sku, "sku");
29 |
30 | this.productId = productId;
31 | this.sku = sku;
32 | this.amountAvailable = amountAvailable;
33 | }
34 |
35 | public String getProductId() {
36 | return productId;
37 | }
38 |
39 | public String getSku() {
40 | return sku;
41 | }
42 |
43 | public AmountAvailable getAmountAvailable() {
44 | return amountAvailable;
45 | }
46 |
47 | public uk.co.danielbryant.shopping.stockmanager.model.v1.Stock asV1Stock() {
48 | return new uk.co.danielbryant.shopping.stockmanager.model.v1.Stock(productId, sku, amountAvailable.getTotal());
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/shopfront/src/main/java/uk/co/danielbryant/shopping/shopfront/model/Product.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.shopfront.model;
2 |
3 | import com.github.quiram.utils.ReflectiveToStringCompareEquals;
4 |
5 | import java.math.BigDecimal;
6 |
7 | public class Product extends ReflectiveToStringCompareEquals {
8 | private String id;
9 | private String sku;
10 | private String name;
11 | private String description;
12 | private BigDecimal price;
13 | private int amountAvailable;
14 |
15 | private Product() {
16 | // For deserialisation
17 | }
18 |
19 | public Product(String id, String name, String description, BigDecimal price) {
20 | this.id = id;
21 | this.name = name;
22 | this.description = description;
23 | this.price = price;
24 | }
25 |
26 | public Product(String id, String sku, String name, String description, BigDecimal price, int amountAvailable) {
27 | this.id = id;
28 | this.sku = sku;
29 | this.name = name;
30 | this.description = description;
31 | this.price = price;
32 | this.amountAvailable = amountAvailable;
33 | }
34 |
35 | public String getId() {
36 | return id;
37 | }
38 |
39 | public String getSku() {
40 | return sku;
41 | }
42 |
43 | public String getName() {
44 | return name;
45 | }
46 |
47 | public String getDescription() {
48 | return description;
49 | }
50 |
51 | public BigDecimal getPrice() {
52 | return price;
53 | }
54 |
55 | public int getAmountAvailable() {
56 | return amountAvailable;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/jenkins-base/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM jenkins/jenkins
2 |
3 | USER root
4 |
5 | # Add sudo capabilities
6 | RUN apt-get update && apt-get install -y sudo && rm -rf /var/lib/apt/lists/*
7 | RUN echo "jenkins ALL=NOPASSWD: ALL" >> /etc/sudoers
8 |
9 | # Install vim (useful to have there)
10 | RUN apt-get update && apt-get install -y vim
11 |
12 | # Configure Maven to be auto-installed in Jenkins
13 | COPY --chown=jenkins:jenkins mvn/* /var/jenkins_home/
14 |
15 | # Easy access to maven in command-line (once installed)
16 | RUN ln -s /var/jenkins_home/tools/hudson.tasks.Maven_MavenInstallation/Default/bin/mvn /usr/bin/mvn
17 |
18 | # Configure Docker to be auto-installed in Jenkins
19 | COPY --chown=jenkins:jenkins docker/org.jenkinsci.plugins.docker.commons.tools.DockerTool.xml /var/jenkins_home/
20 |
21 | # Easy access to docker in command-line (once installed)
22 | RUN ln -s /var/jenkins_home/tools/org.jenkinsci.plugins.docker.commons.tools.DockerTool/Default/bin/docker /usr/bin/docker
23 |
24 | # Install Docker Compose
25 | RUN curl -L https://github.com/docker/compose/releases/download/1.21.0/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
26 | RUN chmod a+x /usr/local/bin/docker-compose
27 |
28 | # Pre-load all necessary plugins
29 | COPY --chown=jenkins:jenkins plugins/plugins-list /var/jenkins_home/
30 | RUN /usr/local/bin/install-plugins.sh `cat /var/jenkins_home/plugins-list`
31 |
32 | # Configure build all three pipelines
33 | COPY --chown=jenkins:jenkins jobs/ /var/jenkins_home/jobs/
34 |
35 | # Relax security rules to be able to display Serenity BDD reports correctly
36 | ENV JAVA_OPTS -Dhudson.model.DirectoryBrowserSupport.CSP=\"\"
37 |
38 | USER jenkins
39 |
--------------------------------------------------------------------------------
/productcatalogue/src/main/java/uk/co/danielbryant/shopping/productcatalogue/model/v2/Product.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.productcatalogue.model.v2;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 |
5 | import static com.github.quiram.utils.ArgumentChecks.ensureNotBlank;
6 | import static com.github.quiram.utils.ArgumentChecks.ensureNotNull;
7 |
8 | public class Product {
9 | private String id;
10 | private String name;
11 | private String description;
12 | private Price price;
13 |
14 | private Product() {
15 | // Needed for Jackson deserialization
16 | }
17 |
18 | public Product(String id, String name, String description, Price price) {
19 | ensureNotBlank(id, "id");
20 | ensureNotBlank(name, "name");
21 | ensureNotBlank(description, "description");
22 | ensureNotNull(price, "price");
23 |
24 | this.id = id;
25 | this.name = name;
26 | this.description = description;
27 | this.price = price;
28 | }
29 |
30 | @JsonProperty
31 | public String getId() {
32 | return id;
33 | }
34 |
35 | @JsonProperty
36 | public String getName() {
37 | return name;
38 | }
39 |
40 | @JsonProperty
41 | public String getDescription() {
42 | return description;
43 | }
44 |
45 | @JsonProperty
46 | public Price getPrice() {
47 | return price;
48 | }
49 |
50 | public uk.co.danielbryant.shopping.productcatalogue.model.v1.Product asV1Product() {
51 | return new uk.co.danielbryant.shopping.productcatalogue.model.v1.Product(id, name, description, price.getSingle().getValue());
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/productcatalogue/src/main/java/uk/co/danielbryant/shopping/productcatalogue/resources/v2/ProductResource.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.productcatalogue.resources.v2;
2 |
3 | import com.codahale.metrics.annotation.Timed;
4 | import com.google.inject.Inject;
5 | import uk.co.danielbryant.shopping.productcatalogue.model.v2.Product;
6 | import uk.co.danielbryant.shopping.productcatalogue.services.ProductService;
7 |
8 | import javax.ws.rs.GET;
9 | import javax.ws.rs.Path;
10 | import javax.ws.rs.PathParam;
11 | import javax.ws.rs.Produces;
12 | import javax.ws.rs.core.MediaType;
13 | import javax.ws.rs.core.Response;
14 | import java.util.Optional;
15 |
16 | @Path("/v2/products")
17 | @Produces(MediaType.APPLICATION_JSON)
18 | public class ProductResource {
19 |
20 | private ProductService productService;
21 |
22 | @Inject
23 | public ProductResource(ProductService productService) {
24 | this.productService = productService;
25 | }
26 |
27 | @GET
28 | @Timed
29 | public Response getAllProducts() {
30 | return Response.status(200)
31 | .entity(productService.getAllProducts())
32 | .build();
33 | }
34 |
35 | @GET
36 | @Timed
37 | @Path("{id}")
38 | public Response getProduct(@PathParam("id") String id) {
39 | Optional result = productService.getProduct(id);
40 |
41 | if (result.isPresent()) {
42 | return Response.status(Response.Status.OK)
43 | .entity(result.get())
44 | .build();
45 | } else {
46 | return Response.status(Response.Status.NOT_FOUND)
47 | .build();
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/productcatalogue/src/main/java/uk/co/danielbryant/shopping/productcatalogue/ProductServiceApplication.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.productcatalogue;
2 |
3 | import com.google.inject.Guice;
4 | import com.google.inject.Injector;
5 | import io.dropwizard.Application;
6 | import io.dropwizard.setup.Bootstrap;
7 | import io.dropwizard.setup.Environment;
8 | import uk.co.danielbryant.shopping.productcatalogue.configuration.ProductServiceConfiguration;
9 | import uk.co.danielbryant.shopping.productcatalogue.healthchecks.BasicHealthCheck;
10 | import uk.co.danielbryant.shopping.productcatalogue.resources.v2.ProductResource;
11 |
12 | public class ProductServiceApplication extends Application {
13 | public static void main(String[] args) throws Exception {
14 | new ProductServiceApplication().run(args);
15 | }
16 |
17 | @Override
18 | public String getName() {
19 | return "product-list-service";
20 | }
21 |
22 | @Override
23 | public void initialize(Bootstrap bootstrap) {
24 | // nothing to do yet
25 | }
26 |
27 | @Override
28 | public void run(ProductServiceConfiguration config,
29 | Environment environment) {
30 | final BasicHealthCheck healthCheck = new BasicHealthCheck(config.getVersion());
31 | environment.healthChecks().register("template", healthCheck);
32 |
33 | Injector injector = Guice.createInjector();
34 | environment.jersey().register(injector.getInstance(uk.co.danielbryant.shopping.productcatalogue.resources.v1.ProductResource.class));
35 | environment.jersey().register(injector.getInstance(ProductResource.class));
36 | }
37 | }
--------------------------------------------------------------------------------
/jenkins-kubernetes/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
4 |
5 | pushd ${SCRIPT_DIR}
6 |
7 | graceful_exit() {
8 | eval "popd; exit $1" # This ensures that popd is executed without affecting the result code of the script
9 | }
10 |
11 | no_file_error_message() {
12 | file_name=$1
13 |
14 | echo "${file_name} file not found"
15 | echo "You need to run minikube first to generate secret files."
16 |
17 | graceful_exit -1
18 | }
19 |
20 | ensure_file() {
21 | file_name=$1
22 |
23 | [ -f ${file_name} ] || no_file_error_message ${file_name}
24 | }
25 |
26 | # Copy files to connect to minikube
27 |
28 | source_dir=$HOME/.minikube
29 | dest_dir=kubernetes/secrets
30 | secret_files="ca.crt client.crt client.key"
31 |
32 | mkdir -p ${dest_dir}
33 |
34 | for file in ${secret_files}; do
35 | ensure_file ${source_dir}/${file}
36 | cp ${source_dir}/${file} ${dest_dir}/${file}
37 | done
38 |
39 | # Copy IP address into kubeconfig
40 |
41 | minikube_ip=$(minikube ip)
42 |
43 | cat kubernetes/kubeconfig_template | sed -e s/"%MINIKUBE_IP%"/"${minikube_ip}"/ > kubernetes/kubeconfig
44 |
45 | # build image
46 | docker build -t quiram/jenkins-kubernetes .
47 | exit_code=$?
48 |
49 | # show message about not publishing this image (delete from my docker hub!)
50 |
51 | echo
52 | echo "The image quiram/jenkins-minikube is ready to be used, you can run it with the accompanying docker-compose.yml"
53 | echo "WARNING: this image contains the private key to access your locally-run minikube."
54 | echo "WARNING: it is not advisable to publish this image into a publicly-accessible docker repository."
55 | echo
56 |
57 | graceful_exit ${exit_code}
58 |
--------------------------------------------------------------------------------
/stockmanager/src/main/java/uk/co/danielbryant/shopping/stockmanager/config/DataGenerator.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.stockmanager.config;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import org.springframework.stereotype.Component;
7 | import org.springframework.transaction.annotation.Transactional;
8 | import uk.co.danielbryant.shopping.stockmanager.model.v2.AmountAvailable;
9 | import uk.co.danielbryant.shopping.stockmanager.model.v2.Stock;
10 | import uk.co.danielbryant.shopping.stockmanager.repositories.StockRepository;
11 |
12 | import javax.annotation.PostConstruct;
13 |
14 | @Component
15 | public class DataGenerator {
16 |
17 | private static final Logger LOGGER = LoggerFactory.getLogger(DataGenerator.class);
18 |
19 | private StockRepository stockRepository;
20 |
21 | @Autowired
22 | protected DataGenerator(StockRepository stockRepository) {
23 | this.stockRepository = stockRepository;
24 | }
25 |
26 | @PostConstruct
27 | @Transactional
28 | public void init() {
29 | LOGGER.info("Generating synthetic data for demonstration purposes...");
30 |
31 | stockRepository.save(new Stock("1", "12345678", new AmountAvailable(5, 2)));
32 | stockRepository.save(new Stock("2", "34567890", new AmountAvailable(2, 2)));
33 | stockRepository.save(new Stock("3", "54326745", new AmountAvailable(999, 500)));
34 | stockRepository.save(new Stock("4", "93847614", new AmountAvailable(0, 0)));
35 | stockRepository.save(new Stock("5", "11856388", new AmountAvailable(1, 1)));
36 |
37 | LOGGER.info("... data generation complete");
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/shopfront/src/test/java/uk/co/danielbryant/shopping/shopfront/StockManagerCDC.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.shopfront;
2 |
3 | import org.junit.Test;
4 | import org.junit.runner.RunWith;
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import org.springframework.boot.test.context.SpringBootTest;
7 | import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
8 | import org.springframework.test.annotation.DirtiesContext;
9 | import org.springframework.test.context.junit4.SpringRunner;
10 | import uk.co.danielbryant.shopping.shopfront.repo.StockRepo;
11 | import uk.co.danielbryant.shopping.shopfront.services.dto.StockDTO;
12 |
13 | import java.util.Map;
14 |
15 | import static org.hamcrest.Matchers.hasItems;
16 | import static org.hamcrest.Matchers.hasSize;
17 | import static org.junit.Assert.assertEquals;
18 | import static org.junit.Assert.assertThat;
19 |
20 | @RunWith(SpringRunner.class)
21 | @SpringBootTest
22 | @AutoConfigureStubRunner(ids = {"uk.co.danielbryant.shopping:stockmanager:+:stubs:8030"}, workOffline = true)
23 | @DirtiesContext
24 | public class StockManagerCDC {
25 | @Autowired
26 | private StockRepo stockRepo;
27 |
28 | @Test
29 | public void canReadResponseWithIndividualStock() {
30 | final StockDTO expected = new StockDTO("123", "sku-123", 10);
31 | final StockDTO actual = stockRepo.getStockDTO("123");
32 | assertEquals(expected, actual);
33 | }
34 |
35 | @Test
36 | public void canReadResponseWithAllStock() {
37 | final Map stockDTOs = stockRepo.getStockDTOs();
38 | assertThat(stockDTOs.values(), hasSize(3));
39 | assertThat(stockDTOs.keySet(), hasItems("1", "2", "3"));
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/featureflags/src/test/java/com/github/quiram/shopping/featureflags/model/FlagTest.java:
--------------------------------------------------------------------------------
1 | package com.github.quiram.shopping.featureflags.model;
2 |
3 | import org.junit.Rule;
4 | import org.junit.Test;
5 | import org.junit.rules.ExpectedException;
6 |
7 | import java.util.function.Function;
8 |
9 | import static com.github.quiram.utils.Random.randomBoolean;
10 | import static com.github.quiram.test_utils.ArgumentChecks.BLANK_VALUES;
11 | import static com.github.quiram.test_utils.ArgumentChecks.assertIllegalArguments;
12 | import static com.github.quiram.utils.Random.randomLong;
13 | import static com.github.quiram.utils.Random.randomString;
14 | import static java.util.Arrays.asList;
15 |
16 | public class FlagTest {
17 | @Rule
18 | public ExpectedException onBadInput = ExpectedException.none();
19 |
20 | @Test
21 | public void portionMustBeValidPercentage() {
22 | assertIllegalArguments(portion -> new Flag(randomLong(), randomString(), portion, randomBoolean()), "portionIn", asList(-1, 101));
23 | }
24 |
25 | @Test
26 | public void portionInCanBeZero() {
27 | new Flag(randomLong(), randomString(), 0, randomBoolean());
28 | }
29 |
30 | @Test
31 | public void portionInCanBePositive() {
32 | new Flag(randomLong(), randomString(), 10, randomBoolean());
33 | }
34 |
35 | @Test
36 | public void portionInCanBeUpTo100() {
37 | new Flag(randomLong(), randomString(), 100, randomBoolean());
38 | }
39 |
40 | @Test
41 | public void nameMustHaveValue() {
42 | Function constructor = name -> new Flag(randomLong(), name, 1, randomBoolean());
43 | final String field = "name";
44 |
45 | assertIllegalArguments(constructor, field, BLANK_VALUES);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/shopfront/src/test/java/uk/co/danielbryant/shopping/shopfront/services/dto/SmartThrottleFlagTest.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.shopfront.services.dto;
2 |
3 | import org.junit.Test;
4 |
5 | import java.time.LocalDate;
6 |
7 | import static java.time.LocalDate.now;
8 | import static java.time.temporal.ChronoUnit.DAYS;
9 | import static java.time.temporal.ChronoUnit.YEARS;
10 | import static org.hamcrest.Matchers.is;
11 | import static org.junit.Assert.assertThat;
12 |
13 | public class SmartThrottleFlagTest {
14 | private static final LocalDate tomorrow = now().plus(1, DAYS);
15 | private static final LocalDate today = now();
16 | private static final LocalDate yesterday = now().minus(1, DAYS);
17 | private static final LocalDate lastYear = now().minus(1, YEARS);
18 |
19 | @Test
20 | public void flagIsZeroBeforeStartDate() {
21 | SmartThrottleFlag smartThrottleFlag = new SmartThrottleFlag(5, tomorrow, 10);
22 | assertThat(smartThrottleFlag.getPortionIn(), is(0));
23 | }
24 |
25 | @Test
26 | public void flagIsInitialPortionAtStartDate() {
27 | SmartThrottleFlag smartThrottleFlag = new SmartThrottleFlag(5, today, 10);
28 | assertThat(smartThrottleFlag.getPortionIn(), is(5));
29 | }
30 |
31 | @Test
32 | public void flagIsInitialPlusOneIncrementOnDayAfterStartDate() {
33 | SmartThrottleFlag smartThrottleFlag = new SmartThrottleFlag(5, yesterday, 10);
34 | assertThat(smartThrottleFlag.getPortionIn(), is(15));
35 | }
36 |
37 | @Test
38 | public void flagMaxesOutAt100AfterEnoughDaysHavePassed() {
39 | SmartThrottleFlag smartThrottleFlag = new SmartThrottleFlag(5, lastYear, 10);
40 | assertThat(smartThrottleFlag.getPortionIn(), is(100));
41 | }
42 | }
--------------------------------------------------------------------------------
/shopfront/src/main/java/uk/co/danielbryant/shopping/shopfront/repo/AdaptivePricingRepo.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.shopfront.repo;
2 |
3 | import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
4 | import org.slf4j.Logger;
5 | import org.slf4j.LoggerFactory;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.beans.factory.annotation.Qualifier;
8 | import org.springframework.beans.factory.annotation.Value;
9 | import org.springframework.stereotype.Component;
10 | import org.springframework.web.client.RestTemplate;
11 | import uk.co.danielbryant.shopping.shopfront.services.dto.PriceDTO;
12 |
13 | import java.math.BigDecimal;
14 | import java.util.Optional;
15 |
16 | import static uk.co.danielbryant.shopping.shopfront.model.Constants.ADAPTIVE_PRICING_COMMAND_KEY;
17 |
18 | @Component
19 | public class AdaptivePricingRepo {
20 |
21 | private static final Logger LOGGER = LoggerFactory.getLogger(AdaptivePricingRepo.class);
22 |
23 | @Value("${adaptivePricingUri}")
24 | private String adaptivePricingUri;
25 |
26 | @Autowired
27 | @Qualifier(value = "stdRestTemplate")
28 | private RestTemplate restTemplate;
29 |
30 | @HystrixCommand(commandKey = ADAPTIVE_PRICING_COMMAND_KEY,
31 | fallbackMethod = "getPriceFallback")
32 | public Optional getPriceFor(String productName) {
33 | return Optional.of(restTemplate.getForObject(adaptivePricingUri + "/price?productName=" + productName, PriceDTO.class).getPrice());
34 | }
35 |
36 | public Optional getPriceFallback(String productName) {
37 | LOGGER.info("FALLBACK used when contacting Adaptive Pricing Service for {}", productName);
38 | return Optional.empty();
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/stockmanager/src/test/resources/contracts/stockmanager/getAllStock-v2.groovy:
--------------------------------------------------------------------------------
1 | package contracts.stockmanager
2 |
3 | import org.springframework.cloud.contract.spec.Contract
4 |
5 | Contract.make {
6 | request {
7 | headers {
8 | accept("application/vnd.stock.v2+json")
9 | }
10 | method 'GET'
11 | url '/stocks'
12 | }
13 | response {
14 | status 200
15 | body(
16 | [
17 | [
18 | productId : "1",
19 | sku : "sku-1",
20 | amountAvailable: [
21 | total : 10,
22 | perPurchase: 5
23 | ]
24 | ],
25 | [
26 | productId : "2",
27 | sku : "sku-2",
28 | amountAvailable: [
29 | total : 20,
30 | perPurchase: 10
31 | ]
32 | ],
33 | [
34 | productId : "3",
35 | sku : "sku-3",
36 | amountAvailable: [
37 | total : 30,
38 | perPurchase: 15
39 | ]
40 | ]
41 | ]
42 | )
43 | headers {
44 | contentType('application/vnd.stock.v2+json')
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/stockmanager/src/test/java/uk/co/danielbryant/shopping/stockmanager/model/v1/StockTest.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.stockmanager.model.v1;
2 |
3 | import org.junit.Rule;
4 | import org.junit.Test;
5 | import org.junit.rules.ExpectedException;
6 |
7 | import java.util.function.Function;
8 |
9 | import static com.github.quiram.test_utils.ArgumentChecks.BLANK_VALUES;
10 | import static com.github.quiram.test_utils.ArgumentChecks.assertIllegalArguments;
11 | import static com.github.quiram.utils.Random.randomString;
12 |
13 | public class StockTest {
14 | @Rule
15 | public ExpectedException onBadInput = ExpectedException.none();
16 |
17 | @Test
18 | public void cannotHaveNegativeAmountOfStock() {
19 | onBadInput.expect(IllegalArgumentException.class);
20 | onBadInput.expectMessage("amount");
21 | onBadInput.expectMessage("negative");
22 | new Stock(randomString(), randomString(), -1);
23 | }
24 |
25 | @Test
26 | public void availableAmountCanBeZero() {
27 | new Stock(randomString(), randomString(), 0);
28 | }
29 |
30 | @Test
31 | public void availableAmountCanBePositive() {
32 | new Stock(randomString(), randomString(), 10);
33 | }
34 |
35 | @Test
36 | public void idMustHaveValue() {
37 | Function constructor = id -> new Stock(id, randomString(), 1);
38 | final String field = "productId";
39 |
40 | assertIllegalArguments(constructor, field, BLANK_VALUES);
41 | }
42 |
43 | @Test
44 | public void skuMustHaveValue() {
45 | Function constructor = sku -> new Stock(randomString(), sku, 1);
46 | final String field = "sku";
47 |
48 | assertIllegalArguments(constructor, field, BLANK_VALUES);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/stockmanager/src/test/java/uk/co/danielbryant/shopping/stockmanager/StockManagerCDCBase.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.stockmanager;
2 |
3 | import io.restassured.module.mockmvc.RestAssuredMockMvc;
4 | import org.junit.Before;
5 | import uk.co.danielbryant.shopping.stockmanager.exceptions.StockNotFoundException;
6 | import uk.co.danielbryant.shopping.stockmanager.model.v2.AmountAvailable;
7 | import uk.co.danielbryant.shopping.stockmanager.model.v2.Stock;
8 | import uk.co.danielbryant.shopping.stockmanager.resources.StockResource;
9 | import uk.co.danielbryant.shopping.stockmanager.services.StockService;
10 |
11 | import java.util.List;
12 | import java.util.stream.IntStream;
13 |
14 | import static java.util.stream.Collectors.toList;
15 | import static org.mockito.MockitoAnnotations.initMocks;
16 |
17 | public class StockManagerCDCBase {
18 |
19 | @Before
20 | public void setup() {
21 | initMocks(this);
22 | RestAssuredMockMvc.standaloneSetup(new StockResource(new FakeStockService()));
23 | }
24 |
25 | private class FakeStockService extends StockService {
26 | @Override
27 | public Stock getStock(String productId) throws StockNotFoundException {
28 | return newStock(productId, 10);
29 | }
30 |
31 | @Override
32 | public List getStocks() {
33 | return IntStream.rangeClosed(1, 100).mapToObj(this::newStock).collect(toList());
34 | }
35 |
36 | private Stock newStock(int i) {
37 | return newStock(Integer.toString(i), i * 10);
38 | }
39 |
40 | private Stock newStock(String productId, int amountAvailable) {
41 | return new Stock(productId, "sku-" + productId, new AmountAvailable(amountAvailable, amountAvailable / 2));
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/acceptance-tests/src/test/java/com/github/quiram/shopping/acceptancetests/pages/ShopfrontHomePage.java:
--------------------------------------------------------------------------------
1 | package com.github.quiram.shopping.acceptancetests.pages;
2 |
3 | import net.serenitybdd.core.pages.PageObject;
4 | import org.openqa.selenium.TimeoutException;
5 | import org.openqa.selenium.WebElement;
6 | import org.openqa.selenium.support.FindBy;
7 |
8 | import java.util.List;
9 |
10 | import static com.github.quiram.utils.Collections.map;
11 | import static java.lang.String.format;
12 | import static java.util.concurrent.TimeUnit.MINUTES;
13 | import static org.awaitility.Awaitility.await;
14 | import static org.openqa.selenium.support.ui.ExpectedConditions.titleContains;
15 |
16 | public class ShopfrontHomePage extends PageObject {
17 | private static final String SHOPFRONT_IP = System.getenv("shopfront_ip");
18 | private static final String SHOPFRONT_URL = format("http://%s:8010/", SHOPFRONT_IP);
19 |
20 | @FindBy(xpath = "//*[@id=\"product-table\"]/tbody/tr[*]/td[3]")
21 | private List productNames;
22 |
23 | @FindBy(xpath = "//*[@id=\"product-table\"]/tbody/tr[*]/td[5]")
24 | private List productPrices;
25 |
26 | public void load() {
27 | await().atMost(3, MINUTES).until(this::pageIsReady);
28 | }
29 |
30 | private boolean pageIsReady() {
31 | try {
32 | openAt(SHOPFRONT_URL);
33 | waitFor(titleContains("Java Shopfront"));
34 | return true;
35 | } catch (TimeoutException | UnsupportedOperationException e) {
36 | return false;
37 | }
38 | }
39 |
40 |
41 | public List getProductNames() {
42 | return map(productNames, WebElement::getText);
43 | }
44 |
45 | public List getPrices() {
46 | return map(productPrices, WebElement::getText);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/stockmanager/src/test/java/uk/co/danielbryant/shopping/stockmanager/model/v2/AmountAvailableTest.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.stockmanager.model.v2;
2 |
3 | import org.junit.Rule;
4 | import org.junit.Test;
5 | import org.junit.rules.ExpectedException;
6 |
7 | import static com.github.quiram.utils.Random.randomPositiveInt;
8 |
9 | public class AmountAvailableTest {
10 | @Rule
11 | public ExpectedException onBadInput = ExpectedException.none();
12 |
13 | @Test
14 | public void cannotHaveNegativeTotalAmount() {
15 | onBadInput.expect(IllegalArgumentException.class);
16 | onBadInput.expectMessage("total");
17 | new AmountAvailable(-1, randomPositiveInt());
18 | }
19 |
20 | @Test
21 | public void cannotHaveNegativePerPurchaseAmount() {
22 | onBadInput.expect(IllegalArgumentException.class);
23 | onBadInput.expectMessage("perPurchase");
24 | new AmountAvailable(randomPositiveInt(), -1);
25 | }
26 |
27 | @Test
28 | public void perPurchaseCannotBeHigherThanTotal() {
29 | onBadInput.expect(IllegalArgumentException.class);
30 | onBadInput.expectMessage("perPurchase");
31 | onBadInput.expectMessage("total");
32 | final int total = randomPositiveInt();
33 | new AmountAvailable(total, total + 1);
34 | }
35 |
36 | @Test
37 | public void totalAmountCanBeZero() {
38 | new AmountAvailable(0, 0);
39 | }
40 |
41 | @Test
42 | public void totalAmountCanBePositive() {
43 | new AmountAvailable(1, 0);
44 | }
45 |
46 | @Test
47 | public void perPurchaseAmountCanBeZero() {
48 | new AmountAvailable(randomPositiveInt(), 0);
49 | }
50 |
51 | @Test
52 | public void perPurchaseAmountCanBePositive() {
53 | new AmountAvailable(randomPositiveInt(), 1);
54 | }
55 |
56 | }
--------------------------------------------------------------------------------
/productcatalogue/src/main/java/uk/co/danielbryant/shopping/productcatalogue/resources/v1/ProductResource.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.productcatalogue.resources.v1;
2 |
3 | import com.codahale.metrics.annotation.Timed;
4 | import com.google.inject.Inject;
5 | import uk.co.danielbryant.shopping.productcatalogue.model.v2.Product;
6 | import uk.co.danielbryant.shopping.productcatalogue.services.ProductService;
7 |
8 | import javax.ws.rs.GET;
9 | import javax.ws.rs.Path;
10 | import javax.ws.rs.PathParam;
11 | import javax.ws.rs.Produces;
12 | import javax.ws.rs.core.MediaType;
13 | import javax.ws.rs.core.Response;
14 | import java.util.Optional;
15 |
16 | import static com.github.quiram.utils.Collections.map;
17 |
18 | @Path("/products")
19 | @Produces(MediaType.APPLICATION_JSON)
20 | public class ProductResource {
21 |
22 | private ProductService productService;
23 |
24 | @Inject
25 | public ProductResource(ProductService productService) {
26 | this.productService = productService;
27 | }
28 |
29 | @GET
30 | @Timed
31 | public Response getAllProducts() {
32 | return Response.status(200)
33 | .entity(map(productService.getAllProducts(), Product::asV1Product))
34 | .build();
35 | }
36 |
37 | @GET
38 | @Timed
39 | @Path("{id}")
40 | public Response getProduct(@PathParam("id") String id) {
41 | Optional result = productService.getProduct(id).map(Product::asV1Product);
42 |
43 | if (result.isPresent()) {
44 | return Response.status(Response.Status.OK)
45 | .entity(result.get())
46 | .build();
47 | } else {
48 | return Response.status(Response.Status.NOT_FOUND)
49 | .build();
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/non-spring-boot-master-pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | com.github.quiram.shopping
8 | non-spring-boot-master-pom
9 | 0.0.1-SNAPSHOT
10 | pom
11 |
12 |
13 | UTF-8
14 | UTF-8
15 |
16 | 3.7.0
17 | 1.8
18 | 1.8
19 | 2.21.0
20 | 1.6
21 | 1.0.3
22 |
23 | 1.3.1
24 | 4.1.0
25 | 4.12
26 | v5.0.1
27 | v4.0.0
28 | 2.6.6
29 | 3.0.1
30 | 3.1.0
31 |
32 |
33 |
34 |
35 | jitpack.io
36 | https://jitpack.io
37 |
38 |
39 |
--------------------------------------------------------------------------------
/jenkins-aws-ecs/jobs/deploy/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | false
5 |
6 |
7 |
8 |
9 | project_name
10 |
11 | none
12 | true
13 |
14 |
15 |
16 |
17 |
18 | 2
19 |
20 |
21 | https://github.com/quiram/oreilly-cd-in-java
22 |
23 |
24 |
25 |
26 | */master
27 |
28 |
29 | false
30 |
31 |
32 | true
33 | false
34 | false
35 | false
36 |
37 | false
38 |
39 |
40 | jenkins-aws-ecs/deploy-to-aws-ecs.sh
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/productcatalogue/src/main/java/uk/co/danielbryant/shopping/productcatalogue/model/v2/Price.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.productcatalogue.model.v2;
2 |
3 | import java.math.BigDecimal;
4 |
5 | import static com.github.quiram.utils.ArgumentChecks.ensure;
6 | import static com.github.quiram.utils.ArgumentChecks.ensureNotNull;
7 |
8 | public class Price {
9 | private UnitPrice single;
10 | private BulkPrice bulkPrice;
11 |
12 | public Price() {
13 |
14 | }
15 |
16 | public Price(UnitPrice single, BulkPrice bulkPrice) {
17 | ensureNotNull(single, "single price");
18 | ensure(() -> bulkPrice == null || bulkPriceLowerThanSinglePrice(bulkPrice, single),
19 | "bulk price must be lower than single price or not be there");
20 |
21 | this.single = single;
22 | this.bulkPrice = bulkPrice;
23 | }
24 |
25 | private static boolean bulkPriceLowerThanSinglePrice(BulkPrice bulkPrice, UnitPrice singlePrice) {
26 | return bulkPrice.getUnit().compareTo(singlePrice) < 0;
27 | }
28 |
29 | public static Price singlePrice(int singlePrice) {
30 | return new Price(new UnitPrice(singlePrice), null);
31 | }
32 |
33 | public static Price singlePrice(String singlePrice) {
34 | return new Price(new UnitPrice(new BigDecimal(singlePrice)), null);
35 | }
36 |
37 | public static Price complexPrice(String singlePrice, String bulkPrice, int bulkMinAmount) {
38 | return new Price(new UnitPrice(new BigDecimal(singlePrice)), new BulkPrice(new UnitPrice(new BigDecimal(bulkPrice)), bulkMinAmount));
39 | }
40 |
41 | public UnitPrice getSingle() {
42 | return single;
43 | }
44 |
45 | public void setSingle(UnitPrice single) {
46 | this.single = single;
47 | }
48 |
49 | public BulkPrice getBulkPrice() {
50 | return bulkPrice;
51 | }
52 |
53 | public void setBulkPrice(BulkPrice bulkPrice) {
54 | this.bulkPrice = bulkPrice;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/productcatalogue/src/test/java/uk/co/danielbryant/shopping/productcatalogue/model/v1/ProductTest.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.productcatalogue.model.v1;
2 |
3 | import org.junit.Rule;
4 | import org.junit.Test;
5 | import org.junit.rules.ExpectedException;
6 |
7 | import java.math.BigDecimal;
8 |
9 | import static com.amarinperez.test_utils.ArgumentChecks.BLANK_VALUES;
10 | import static com.amarinperez.test_utils.ArgumentChecks.assertIllegalArguments;
11 | import static com.github.quiram.utils.Random.randomString;
12 |
13 | public class ProductTest {
14 | @Rule
15 | public ExpectedException onBadData = ExpectedException.none();
16 |
17 | @Test
18 | public void productCannotBeFree() {
19 | onBadData.expect(IllegalArgumentException.class);
20 | onBadData.expectMessage("price");
21 | new Product(randomString(), randomString(), randomString(), new BigDecimal(0));
22 | }
23 |
24 | @Test
25 | public void priceCannotBeNegative() {
26 | onBadData.expect(IllegalArgumentException.class);
27 | onBadData.expectMessage("price");
28 | new Product(randomString(), randomString(), randomString(), new BigDecimal(-1));
29 | }
30 |
31 | @Test
32 | public void priceCannotBeNull() {
33 | onBadData.expect(IllegalArgumentException.class);
34 | onBadData.expectMessage("price");
35 | new Product(randomString(), randomString(), randomString(), null);
36 | }
37 |
38 | @Test
39 | public void idMustBePresent() {
40 | assertIllegalArguments(id -> new Product(id, randomString(), randomString(), new BigDecimal(10)), "id", BLANK_VALUES);
41 | }
42 |
43 | @Test
44 | public void nameMustBePresent() {
45 | assertIllegalArguments(name -> new Product(randomString(), name, randomString(), new BigDecimal(10)), "name", BLANK_VALUES);
46 | }
47 |
48 | @Test
49 | public void descriptionMustBePresent() {
50 | assertIllegalArguments(description -> new Product(randomString(), randomString(), description, new BigDecimal(10)), "description", BLANK_VALUES);
51 | }
52 | }
--------------------------------------------------------------------------------
/jenkins-aws-ecs/deploy-to-aws-ecs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo "Deploying service '${project_name}'"
4 |
5 | cp jenkins-aws-ecs/task-definitions/${project_name}-task.json taskdef.json
6 |
7 | # The below has been adapted from https://docs.aws.amazon.com/AWSGettingStartedContinuousDeliveryPipeline/latest/GettingStarted/CICD_Jenkins_Pipeline.html
8 |
9 | #Load Constants
10 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
11 |
12 | pushd ${SCRIPT_DIR}
13 | source ./setup/constants.sh
14 | popd
15 |
16 | FAMILY=`sed -n 's/.*"family": "\(.*\)",/\1/p' taskdef.json`
17 | SERVICE_NAME=${project_name}-service
18 |
19 | #Rename the task definition file to reflect version
20 | cp taskdef.json ${project_name}-v_${BUILD_NUMBER}.json
21 | #Register the task definition in the repository
22 | aws ecs register-task-definition --family ${FAMILY} --cli-input-json file://${WORKSPACE}/${project_name}-v_${BUILD_NUMBER}.json --region ${REGION}
23 | MISSING_SERVICES=`aws ecs describe-services --services ${SERVICE_NAME} --cluster ${CLUSTER_NAME} --region ${REGION} | jq .failures[]`
24 | INACTIVE_SERVICES=`aws ecs describe-services --services ${SERVICE_NAME} --cluster ${CLUSTER_NAME} --region ${REGION} | jq .services[].status | grep INACTIVE`
25 | #Get latest revision
26 | REVISION=`aws ecs describe-task-definition --task-definition ${project_name} --region ${REGION} | jq .taskDefinition.revision`
27 |
28 | #Create or update service
29 | if [ "$MISSING_SERVICES" == "" -a "${INACTIVE_SERVICES}" == "" ]; then
30 | echo "entered existing service"
31 | DESIRED_COUNT=`aws ecs describe-services --services ${SERVICE_NAME} --cluster ${CLUSTER_NAME} --region ${REGION} | jq .services[].desiredCount`
32 | if [ "${DESIRED_COUNT}" == "0" ]; then
33 | DESIRED_COUNT="1"
34 | fi
35 | aws ecs update-service --cluster ${CLUSTER_NAME} --region ${REGION} --service ${SERVICE_NAME} --task-definition ${FAMILY}:${REVISION} --desired-count ${DESIRED_COUNT}
36 | else
37 | echo "entered new service"
38 | aws ecs create-service --service-name ${SERVICE_NAME} --desired-count 1 --task-definition ${FAMILY} --cluster ${CLUSTER_NAME} --region ${REGION}
39 | fi
40 |
41 |
--------------------------------------------------------------------------------
/stockmanager/src/test/java/uk/co/danielbryant/shopping/stockmanager/StockManagerApplicationCT.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.stockmanager;
2 |
3 | import io.restassured.specification.RequestSpecification;
4 | import org.junit.Test;
5 |
6 | import static io.restassured.RestAssured.given;
7 | import static io.restassured.http.ContentType.JSON;
8 | import static org.hamcrest.core.Is.is;
9 | import static org.springframework.http.HttpStatus.NOT_FOUND;
10 |
11 | public class StockManagerApplicationCT {
12 |
13 | private static final String STOCK_V2_CONTENT_TYPE = "application/vnd.stock.v2+json";
14 | private final String baseUrl = "http://localhost:8030";
15 |
16 | @Test
17 | public void listOfStocksProvidesFiveElements() {
18 | when().get(baseUrl + "/stocks")
19 | .then().assertThat()
20 | .body("size()", is(5));
21 | }
22 |
23 | @Test
24 | public void listOfStocksProvidesFiveElementsInV2() {
25 | when(STOCK_V2_CONTENT_TYPE).get(baseUrl + "/stocks")
26 | .then().assertThat()
27 | .body("size()", is(5), "[0].amountAvailable.total", is(5));
28 | }
29 |
30 | @Test
31 | public void canGetSpecificStock() {
32 | when().get(baseUrl + "/stocks/1")
33 | .then().assertThat()
34 | .body("sku", is("12345678"), "amountAvailable", is(5));
35 | }
36 |
37 | @Test
38 | public void canGetSpecificStockInVersion2() {
39 | when(STOCK_V2_CONTENT_TYPE).get(baseUrl + "/stocks/1")
40 | .then().assertThat()
41 | .body("sku", is("12345678"),
42 | "amountAvailable.total", is(5),
43 | "amountAvailable.perPurchase", is(2));
44 | }
45 |
46 | @Test
47 | public void canHandleNotFoundStock() {
48 | when().get(baseUrl + "/stocks/99999999")
49 | .then().assertThat()
50 | .statusCode(is(NOT_FOUND.value()));
51 | }
52 |
53 | private RequestSpecification when() {
54 | return when(JSON.toString());
55 | }
56 |
57 | private RequestSpecification when(String acceptType) {
58 | return given().accept(acceptType).when();
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/shopfront/src/main/java/uk/co/danielbryant/shopping/shopfront/repo/FeatureFlagsRepo.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.shopfront.repo;
2 |
3 | import org.slf4j.Logger;
4 | import org.springframework.beans.factory.annotation.Autowired;
5 | import org.springframework.beans.factory.annotation.Qualifier;
6 | import org.springframework.beans.factory.annotation.Value;
7 | import org.springframework.http.converter.HttpMessageNotReadableException;
8 | import org.springframework.stereotype.Component;
9 | import org.springframework.web.client.HttpClientErrorException;
10 | import org.springframework.web.client.HttpServerErrorException;
11 | import org.springframework.web.client.ResourceAccessException;
12 | import org.springframework.web.client.RestTemplate;
13 | import uk.co.danielbryant.shopping.shopfront.services.dto.FlagDTO;
14 |
15 | import java.util.Optional;
16 |
17 | import static java.lang.String.format;
18 | import static org.slf4j.LoggerFactory.getLogger;
19 |
20 | @Component
21 | public class FeatureFlagsRepo {
22 | private static final Logger LOGGER = getLogger(FeatureFlagsRepo.class);
23 |
24 | @Value("${featureFlagsUri}")
25 | private String featureFlagsUri;
26 |
27 | @Autowired
28 | @Qualifier(value = "stdRestTemplate")
29 | private RestTemplate restTemplate;
30 |
31 | public FeatureFlagsRepo() {
32 | // Needed by Spring
33 | }
34 |
35 | FeatureFlagsRepo(String featureFlagsUri, RestTemplate restTemplate) {
36 | this.featureFlagsUri = featureFlagsUri;
37 | this.restTemplate = restTemplate;
38 | }
39 |
40 | public Optional getFlag(long flagId) {
41 | try {
42 | final String flagUrl = featureFlagsUri + "/flags/" + flagId;
43 | LOGGER.info("Fetching flag from {}", flagUrl);
44 | final FlagDTO flag = restTemplate.getForObject(flagUrl, FlagDTO.class);
45 | return Optional.ofNullable(flag);
46 | } catch (HttpClientErrorException | HttpServerErrorException |
47 | ResourceAccessException | HttpMessageNotReadableException e) {
48 | final String msg = "Failed to retrieve flag %s; falling back to no flag";
49 | LOGGER.info(format(msg, flagId), e);
50 | return Optional.empty();
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/jenkins-aws-ecs/setup/delete-env.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
4 |
5 | pushd ${SCRIPT_DIR}
6 | source ./constants.sh
7 |
8 | # delete EC2 instances
9 | echo "Getting current instances..."
10 | run_aws ec2 describe-instances --filter "'Name=tag:${TAG_KEY},Values=${TAG_VALUE}'"
11 | instances=`echo ${AWS_LAST_RESULT} | jq .Reservations[].Instances[].InstanceId`
12 |
13 | echo "Deleting current instances..."
14 | run_aws ec2 terminate-instances --instance-ids ${instances}
15 |
16 | count_non_terminated_instances
17 | while [ ${number_of_non_terminated_instances} -ne 0 ]; do
18 | echo "Waiting for ${number_of_non_terminated_instances} instance(s) to be completely terminated..."
19 | count_non_terminated_instances
20 | sleep 5
21 | done
22 |
23 | # Delete IAM roles
24 | # Removing role from instance profile first
25 | run_aws iam remove-role-from-instance-profile \
26 | --instance-profile-name ${ECS_INSTANCE_ROLE} \
27 | --role-name ${ECS_INSTANCE_ROLE}
28 |
29 | delete_iam_role ${ECS_INSTANCE_ROLE} ${EC2_FOR_ECS_POLICY_ARN}
30 | #delete_iam_role ${ECS_SERVICE_ROLE} ${EC2_CONTAINER_SERVICE_ARN}
31 |
32 | echo "Removing security group ${SECURITY_GROUP_NAME}"
33 | run_aws ec2 delete-security-group --group-name ${SECURITY_GROUP_NAME}
34 |
35 | echo "Removing key ${KEY_PAIR_NAME} associated private key ${PRIVATE_KEY_FILE}"
36 | [ -f ${PRIVATE_KEY_FILE} ] && rm -f ${PRIVATE_KEY_FILE}
37 | run_aws ec2 delete-key-pair --key-name ${KEY_PAIR_NAME}
38 |
39 | echo "Removing services from cluster"
40 | # Get services names
41 | run_aws ecs list-services --cluster ${CLUSTER_NAME}
42 | services=`echo ${AWS_LAST_RESULT} | jq .serviceArns[] | cut -d\" -f2 | cut -d\/ -f2`
43 | for service in ${services}; do
44 | run_aws ecs delete-service --cluster ${CLUSTER_NAME} --region ${REGION} --service ${service}
45 | done
46 |
47 | echo "De-registering task definitions from cluster"
48 | run_aws ecs list-task-definitions
49 | task_defs=`echo ${AWS_LAST_RESULT} | jq .taskDefinitionArns[] | cut -d\" -f2 | cut -d\/ -f2`
50 |
51 | for task_def in ${task_defs}; do
52 | run_aws ecs deregister-task-definition --task-definition ${task_def}
53 | done
54 |
55 | echo "Removing cluster ${CLUSTER_NAME}"
56 | run_aws ecs delete-cluster --cluster ${CLUSTER_NAME}
57 |
58 |
59 | popd
--------------------------------------------------------------------------------
/external-adaptive-pricing/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | external-adaptive-pricing
7 | 0.0.1-SNAPSHOT
8 | jar
9 |
10 | external-adaptive-pricing
11 | Service that represents an external, unowned Adaptive Pricing Service
12 |
13 |
14 | com.github.quiram.shopping
15 | spring-boot-master-pom
16 | 0.0.1-SNAPSHOT
17 | ../spring-boot-master-pom.xml
18 |
19 |
20 |
21 |
22 | org.springframework.boot
23 | spring-boot-starter-web
24 |
25 |
26 | org.springframework.boot
27 | spring-boot-starter-actuator
28 |
29 |
30 | com.github.quiram
31 | java-utils
32 | ${java-utils.version}
33 |
34 |
35 |
36 | org.springframework.boot
37 | spring-boot-starter-test
38 | test
39 |
40 |
41 | junit
42 | junit
43 | test
44 |
45 |
46 | org.hamcrest
47 | hamcrest-core
48 | test
49 |
50 |
51 | com.github.quiram
52 | java-test-utils
53 | ${java-test-utils.version}
54 | test
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/acceptance-tests/src/test/java/com/github/quiram/shopping/acceptancetests/ShoppingAT.java:
--------------------------------------------------------------------------------
1 | package com.github.quiram.shopping.acceptancetests;
2 |
3 | import com.github.quiram.shopping.acceptancetests.steps.FeatureFlagsSteps;
4 | import com.github.quiram.shopping.acceptancetests.steps.ShopfrontSteps;
5 | import net.serenitybdd.junit.runners.SerenityRunner;
6 | import net.thucydides.core.annotations.Managed;
7 | import net.thucydides.core.annotations.Steps;
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 | import org.openqa.selenium.WebDriver;
11 |
12 | @RunWith(SerenityRunner.class)
13 | public class ShoppingAT {
14 |
15 | @Managed(driver = "htmlunit")
16 | WebDriver driver;
17 |
18 | @Steps
19 | private ShopfrontSteps shopfrontSteps;
20 |
21 | @Steps
22 | private FeatureFlagsSteps featureFlagsSteps;
23 |
24 |
25 | @Test
26 | public void numberOfProductsAsExpected() {
27 | // GIVEN
28 | shopfrontSteps.shopfront_service_is_ready();
29 |
30 | // WHEN
31 | shopfrontSteps.user_obtains_the_list_of_products();
32 |
33 | // THEN
34 | shopfrontSteps.product_list_has_size(5);
35 |
36 | // AND
37 | shopfrontSteps.includes_product_name("Widget");
38 | }
39 |
40 | @Test
41 | public void disableAdaptivePricingFeature() {
42 | // GIVEN
43 | featureFlagsSteps.feature_flags_service_is_ready();
44 |
45 | // AND
46 | shopfrontSteps.shopfront_service_is_ready();
47 |
48 | // WHEN
49 | featureFlagsSteps.admin_sets_the_adaptive_pricing_feature_flag_to(0);
50 |
51 | // AND
52 | shopfrontSteps.check_all_prices();
53 |
54 | // AND
55 | shopfrontSteps.check_all_prices_again();
56 |
57 | // THEN
58 | shopfrontSteps.prices_have_not_changed();
59 | }
60 |
61 | @Test
62 | public void enableAdaptivePricingFeature() {
63 | // GIVEN
64 | featureFlagsSteps.feature_flags_service_is_ready();
65 |
66 | // AND
67 | shopfrontSteps.shopfront_service_is_ready();
68 |
69 | // WHEN
70 | featureFlagsSteps.admin_sets_the_adaptive_pricing_feature_flag_to(100);
71 |
72 | // AND
73 | shopfrontSteps.check_all_prices();
74 |
75 | // AND
76 | shopfrontSteps.check_all_prices_again();
77 |
78 | // THEN
79 | shopfrontSteps.all_prices_have_changed();
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/stockmanager/src/main/java/uk/co/danielbryant/shopping/stockmanager/resources/StockResource.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.stockmanager.resources;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import org.springframework.http.HttpStatus;
7 | import org.springframework.web.bind.annotation.*;
8 | import uk.co.danielbryant.shopping.stockmanager.exceptions.StockNotFoundException;
9 | import uk.co.danielbryant.shopping.stockmanager.model.v2.Stock;
10 | import uk.co.danielbryant.shopping.stockmanager.services.StockService;
11 |
12 | import java.util.List;
13 |
14 | import static com.github.quiram.utils.Collections.map;
15 |
16 | @RestController
17 | @RequestMapping("/stocks")
18 | public class StockResource {
19 |
20 | private static final Logger LOGGER = LoggerFactory.getLogger(StockResource.class);
21 | private static final String STOCK_V2_CONTENT_TYPE = "application/vnd.stock.v2+json";
22 |
23 | @Autowired
24 | private StockService stockService;
25 |
26 | public StockResource() {
27 | }
28 |
29 | public StockResource(StockService stockService) {
30 | this.stockService = stockService;
31 | }
32 |
33 | @RequestMapping()
34 | public List getStocksV1() {
35 | LOGGER.info("getStocks (v1, All stocks)");
36 | return map(stockService.getStocks(), Stock::asV1Stock);
37 | }
38 |
39 | @RequestMapping(produces = STOCK_V2_CONTENT_TYPE)
40 | public List getStocksV2() {
41 | LOGGER.info("getStocks (v2, All stocks)");
42 | return stockService.getStocks();
43 | }
44 |
45 | @RequestMapping("{productId}")
46 | public uk.co.danielbryant.shopping.stockmanager.model.v1.Stock getStockV1(@PathVariable("productId") String productId) throws StockNotFoundException {
47 | LOGGER.info("getStock (v1) with productId: {}", productId);
48 | return stockService.getStock(productId).asV1Stock();
49 | }
50 |
51 | @RequestMapping(path = "{productId}", produces = STOCK_V2_CONTENT_TYPE)
52 | public Stock getStockV2(@PathVariable("productId") String productId) throws StockNotFoundException {
53 | LOGGER.info("getStock (v2) with productId: {}", productId);
54 | return stockService.getStock(productId);
55 | }
56 |
57 | @ExceptionHandler
58 | @ResponseStatus(HttpStatus.NOT_FOUND)
59 | public void handleStockNotFound(StockNotFoundException snfe) {
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/shopfront/src/main/java/uk/co/danielbryant/shopping/shopfront/repo/StockRepo.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.shopfront.repo;
2 |
3 | import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
4 | import org.slf4j.Logger;
5 | import org.slf4j.LoggerFactory;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.beans.factory.annotation.Qualifier;
8 | import org.springframework.beans.factory.annotation.Value;
9 | import org.springframework.core.ParameterizedTypeReference;
10 | import org.springframework.http.HttpMethod;
11 | import org.springframework.http.ResponseEntity;
12 | import org.springframework.stereotype.Component;
13 | import org.springframework.web.client.RestTemplate;
14 | import uk.co.danielbryant.shopping.shopfront.services.dto.StockDTO;
15 |
16 | import java.util.List;
17 | import java.util.Map;
18 |
19 | import static com.github.quiram.utils.Collections.toMap;
20 | import static java.util.Collections.emptyMap;
21 |
22 | @Component
23 | public class StockRepo {
24 |
25 | private static final Logger LOGGER = LoggerFactory.getLogger(StockRepo.class);
26 |
27 | @Value("${stockManagerUri}")
28 | private String stockManagerUri;
29 |
30 | @Autowired
31 | @Qualifier(value = "stdRestTemplate")
32 | private RestTemplate restTemplate;
33 |
34 | public StockRepo() {
35 | // Needed by Spring
36 | }
37 |
38 | public StockRepo(String stockManagerUri, RestTemplate restTemplate) {
39 | this.stockManagerUri = stockManagerUri;
40 | this.restTemplate = restTemplate;
41 | }
42 |
43 | @HystrixCommand(fallbackMethod = "stocksNotFound") // Hystrix circuit breaker for fault-tolerance demo
44 | public Map getStockDTOs() {
45 | LOGGER.info("getStocksDTOs");
46 | ResponseEntity> stockManagerResponse =
47 | restTemplate.exchange(stockManagerUri + "/stocks",
48 | HttpMethod.GET, null, new ParameterizedTypeReference>() {
49 | });
50 | List stockDTOs = stockManagerResponse.getBody();
51 |
52 | return toMap(stockDTOs, StockDTO::getProductId);
53 | }
54 |
55 | public StockDTO getStockDTO(String id) {
56 | return restTemplate.getForObject(stockManagerUri + "/stocks/" + id, StockDTO.class);
57 | }
58 |
59 | public Map stocksNotFound() {
60 | LOGGER.info("stocksNotFound *** FALLBACK ***");
61 | return emptyMap();
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/stockmanager/src/test/java/uk/co/danielbryant/shopping/stockmanager/model/v2/StockTest.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.stockmanager.model.v2;
2 |
3 | import org.junit.Rule;
4 | import org.junit.Test;
5 | import org.junit.rules.ExpectedException;
6 |
7 | import java.util.function.Function;
8 |
9 | import static com.github.quiram.test_utils.ArgumentChecks.BLANK_VALUES;
10 | import static com.github.quiram.test_utils.ArgumentChecks.assertIllegalArguments;
11 | import static com.github.quiram.utils.Random.randomInt;
12 | import static com.github.quiram.utils.Random.randomPositiveInt;
13 | import static com.github.quiram.utils.Random.randomString;
14 | import static java.lang.Math.abs;
15 | import static org.hamcrest.Matchers.is;
16 | import static org.junit.Assert.assertThat;
17 |
18 | public class StockTest {
19 | @Rule
20 | public ExpectedException onBadInput = ExpectedException.none();
21 |
22 | @Test
23 | public void mustHaveAmount() {
24 | onBadInput.expect(IllegalArgumentException.class);
25 | onBadInput.expectMessage("amount");
26 | onBadInput.expectMessage("null");
27 | new Stock(randomString(), randomString(), null);
28 | }
29 |
30 | @Test
31 | public void idMustHaveValue() {
32 | Function constructor = id -> new Stock(id, randomString(), randomAmount());
33 | final String field = "productId";
34 |
35 | assertIllegalArguments(constructor, field, BLANK_VALUES);
36 | }
37 |
38 | @Test
39 | public void skuMustHaveValue() {
40 | Function constructor = sku -> new Stock(randomString(), sku, randomAmount());
41 | final String field = "sku";
42 |
43 | assertIllegalArguments(constructor, field, BLANK_VALUES);
44 | }
45 |
46 | @Test
47 | public void canTransformToV1() {
48 | final String productId = randomString();
49 | final String sku = randomString();
50 | final AmountAvailable amountAvailable = randomAmount();
51 | final Stock v2Stock = new Stock(productId, sku, amountAvailable);
52 | final uk.co.danielbryant.shopping.stockmanager.model.v1.Stock v1Stock = v2Stock.asV1Stock();
53 | assertThat(v1Stock.getProductId(), is(productId));
54 | assertThat(v1Stock.getSku(), is(sku));
55 | assertThat(v1Stock.getAmountAvailable(), is(amountAvailable.getTotal()));
56 | }
57 |
58 | private static AmountAvailable randomAmount() {
59 | final int total = randomPositiveInt();
60 | return new AmountAvailable(total, total - 1);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/featureflags/src/main/java/com/github/quiram/shopping/featureflags/services/FlagService.java:
--------------------------------------------------------------------------------
1 | package com.github.quiram.shopping.featureflags.services;
2 |
3 | import com.github.quiram.shopping.featureflags.exceptions.FlagCreatedWithIdException;
4 | import com.github.quiram.shopping.featureflags.exceptions.FlagNameAlreadyExistsException;
5 | import com.github.quiram.shopping.featureflags.exceptions.FlagNotFoundException;
6 | import com.github.quiram.shopping.featureflags.exceptions.FlagWithoutIdException;
7 | import com.github.quiram.shopping.featureflags.model.Flag;
8 | import com.github.quiram.shopping.featureflags.repositories.FlagRepository;
9 | import org.springframework.beans.factory.annotation.Autowired;
10 | import org.springframework.stereotype.Service;
11 |
12 | import java.util.List;
13 | import java.util.Optional;
14 |
15 | import static java.util.stream.Collectors.toList;
16 | import static java.util.stream.StreamSupport.stream;
17 |
18 | @Service
19 | public class FlagService {
20 |
21 | private FlagRepository flagRepository;
22 |
23 | @Autowired
24 | FlagService(FlagRepository flagRepository) {
25 | this.flagRepository = flagRepository;
26 | }
27 |
28 | public List getFlags() {
29 | return stream(flagRepository.findAll().spliterator(), false).collect(toList());
30 | }
31 |
32 | public Flag getFlag(Long id) throws FlagNotFoundException {
33 | return Optional.ofNullable(flagRepository.findOne(id))
34 | .orElseThrow(() -> new FlagNotFoundException("Flag not found with id: " + id));
35 | }
36 |
37 | public Flag addFlag(Flag flag) throws FlagCreatedWithIdException, FlagNameAlreadyExistsException {
38 | if (flag.getFlagId() != null) {
39 | throw new FlagCreatedWithIdException("flag includes the id " + flag.getFlagId());
40 | }
41 |
42 | if (flagRepository.findByName(flag.getName()) != null) {
43 | throw new FlagNameAlreadyExistsException("there is already a flag with name " + flag.getName());
44 | }
45 |
46 | return flagRepository.save(flag);
47 | }
48 |
49 | public void removeFlag(Long id) throws FlagNotFoundException {
50 | getFlag(id);
51 | flagRepository.delete(id);
52 | }
53 |
54 | public void updateFlag(Flag flag) throws FlagNotFoundException, FlagWithoutIdException {
55 | if (flag.getFlagId() == null) {
56 | throw new FlagWithoutIdException(flag.getName());
57 | }
58 |
59 | getFlag(flag.getFlagId());
60 | flagRepository.save(flag);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/jenkins-base/jobs/test-featureflags-db/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | false
6 |
7 |
8 | 2
9 |
10 |
11 | https://github.com/quiram/oreilly-cd-in-java
12 |
13 |
14 |
15 |
16 | */master
17 |
18 |
19 | false
20 |
21 |
22 |
23 | test-featureflags-db/.*
24 |
25 |
26 |
27 |
28 | true
29 | false
30 | false
31 | false
32 |
33 |
34 | * * * * *
35 | false
36 |
37 |
38 | false
39 |
40 |
41 |
42 |
43 | DockerHub
44 |
45 | quiram/test-featureflags-db
46 | false
47 | true
48 | test-featureflags-db
49 | false
50 | false
51 |
52 | false
53 | true
54 | false
55 |
56 | false
57 | Default
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/jenkins-base/jobs/fake-adaptive-pricing/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | false
6 |
7 |
8 | 2
9 |
10 |
11 | https://github.com/quiram/oreilly-cd-in-java
12 |
13 |
14 |
15 |
16 | */master
17 |
18 |
19 | false
20 |
21 |
22 |
23 | fake-adaptive-pricing/.*
24 |
25 |
26 |
27 |
28 | true
29 | false
30 | false
31 | false
32 |
33 |
34 | * * * * *
35 | false
36 |
37 |
38 | false
39 |
40 |
41 |
42 |
43 | DockerHub
44 |
45 | quiram/fake-adaptive-pricing
46 | false
47 | true
48 | fake-adaptive-pricing
49 | false
50 | false
51 |
52 | false
53 | true
54 | false
55 |
56 | false
57 | Default
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/jenkins-base/README.md:
--------------------------------------------------------------------------------
1 | # Jenkins - Base
2 | A sample Jenkins server with pre-created jobs that builds everything, but doesn't deploy (there is a placeholder deploy job that doesn't do anything).
3 |
4 | The placeholder deploy job will be overridden by other Jenkins docker containers that do implement different deployment mechanisms.
5 |
6 | ## Procedure
7 | 1. Run `build.sh` to generate the necessary config files and docker image
8 | 1. Run `docker-compose -f docker-compose.yml up` to run the generated service; the Jenkins server is available at `http://localhost:8080/`
9 | 1. Once the Jenkins image has started, you'll need to ssh into it and open up permissions of the Docker socket file (**warning:** there is a security risk on doing this, see below for details).
10 | 1. Identify the Docker ID of the image: ``docker_id=`docker ps | grep jenkins-kubernetes | cut -f1 -d\ ` ``
11 | 1. SSH into the Docker container: `docker exec -ti ${docker_id} bash`
12 | 1. Once inside, open up permissions for the Docker socket file: `sudo chmod 777 /run/docker.sock`
13 | 1. Once Jenkins is up and running, you will need to create a Credentials key with id "DockerHub" and your user and password for Docker Hub.
14 |
15 | ### Security exposure of opening up permissions in Docker socket file
16 | This CI/CD setup implies a Jenkins instance running inside a Docker container, with builds that invoke the `docker`
17 | command themselves to pack and deploy applications as Docker images. This means that we are trying to run `docker`
18 | within a Docker container. This is not trivial, and the implications are still being worked out. There are essentially two ways of achieving this:
19 | - Running Docker-in-Docker, that is, running a second Docker daemon within the Docker container itself.
20 | - Exposing the Docker daemon _in the host computer_ to the Docker containers, so running the command `docker` within the containers will communicate to the Docker daemon
21 | _in the host computer._
22 |
23 | Here we have opted for the latter, which is essentially achieved by making the Docker socket file accessible by any process... which isn't the safest of set-ups,
24 | so please don't do this in your production environment. It is ok to do it (temporarily) in your local machine to try out the examples laid out here, but once you're
25 | finished make sure to restore the permissions of the Docker socket file to its original settings; restarting the Docker server should do the trick.
26 |
27 | More on Docker-in-Docker on this [excellent post by Jérôme Petazzoni](https://jpetazzo.github.io/2015/09/03/do-not-use-docker-in-docker-for-ci/).
28 |
--------------------------------------------------------------------------------
/productcatalogue/src/test/java/uk/co/danielbryant/shopping/productcatalogue/model/v2/ProductTest.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.productcatalogue.model.v2;
2 |
3 | import org.junit.Rule;
4 | import org.junit.Test;
5 | import org.junit.rules.ExpectedException;
6 |
7 | import static com.amarinperez.test_utils.ArgumentChecks.BLANK_VALUES;
8 | import static com.amarinperez.test_utils.ArgumentChecks.assertIllegalArguments;
9 | import static com.github.quiram.utils.Random.randomDouble;
10 | import static com.github.quiram.utils.Random.randomString;
11 | import static org.hamcrest.CoreMatchers.is;
12 | import static org.junit.Assert.assertThat;
13 | import static uk.co.danielbryant.shopping.productcatalogue.model.v2.Price.complexPrice;
14 | import static uk.co.danielbryant.shopping.productcatalogue.model.v2.Price.singlePrice;
15 |
16 | public class ProductTest {
17 | @Rule
18 | public ExpectedException onBadData = ExpectedException.none();
19 |
20 | @Test
21 | public void priceMustBePresent() {
22 | onBadData.expect(IllegalArgumentException.class);
23 | onBadData.expectMessage("price");
24 | new Product(randomString(), randomString(), randomString(), null);
25 | }
26 |
27 | @Test
28 | public void idMustBePresent() {
29 | assertIllegalArguments(id -> new Product(id, randomString(), randomString(), singlePrice(10)), "id", BLANK_VALUES);
30 | }
31 |
32 | @Test
33 | public void nameMustBePresent() {
34 | assertIllegalArguments(name -> new Product(randomString(), name, randomString(), singlePrice(15)), "name", BLANK_VALUES);
35 | }
36 |
37 | @Test
38 | public void descriptionMustBePresent() {
39 | assertIllegalArguments(description -> new Product(randomString(), randomString(), description, singlePrice(20)), "description", BLANK_VALUES);
40 | }
41 |
42 | @Test
43 | public void canConvertToV1Product() {
44 | final Price price = complexPrice(Double.toString(randomDouble(100, 2) + 100),
45 | Double.toString(randomDouble(100, 2)),
46 | 5);
47 | final String id = randomString();
48 | final String name = randomString();
49 | final String description = randomString();
50 | final Product v2Product = new Product(id, name, description, price);
51 | final uk.co.danielbryant.shopping.productcatalogue.model.v1.Product v1Product = v2Product.asV1Product();
52 | assertThat(v1Product.getId(), is(id));
53 | assertThat(v1Product.getName(), is(name));
54 | assertThat(v1Product.getDescription(), is(description));
55 | assertThat(v1Product.getPrice(), is(price.getSingle().getValue()));
56 | }
57 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Extended Java Shop
2 | This repo contains code samples for the book ["Continuous Delivery in Java"](http://shop.oreilly.com/product/0636920078777.do). It started as a fork of [Daniel Bryant](https://github.com/danielbryantuk)'s [O'Reilly Docker Java Shopping repository](https://github.com/danielbryantuk/oreilly-docker-java-shopping), but it evolved to become a repository on its own right. Problems running the code? Raise a [new issue](https://github.com/continuous-delivery-in-java/extended-java-shop/issues/new).
3 |
4 | This README is intended to provide high-level guidance of the project, and detailed instructions can be found in the accompanying book.
5 |
6 | ## Project Structure
7 |
8 | Further instructions may exist within the following sub-folders.
9 |
10 | * acceptance-tests
11 | * Simple examples of functional end-to-end tests that use JUnit, [REST-assured](http://rest-assured.io/), and SerenityBDD to test the entire set of services comprising the Extended Java Shop.
12 | * external-adaptive-pricing
13 | * Service that represents some adaptive pricing service provided by a supposed third party.
14 | * fake-adaptive-pricing
15 | * The fake service that a team would use for tests, instead of the real external-adaptive-pricing service.
16 | * featureflags
17 | * The Feature Flags Service that is used within the Extended Java Shop to decide which features should be enabled and to what degree.
18 | * featureflags-db
19 | * The DB used by the Feature Flags Service. In this case it will be just a Postgre DB in a Docker container, in a real-life situation it would be a proper database.
20 | * jenkins-aws-ecs
21 | * A pre-built Jenkins instance that deploys to a known AWS ECS Cluster; this is based on jenkins-base.
22 | * jenkins-base
23 | * A pre-built Jenkins instance with job definitions for all the services and tests. It includes an empty "deploy" job which doesn't do anything; this job is overridden at `jenkins-kubernetes` and `jenkins-aws-ecs` to deploy to the right location.
24 | * jenkins-kubernetes
25 | * A pre-built Jenkins instance that deploys to a locally running Kubernetes Cluster; this is based on jenkins-base.
26 | * productcatalogue
27 | * The Product Catalogue Service, which provides product details like name and price.
28 | * shopfront
29 | * The Shopfront Service that provides the primary entry point for the end-user (both Web UI and API-driven).
30 | * stockmanager
31 | * The Stock Manager Service, which provides stock information, such as SKU and available quantity.
32 | * test-featureflags-db
33 | * The DB used by Feature Flags Service in the test environment. In this case, like featureflags-db, it's just a Postgre DB in a Docker container, although in a real-life situation it would probably be a proper database.
34 |
--------------------------------------------------------------------------------
/shopfront/src/test/java/uk/co/danielbryant/shopping/shopfront/services/FeatureFlagsServiceTest.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.shopfront.services;
2 |
3 | import org.junit.Before;
4 | import org.junit.Test;
5 | import org.junit.runner.RunWith;
6 | import org.mockito.Mock;
7 | import org.mockito.runners.MockitoJUnitRunner;
8 | import uk.co.danielbryant.shopping.shopfront.repo.FeatureFlagsRepo;
9 | import uk.co.danielbryant.shopping.shopfront.services.dto.FlagDTO;
10 |
11 | import java.util.Optional;
12 | import java.util.Random;
13 |
14 | import static com.github.quiram.utils.Random.randomLong;
15 | import static com.github.quiram.utils.Random.randomString;
16 | import static org.junit.Assert.assertFalse;
17 | import static org.junit.Assert.assertTrue;
18 | import static org.mockito.Matchers.anyLong;
19 | import static org.mockito.Mockito.when;
20 |
21 | @RunWith(MockitoJUnitRunner.class)
22 | public class FeatureFlagsServiceTest {
23 | @Mock
24 | private FeatureFlagsRepo featureFlagsRepo;
25 |
26 | @Mock
27 | private Random random;
28 | private FeatureFlagsService featureFlagsService;
29 |
30 | @Before
31 | public void setUp() {
32 | featureFlagsService = new FeatureFlagsService(featureFlagsRepo, random);
33 | }
34 |
35 | @Test
36 | public void neverApplyIfFlagNotFound() {
37 | when(featureFlagsRepo.getFlag(anyLong())).thenReturn(Optional.empty());
38 | assertFalse(featureFlagsService.shouldApplyFeatureWithFlag(randomLong()));
39 | }
40 |
41 | @Test
42 | public void neverApplyIfFlagSetToZero() {
43 | when(featureFlagsRepo.getFlag(anyLong())).thenReturn(Optional.of(new FlagDTO(randomLong(), randomString(), 0)));
44 | assertFalse(featureFlagsService.shouldApplyFeatureWithFlag(randomLong()));
45 | }
46 |
47 | @Test
48 | public void alwaysApplyIfFlagSetToHundred() {
49 | when(featureFlagsRepo.getFlag(anyLong())).thenReturn(Optional.of(new FlagDTO(randomLong(), randomString(), 100)));
50 | assertTrue(featureFlagsService.shouldApplyFeatureWithFlag(randomLong()));
51 | }
52 |
53 | @Test
54 | public void applyIfRandomIsLowerThanFlag() {
55 | when(featureFlagsRepo.getFlag(anyLong())).thenReturn(Optional.of(new FlagDTO(randomLong(), randomString(), 50)));
56 | when(random.nextInt(100)).thenReturn(20);
57 | assertTrue(featureFlagsService.shouldApplyFeatureWithFlag(randomLong()));
58 | }
59 |
60 | @Test
61 | public void notApplyIfRandomIsHigherThanFlag() {
62 | when(featureFlagsRepo.getFlag(anyLong())).thenReturn(Optional.of(new FlagDTO(randomLong(), randomString(), 50)));
63 | when(random.nextInt(100)).thenReturn(70);
64 | assertFalse(featureFlagsService.shouldApplyFeatureWithFlag(randomLong()));
65 | }
66 |
67 | }
--------------------------------------------------------------------------------
/featureflags/src/test/java/com/github/quiram/shopping/featureflags/repositories/FlagRepositoryIT.java:
--------------------------------------------------------------------------------
1 | package com.github.quiram.shopping.featureflags.repositories;
2 |
3 | import com.github.quiram.shopping.featureflags.model.Flag;
4 | import org.junit.Test;
5 | import org.junit.runner.RunWith;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
8 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
9 | import org.springframework.test.context.TestPropertySource;
10 | import org.springframework.test.context.junit4.SpringRunner;
11 |
12 | import java.util.stream.StreamSupport;
13 |
14 | import static com.github.quiram.utils.Random.randomBoolean;
15 | import static com.github.quiram.utils.Random.randomInt;
16 | import static com.github.quiram.utils.Random.randomString;
17 | import static org.hamcrest.Matchers.is;
18 | import static org.junit.Assert.*;
19 |
20 | @RunWith(SpringRunner.class)
21 | @DataJpaTest
22 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
23 | @TestPropertySource(properties = {
24 | "spring.datasource.url= jdbc:postgresql://${test_db_host}/featureflags",
25 | "spring.datasource.platform=postgresql",
26 | "spring.datasource.username=testuser",
27 | "spring.datasource.password=test-password",
28 | "spring.jpa.hibernate.ddl-auto=create"
29 | })
30 | public class FlagRepositoryIT {
31 | @Autowired
32 | private FlagRepository flagRepository;
33 |
34 | @Test
35 | public void getExistingFlags() {
36 | final Iterable all = flagRepository.findAll();
37 | assertEquals(3, StreamSupport.stream(all.spliterator(), false).count());
38 | }
39 |
40 | @Test
41 | public void canSeeFlagAfterInserting() {
42 | final Flag newFlag = new Flag(null, randomString(), randomInt(100), randomBoolean());
43 | final Flag savedFlag = flagRepository.save(newFlag);
44 | final Flag retrievedFlag = flagRepository.findOne(savedFlag.getFlagId());
45 | assertThat(retrievedFlag.getName(), is(newFlag.getName()));
46 | assertThat(retrievedFlag.getPortionIn(), is(newFlag.getPortionIn()));
47 | assertNotNull(retrievedFlag.getFlagId());
48 | assertThat(retrievedFlag.isSticky(), is(newFlag.isSticky()));
49 | }
50 |
51 | @Test
52 | public void canFindFlagByName() {
53 | final Flag flag = flagRepository.findByName("disabled-feature");
54 | assertNotNull(flag);
55 | assertThat(flag.getPortionIn(), is(0));
56 | }
57 |
58 | @Test
59 | public void canDeleteFlags() {
60 | flagRepository.delete(3L);
61 | final Flag notFound = flagRepository.findOne(3L);
62 | assertNull(notFound);
63 | }
64 | }
--------------------------------------------------------------------------------
/jenkins-kubernetes/README.md:
--------------------------------------------------------------------------------
1 | # Jenkins - Kubernetes
2 | A sample Jenkins server with pre-created jobs that will deploy the three sample services to a kubernetes cluster.
3 |
4 | ## Pre-requisites
5 | `minikube` must be installed locally and running; this will create a minimal kubernetes cluster that Jenkins jobs will deploy to.
6 |
7 | ## Procedure
8 | 1. Make sure `minikube` is running (run `minikube start` if necessary).
9 | 1. Run `build.sh` to generate the necessary config files and docker image
10 | 1. Run `docker-compose -f docker-compose.yml up` to run the generated service; the Jenkins server is available at `http://localhost:8080/`
11 | 1. Once the Jenkins image has started, you'll need to ssh into it and open up permissions of the Docker socket file (**warning:** there is a security risk on doing this, see below for details).
12 | 1. Identify the Docker ID of the image: ``docker_id=`docker ps | grep jenkins-kubernetes | cut -f1 -d\ ` ``
13 | 1. SSH into the Docker container: `docker exec -ti ${docker_id} bash`
14 | 1. Once inside, open up permissions for the Docker socket file: `sudo chmod 777 /run/docker.sock`
15 | 1. Once Jenkins is up and running, you will need to create a Credentials key with id "DockerHub" and your user and password for Docker Hub.
16 | 1. After running the corresponding deployment jobs in Jenkins, run `expose-services.sh` to obtain the URLs where each service is available.
17 |
18 | ### Security exposure of opening up permissions in Docker socket file
19 | This CI/CD setup implies a Jenkins instance running inside a Docker container, with builds that invoke the `docker`
20 | command themselves to pack and deploy applications as Docker images. This means that we are trying to run `docker`
21 | within a Docker container. This is not trivial, and the implications are still being worked out. There are essentially two ways of achieving this:
22 | - Running Docker-in-Docker, that is, running a second Docker daemon within the Docker container itself.
23 | - Exposing the Docker daemon _in the host computer_ to the Docker containers, so running the command `docker` within the containers will communicate to the Docker daemon
24 | _in the host computer._
25 |
26 | Here we have opted for the latter, which is essentially achieved by making the Docker socket file accessible by any process... which isn't the safest of set-ups,
27 | so please don't do this in your production environment. It is ok to do it (temporarily) in your local machine to try out the examples laid out here, but once you're
28 | finished make sure to restore the permissions of the Docker socket file to its original settings; restarting the Docker server should do the trick.
29 |
30 | More on Docker-in-Docker on this [excellent post by Jérôme Petazzoni](https://jpetazzo.github.io/2015/09/03/do-not-use-docker-in-docker-for-ci/).
--------------------------------------------------------------------------------
/jenkins-aws-ecs/setup/create-env.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
4 |
5 | pushd ${SCRIPT_DIR}
6 | source ./constants.sh
7 |
8 | # Create cluster
9 | echo "Creating cluster with name ${CLUSTER_NAME}"
10 | run_aws ecs create-cluster --cluster-name ${CLUSTER_NAME}
11 |
12 | # Create key pair
13 | echo "Creating key pair with name ${KEY_PAIR_NAME}"
14 | run_aws ec2 create-key-pair --key-name ${KEY_PAIR_NAME}
15 | echo ${AWS_LAST_RESULT} | jq .KeyMaterial | cut -d\" -f2 | awk '{gsub(/\\n/,"\n")}1' >${PRIVATE_KEY_FILE}
16 | chmod 400 ${PRIVATE_KEY_FILE}
17 | echo "Private key for key pair ${KEY_PAIR_NAME} available at `pwd`/${PRIVATE_KEY_FILE}"
18 | echo "For your convenience, permissions to this file have been set to 400"
19 |
20 | # Create security group
21 | echo "Creating security group ${SECURITY_GROUP_NAME}"
22 | run_aws ec2 create-security-group \
23 | --description '"Security group to access all containers within the Extended Java Shop"' \
24 | --group-name ${SECURITY_GROUP_NAME}
25 |
26 | # Add rules to security group
27 | echo "Adding rules to security group ${SECURITY_GROUP_NAME}"
28 | add_ingress_rule ${SECURITY_GROUP_NAME} 22
29 | add_ingress_rule ${SECURITY_GROUP_NAME} 8000-8100
30 | add_ingress_rule ${SECURITY_GROUP_NAME} 5432
31 |
32 | # Create IAM roles
33 | #create_iam_role ${ECS_SERVICE_ROLE} ${EC2_CONTAINER_SERVICE_ARN} '"Allows ECS to (de)register EC2 instances in and out of load balancers."'
34 | create_iam_role ${ECS_INSTANCE_ROLE} ${EC2_FOR_ECS_POLICY_ARN} '"Allows EC2 instances in an ECS cluster to access ECS."'
35 |
36 | # AWS automatically creates an instance profile when you create a role, with the same name, but it doesn't link the two...
37 | echo "Adding role '${ECS_INSTANCE_ROLE}' to the relevant instance profile"
38 | run_aws iam add-role-to-instance-profile \
39 | --role-name ${ECS_INSTANCE_ROLE} \
40 | --instance-profile-name ${ECS_INSTANCE_ROLE}
41 |
42 | # Create temporary file for cluster attachment
43 | TEMP=".tmp.attachment.sh"
44 |
45 | cat >${TEMP} <> /etc/ecs/ecs.config
48 | EOF
49 |
50 | # Create instances
51 | echo "Creating ${NUMBER_OF_EC2_INSTANCES} EC2 instances that will be added to your ECS cluster"
52 | run_aws ec2 run-instances \
53 | --image-id ${AMI_ID} \
54 | --count ${NUMBER_OF_EC2_INSTANCES} \
55 | --instance-type t2.micro \
56 | --key-name ${KEY_PAIR_NAME} \
57 | --security-groups "${SECURITY_GROUP_NAME}" \
58 | --user-data file://${TEMP} \
59 | --iam-instance-profile Name=${ECS_INSTANCE_ROLE} \
60 | --tag-specifications "'ResourceType=instance,Tags=[{Key=${TAG_KEY},Value=${TAG_VALUE}}]'"
61 |
62 | rm -f ${TEMP}
63 |
64 | echo ""
65 | echo "All done. Your cluster should be ready at https://${REGION}.console.aws.amazon.com/ecs/home?region=${REGION}#/clusters/${CLUSTER_NAME}/services"
66 |
67 | popd
68 |
--------------------------------------------------------------------------------
/jenkins-aws-ecs/README.md:
--------------------------------------------------------------------------------
1 | # Jenkins - AWS ECS
2 | A sample Jenkins server with pre-created jobs that will deploy the three sample services to an Amazon Cloud (ECS)
3 |
4 | ## Pre-requisites
5 | - jq needs to be installed
6 | - An account for AWS ECS is necessary.
7 | - Install AWS - CLI
8 | - Set up AWS CLI locally: https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html
9 | - Run setup/create-env.sh
10 |
11 | ## Procedure
12 | 1. Run `build.sh` to generate the necessary config files and docker image
13 | 1. Run `docker-compose -f docker-compose.yml up` to run the generated service; the Jenkins server is available at `http://localhost:8080/`
14 | 1. Once the Jenkins image has started, you'll need to ssh into it and open up permissions of the Docker socket file (**warning:** there is a security risk on doing this, see below for details).
15 | 1. Identify the Docker ID of the image: ``docker_id=`docker ps | grep jenkins-kubernetes | cut -f1 -d\ ` ``
16 | 1. SSH into the Docker container: `docker exec -ti ${docker_id} bash`
17 | 1. Once inside, open up permissions for the Docker socket file: `sudo chmod 777 /run/docker.sock`
18 | 1. SSH into Jenkins to configure AWS credentials (get the key first from console, etc)
19 | 1. Once Jenkins is up and running, you will need to create a Credentials key with id "DockerHub" and your user and password for Docker Hub.
20 | 1. After running the corresponding deployment jobs in Jenkins, run `expose-services.sh` to obtain the URLs where each service is available.
21 |
22 | ### Security exposure of opening up permissions in Docker socket file
23 | This CI/CD setup implies a Jenkins instance running inside a Docker container, with builds that invoke the `docker`
24 | command themselves to pack and deploy applications as Docker images. This means that we are trying to run `docker`
25 | within a Docker container. This is not trivial, and the implications are still being worked out. There are essentially two ways of achieving this:
26 | - Running Docker-in-Docker, that is, running a second Docker daemon within the Docker container itself.
27 | - Exposing the Docker daemon _in the host computer_ to the Docker containers, so running the command `docker` within the containers will communicate to the Docker daemon
28 | _in the host computer._
29 |
30 | Here we have opted for the latter, which is essentially achieved by making the Docker socket file accessible by any process... which isn't the safest of set-ups,
31 | so please don't do this in your production environment. It is ok to do it (temporarily) in your local machine to try out the examples laid out here, but once you're
32 | finished make sure to restore the permissions of the Docker socket file to its original settings; restarting the Docker server should do the trick.
33 |
34 | More on Docker-in-Docker on this [excellent post by Jérôme Petazzoni](https://jpetazzo.github.io/2015/09/03/do-not-use-docker-in-docker-for-ci/).
35 |
--------------------------------------------------------------------------------
/spring-boot-master-pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | com.github.quiram.shopping
8 | spring-boot-master-pom
9 | 0.0.1-SNAPSHOT
10 | pom
11 |
12 |
13 | org.springframework.boot
14 | spring-boot-starter-parent
15 | 1.5.7.RELEASE
16 |
17 |
18 |
19 |
20 | jitpack.io
21 | https://jitpack.io
22 |
23 |
24 |
25 |
26 | UTF-8
27 | 1.8
28 | [v5.0.0,v5.1.0)
29 | [v5.1.0,v5.2.0)
30 | 3.1.0
31 | Edgware.SR3
32 |
33 |
34 |
35 |
36 |
37 | org.springframework.cloud
38 | spring-cloud-dependencies
39 | ${spring-cloud-dependencies.version}
40 | pom
41 | import
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | org.springframework.boot
50 | spring-boot-maven-plugin
51 |
52 |
53 | org.apache.maven.plugins
54 | maven-failsafe-plugin
55 |
56 |
57 | **/*CT.java
58 | **/*IT.java
59 | **/*CDC.java
60 |
61 |
62 |
63 |
64 |
65 | integration-test
66 | verify
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/jenkins-aws-ecs/setup/constants.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Common constants
4 | export AWS_LOG_FILE=aws.log
5 | export REGION=eu-central-1
6 | export APP_NAME=extended-java-shop
7 | export CLUSTER_NAME=${APP_NAME}-cluster
8 | export AMI_ID=ami-9fc39c74 # This is for region eu-central-1 (Frankfurt)
9 | export KEY_PAIR_NAME=${APP_NAME}-key-pair
10 | export PRIVATE_KEY_FILE=${KEY_PAIR_NAME}_private.pem
11 | export SECURITY_GROUP_NAME=${APP_NAME}-security-group
12 | export NUMBER_OF_EC2_INSTANCES=5
13 | export ECS_INSTANCE_ROLE=ecsInstanceRole # AWS needs this role to be called explicitly this, don't change it!
14 | export EC2_FOR_ECS_POLICY_ARN=arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role # Like previous
15 | #export ECS_SERVICE_ROLE=ecsServiceRole # Like previous
16 | #export EC2_CONTAINER_SERVICE_ARN=arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceRole # Like previous
17 | export TAG_KEY=Group
18 | export TAG_VALUE=${APP_NAME}
19 |
20 | # Useful functions
21 | run_aws() {
22 | cmd="aws $*"
23 | echo "Running '${cmd}'..." >>${AWS_LOG_FILE}
24 | result=`eval ${cmd}`
25 | echo ${result} >>${AWS_LOG_FILE}
26 | export AWS_LAST_RESULT=${result}
27 | }
28 |
29 | add_ingress_rule() {
30 | security_group_name=$1
31 | ports=$2
32 |
33 | run_aws ec2 authorize-security-group-ingress \
34 | --group-name ${security_group_name} \
35 | --protocol tcp --port ${ports} --cidr 0.0.0.0/0
36 | }
37 |
38 | create_iam_role() {
39 | role_name=$1
40 | policy_arn=$2
41 | role_description=$3
42 |
43 | echo "Creating IAM role '${role_name}' (${role_description})."
44 |
45 | # Create temporary file for role creation
46 | TEMP=".tmp.policy.document.json"
47 | cat >${TEMP} < getProducts() {
45 | Map productDTOs = productRepo.getProductDTOs();
46 | Map stockDTOMap = stockRepo.getStockDTOs();
47 |
48 | // Merge productDTOs and stockDTOs to a List of Products
49 | return productDTOs.values().stream()
50 | .map(productDTO -> {
51 | StockDTO stockDTO = stockDTOMap.getOrDefault(productDTO.getId(), DEFAULT_STOCK_DTO);
52 | return new Product(productDTO.getId(), stockDTO.getSku(), productDTO.getName(), productDTO.getDescription(),
53 | getPrice(productDTO), stockDTO.getAmountAvailable());
54 | })
55 | .collect(toList());
56 | }
57 |
58 | private BigDecimal getPrice(ProductDTO productDTO) {
59 | Optional maybeAdaptivePrice = Optional.empty();
60 |
61 | if (featureFlagsService.shouldApplyFeatureWithFlag(ADAPTIVE_PRICING_FLAG_ID))
62 | maybeAdaptivePrice = adaptivePricingRepo.getPriceFor(productDTO.getName());
63 |
64 | return maybeAdaptivePrice.orElse(productDTO.getPrice());
65 | }
66 |
67 | public List productsNotFound() {
68 | return emptyList();
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/acceptance-tests/src/test/java/com/github/quiram/shopping/acceptancetests/steps/ShopfrontSteps.java:
--------------------------------------------------------------------------------
1 | package com.github.quiram.shopping.acceptancetests.steps;
2 |
3 | import com.github.quiram.shopping.acceptancetests.pages.ShopfrontHomePage;
4 | import net.serenitybdd.core.Serenity;
5 | import net.thucydides.core.annotations.Step;
6 |
7 | import java.util.LinkedList;
8 | import java.util.List;
9 |
10 | import static com.github.quiram.utils.Collections.transpose;
11 | import static net.serenitybdd.core.Serenity.sessionVariableCalled;
12 | import static org.hamcrest.Matchers.hasItem;
13 | import static org.hamcrest.Matchers.is;
14 | import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
15 | import static org.junit.Assert.assertThat;
16 |
17 | public class ShopfrontSteps extends StepsBase {
18 | private static final String PRICES_COLLECTION_KEY = "prices_collection";
19 | private List productNames;
20 |
21 | @SuppressWarnings("unused")
22 | private ShopfrontHomePage page;
23 |
24 | @Step
25 | public void shopfront_service_is_ready() {
26 | page.load();
27 | }
28 |
29 | @Step
30 | public void user_obtains_the_list_of_products() {
31 | productNames = page.getProductNames();
32 | }
33 |
34 | @Step("There are {0} products in the list")
35 | public void product_list_has_size(int size) {
36 | assertThat(productNames, hasSize(size));
37 | }
38 |
39 | @Step("Product with name '{0} is in the list")
40 | public void includes_product_name(String name) {
41 | assertThat(productNames, hasItem(name));
42 | }
43 |
44 | @Step
45 | public void check_all_prices() {
46 | checkPrices();
47 | }
48 |
49 | @Step
50 | public void check_all_prices_again() {
51 | checkPrices();
52 | }
53 |
54 | @Step
55 | public void prices_have_not_changed() {
56 | final long numberOfDifferentSetsOfPrices = getPricesCollection().stream().distinct().count();
57 | assertThat(numberOfDifferentSetsOfPrices, is(1L));
58 | }
59 |
60 | @Step
61 | public void all_prices_have_changed() {
62 | List> pricesLists = transpose(getPricesCollection());
63 | pricesLists.forEach(pricesList -> {
64 | final long numberOfDifferentSetsOfPrices = pricesList.stream().distinct().count();
65 | assertThat(numberOfDifferentSetsOfPrices, is(2L));
66 | });
67 | }
68 |
69 | @SuppressWarnings("unchecked")
70 | private List> getPricesCollection() {
71 | List> pricesCollection = (List>) sessionVariableCalled(PRICES_COLLECTION_KEY);
72 | return pricesCollection == null ? new LinkedList<>() : pricesCollection;
73 | }
74 |
75 | private void checkPrices() {
76 | page.load();
77 | addToPrices(page.getPrices());
78 | }
79 |
80 | private void addToPrices(List prices) {
81 | final List> pricesCollection = getPricesCollection();
82 | pricesCollection.add(prices);
83 | Serenity.setSessionVariable(PRICES_COLLECTION_KEY).to(pricesCollection);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/jenkins-kubernetes/jobs/deploy/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | false
5 |
6 |
7 |
8 |
9 | project_name
10 |
11 | none
12 | true
13 |
14 |
15 |
16 |
17 |
18 | 2
19 |
20 |
21 | https://github.com/quiram/oreilly-cd-in-java
22 |
23 |
24 |
25 |
26 | */master
27 |
28 |
29 | false
30 |
31 |
32 | true
33 | false
34 | false
35 | false
36 |
37 | false
38 |
39 |
40 | cp jenkins-kubernetes/service-definitions/${project_name}-service.yaml jenkins-kubernetes/service-definitions/service.yaml
41 |
42 |
43 |
44 | minikube
45 | KubeConfig
46 |
47 |
48 | *
49 |
50 |
51 |
52 |
53 |
54 | https://
55 |
56 |
57 |
58 |
59 | jenkins-kubernetes/service-definitions/service.yaml
60 | true
61 |
62 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/shopfront/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | uk.co.danielbryant.shopping
7 | shopfront
8 | 0.0.1-SNAPSHOT
9 | jar
10 |
11 | shopfront
12 | Docker Java application Shopfront
13 |
14 |
15 | com.github.quiram.shopping
16 | spring-boot-master-pom
17 | 0.0.1-SNAPSHOT
18 | ../spring-boot-master-pom.xml
19 |
20 |
21 |
22 |
23 | org.springframework.boot
24 | spring-boot-starter-thymeleaf
25 |
26 |
27 | org.springframework.boot
28 | spring-boot-starter-actuator
29 |
30 |
31 | org.springframework.cloud
32 | spring-cloud-starter-hystrix
33 |
34 |
35 | org.springframework.cloud
36 | spring-cloud-starter-eureka
37 |
38 |
39 | com.github.quiram
40 | java-utils
41 | ${java-utils.version}
42 |
43 |
44 |
45 | org.springframework.boot
46 | spring-boot-starter-test
47 | test
48 |
49 |
50 | org.mockito
51 | mockito-core
52 | test
53 |
54 |
55 | org.hamcrest
56 | hamcrest-core
57 | test
58 |
59 |
60 |
61 | com.github.tomakehurst
62 | wiremock-standalone
63 | test
64 |
65 |
66 |
67 | org.awaitility
68 | awaitility
69 | ${awaitility.version}
70 | test
71 |
72 |
73 | org.springframework.cloud
74 | spring-cloud-starter-contract-stub-runner
75 | test
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/jenkins-base/jobs/featureflags-db/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | false
6 |
7 |
8 | 2
9 |
10 |
11 | https://github.com/quiram/oreilly-cd-in-java
12 |
13 |
14 |
15 |
16 | */master
17 |
18 |
19 | false
20 |
21 |
22 |
23 | featureflags-db/.*
24 |
25 |
26 |
27 |
28 | true
29 | false
30 | false
31 | false
32 |
33 |
34 | * * * * *
35 | false
36 |
37 |
38 | false
39 |
40 |
41 |
42 |
43 | DockerHub
44 |
45 | quiram/featureflags-db
46 | false
47 | true
48 | featureflags-db
49 | false
50 | false
51 |
52 | false
53 | true
54 | false
55 |
56 | false
57 | Default
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | project_name=$JOB_NAME
67 | false
68 |
69 |
70 | deploy,
71 | SUCCESS
72 | false
73 | false
74 |
75 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/featureflags/src/main/java/com/github/quiram/shopping/featureflags/resources/FlagResource.java:
--------------------------------------------------------------------------------
1 | package com.github.quiram.shopping.featureflags.resources;
2 |
3 | import com.github.quiram.shopping.featureflags.exceptions.FlagCreatedWithIdException;
4 | import com.github.quiram.shopping.featureflags.exceptions.FlagNameAlreadyExistsException;
5 | import com.github.quiram.shopping.featureflags.exceptions.FlagNotFoundException;
6 | import com.github.quiram.shopping.featureflags.exceptions.FlagWithoutIdException;
7 | import com.github.quiram.shopping.featureflags.model.Flag;
8 | import com.github.quiram.shopping.featureflags.services.FlagService;
9 | import org.slf4j.Logger;
10 | import org.slf4j.LoggerFactory;
11 | import org.springframework.beans.factory.annotation.Autowired;
12 | import org.springframework.http.ResponseEntity;
13 | import org.springframework.web.bind.annotation.*;
14 |
15 | import java.net.URI;
16 | import java.util.List;
17 |
18 | import static org.springframework.http.HttpStatus.BAD_REQUEST;
19 | import static org.springframework.http.HttpStatus.NOT_FOUND;
20 | import static org.springframework.web.bind.annotation.RequestMethod.*;
21 |
22 | @RestController
23 | @RequestMapping("/flags")
24 | public class FlagResource {
25 |
26 | private static final Logger LOGGER = LoggerFactory.getLogger(FlagResource.class);
27 |
28 | @Autowired
29 | private FlagService flagService;
30 |
31 | @RequestMapping(method = GET)
32 | public List getFlags() {
33 | LOGGER.info("getFlags (All flags)");
34 | return flagService.getFlags();
35 | }
36 |
37 | @RequestMapping(method = POST)
38 | public ResponseEntity> createFlag(@RequestBody Flag flag) throws FlagCreatedWithIdException, FlagNameAlreadyExistsException {
39 | LOGGER.info("createFlag: {}", flag);
40 | final Flag savedFlag = flagService.addFlag(flag);
41 | return ResponseEntity.created(URI.create("/flags/" + savedFlag.getFlagId())).build();
42 | }
43 |
44 | @RequestMapping(value = "{flagId}", method = GET)
45 | public Flag getFlag(@PathVariable("flagId") Long flagId) throws FlagNotFoundException {
46 | LOGGER.info("getFlag with flagId: {}", flagId);
47 | return flagService.getFlag(flagId);
48 | }
49 |
50 | @RequestMapping(value = "{flagId}", method = DELETE)
51 | public ResponseEntity> deleteFlag(@PathVariable("flagId") Long flagId) throws FlagNotFoundException {
52 | LOGGER.info("deleteFlag with flagId: {}", flagId);
53 | flagService.removeFlag(flagId);
54 | return ResponseEntity.ok().build();
55 | }
56 |
57 | @RequestMapping(value = "{flagId}", method = PUT)
58 | public ResponseEntity> updateFlag(@PathVariable("flagId") Long flagId, @RequestBody Flag flag) throws FlagNotFoundException,
59 | FlagWithoutIdException {
60 | LOGGER.info("updating with flagId: {}", flagId);
61 | flagService.updateFlag(new Flag(flagId, flag.getName(), flag.getPortionIn(), flag.isSticky()));
62 | return ResponseEntity.ok().build();
63 | }
64 |
65 |
66 | @ExceptionHandler
67 | @ResponseStatus(NOT_FOUND)
68 | public void handleFlagNotFound(FlagNotFoundException e) {
69 | }
70 |
71 | @ExceptionHandler
72 | @ResponseStatus(BAD_REQUEST)
73 | public void handleFlagWithId(FlagCreatedWithIdException e) {
74 | }
75 |
76 | @ExceptionHandler
77 | @ResponseStatus(BAD_REQUEST)
78 | public void handleFlagNameAlreadyExists(FlagNameAlreadyExistsException e) {
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/jenkins-base/jobs/featureflags/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | false
6 |
7 |
8 | 2
9 |
10 |
11 | https://github.com/quiram/oreilly-cd-in-java
12 |
13 |
14 |
15 |
16 | */master
17 |
18 |
19 | false
20 |
21 |
22 |
23 | featureflags/.*
24 | spring-boot-master-pom.xml
25 |
26 |
27 |
28 |
29 | true
30 | false
31 | false
32 | false
33 |
34 |
35 | * * * * *
36 | false
37 |
38 |
39 | false
40 |
41 |
42 | clean install
43 | Default
44 | featureflags/pom.xml
45 | false
46 |
47 |
48 | false
49 |
50 |
51 |
52 |
53 | DockerHub
54 |
55 | quiram/featureflags
56 | false
57 | true
58 | featureflags
59 | false
60 | false
61 |
62 | false
63 | true
64 | false
65 |
66 | false
67 | Default
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | project_name=$JOB_NAME
77 | false
78 |
79 |
80 | acceptance-tests,
81 | SUCCESS
82 | false
83 | false
84 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/jenkins-base/jobs/stockmanager/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | false
6 |
7 |
8 | 2
9 |
10 |
11 | https://github.com/quiram/oreilly-cd-in-java
12 |
13 |
14 |
15 |
16 | */master
17 |
18 |
19 | false
20 |
21 |
22 |
23 | stockmanager/.*
24 | spring-boot-master-pom.xml
25 |
26 |
27 |
28 |
29 | true
30 | false
31 | false
32 | false
33 |
34 |
35 | * * * * *
36 | false
37 |
38 |
39 | false
40 |
41 |
42 | clean install
43 | Default
44 | stockmanager/pom.xml
45 | false
46 |
47 |
48 | false
49 |
50 |
51 |
52 |
53 | DockerHub
54 |
55 | quiram/stockmanager
56 | false
57 | true
58 | stockmanager
59 | false
60 | false
61 |
62 | false
63 | true
64 | false
65 |
66 | false
67 | Default
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | project_name=$JOB_NAME
77 | false
78 |
79 |
80 | acceptance-tests,
81 | SUCCESS
82 | false
83 | false
84 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/jenkins-base/jobs/productcatalogue/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | false
6 |
7 |
8 | 2
9 |
10 |
11 | https://github.com/quiram/oreilly-cd-in-java
12 |
13 |
14 |
15 |
16 | */master
17 |
18 |
19 | false
20 |
21 |
22 |
23 | productcatalogue/.*
24 | non-spring-boot-master-pom.xml
25 |
26 |
27 |
28 |
29 | true
30 | false
31 | false
32 | false
33 |
34 |
35 | * * * * *
36 | false
37 |
38 |
39 | false
40 |
41 |
42 | clean install
43 | Default
44 | productcatalogue/pom.xml
45 | false
46 |
47 |
48 | false
49 |
50 |
51 |
52 |
53 | DockerHub
54 |
55 | quiram/productcatalogue
56 | false
57 | true
58 | productcatalogue
59 | false
60 | false
61 |
62 | false
63 | true
64 | false
65 |
66 | false
67 | Default
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | project_name=$JOB_NAME
77 | false
78 |
79 |
80 | acceptance-tests,
81 | SUCCESS
82 | false
83 | false
84 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/jenkins-base/jobs/external-adaptive-pricing/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | false
6 |
7 |
8 | 2
9 |
10 |
11 | https://github.com/quiram/oreilly-cd-in-java
12 |
13 |
14 |
15 |
16 | */master
17 |
18 |
19 | false
20 |
21 |
22 |
23 | external-adaptive-pricing/.*
24 | spring-boot-master-pom.xml
25 |
26 |
27 |
28 |
29 | true
30 | false
31 | false
32 | false
33 |
34 |
35 | * * * * *
36 | false
37 |
38 |
39 | false
40 |
41 |
42 | clean install
43 | Default
44 | external-adaptive-pricing/pom.xml
45 | false
46 |
47 |
48 | false
49 |
50 |
51 |
52 |
53 | DockerHub
54 |
55 | quiram/external-adaptive-pricing
56 | false
57 | true
58 | external-adaptive-pricing
59 | false
60 | false
61 |
62 | false
63 | true
64 | false
65 |
66 | false
67 | Default
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | project_name=$JOB_NAME
77 | false
78 |
79 |
80 | deploy,
81 | SUCCESS
82 | false
83 | false
84 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/productcatalogue/src/test/java/uk/co/danielbryant/shopping/productcatalogue/ProductServiceApplicationCT.java:
--------------------------------------------------------------------------------
1 | package uk.co.danielbryant.shopping.productcatalogue;
2 |
3 | import io.dropwizard.testing.junit.DropwizardAppRule;
4 | import org.glassfish.jersey.client.JerseyClientBuilder;
5 | import org.junit.Before;
6 | import org.junit.ClassRule;
7 | import org.junit.Test;
8 | import uk.co.danielbryant.shopping.productcatalogue.configuration.ProductServiceConfiguration;
9 | import uk.co.danielbryant.shopping.productcatalogue.model.v2.BulkPrice;
10 | import uk.co.danielbryant.shopping.productcatalogue.model.v2.Product;
11 | import uk.co.danielbryant.shopping.productcatalogue.model.v2.UnitPrice;
12 |
13 | import javax.ws.rs.client.Client;
14 | import javax.ws.rs.core.GenericType;
15 | import javax.ws.rs.core.Response;
16 | import java.math.BigDecimal;
17 | import java.util.List;
18 |
19 | import static io.dropwizard.testing.ResourceHelpers.resourceFilePath;
20 | import static org.eclipse.jetty.http.HttpStatus.NOT_FOUND_404;
21 | import static org.eclipse.jetty.http.HttpStatus.OK_200;
22 | import static org.hamcrest.CoreMatchers.is;
23 | import static org.junit.Assert.assertEquals;
24 | import static org.junit.Assert.assertThat;
25 |
26 | public class ProductServiceApplicationCT {
27 | @ClassRule
28 | public static final DropwizardAppRule RULE = new DropwizardAppRule<>(
29 | ProductServiceApplication.class,
30 | resourceFilePath("product-catalogue.yml"));
31 | private Client client;
32 | private String baseUrl;
33 |
34 |
35 | @Before
36 | public void setUp() {
37 | client = new JerseyClientBuilder().build();
38 | baseUrl = "http://localhost:" + RULE.getLocalPort();
39 | }
40 |
41 | @Test
42 | public void canGetAllProducts() {
43 | final Response response = client.target(baseUrl + "/products").request().get();
44 | assertEquals(OK_200, response.getStatus());
45 | final List products = response.readEntity(new GenericType>() {
47 | });
48 | assertEquals(5, products.size());
49 | }
50 |
51 | @Test
52 | public void getV2Products() {
53 | final Response response = client.target(baseUrl + "/v2/products").request().get();
54 | assertEquals(OK_200, response.getStatus());
55 | final List products = response.readEntity(new GenericType>() {
56 | });
57 | assertEquals(5, products.size());
58 | assertThat(products.get(0).getPrice().getBulkPrice(), is(new BulkPrice(new UnitPrice(new BigDecimal("1.00")), 5)));
59 | }
60 |
61 | @Test
62 | public void canGetSpecificProduct() {
63 | final Response response = client.target(baseUrl + "/products/1").request().get();
64 | assertEquals(OK_200, response.getStatus());
65 | final uk.co.danielbryant.shopping.productcatalogue.model.v1.Product product = response.readEntity(uk.co.danielbryant.shopping.productcatalogue.model.v1.Product.class);
66 | assertEquals("Widget", product.getName());
67 | assertEquals(new BigDecimal("1.20"), product.getPrice());
68 | }
69 |
70 | @Test
71 | public void canGetSpecificV2Product() {
72 | final Response response = client.target(baseUrl + "/v2/products/3").request().get();
73 | assertEquals(OK_200, response.getStatus());
74 | final Product product = response.readEntity(Product.class);
75 | assertEquals("Anvil", product.getName());
76 | assertThat(product.getPrice().getSingle().getValue(), is(new BigDecimal("45.50")));
77 | assertThat(product.getPrice().getBulkPrice(), is(new BulkPrice(new UnitPrice(new BigDecimal("45.00")), 10)));
78 | }
79 |
80 | @Test
81 | public void canHandleProductThatDoesNotExist() {
82 | final Response response = client.target(baseUrl + "/products/987897987").request().get();
83 | assertEquals(NOT_FOUND_404, response.getStatus());
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/jenkins-base/jobs/acceptance-tests/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | false
5 |
6 |
7 |
8 |
9 | project_name
10 |
11 | none
12 | true
13 |
14 |
15 |
16 |
17 |
18 | 2
19 |
20 |
21 | https://github.com/quiram/oreilly-cd-in-java
22 |
23 |
24 |
25 |
26 | */master
27 |
28 |
29 | false
30 |
31 |
32 | true
33 | false
34 | false
35 | false
36 |
37 | false
38 |
39 |
40 | echo "Running acceptance tests for ${project_name}"
41 |
42 |
43 | cat acceptance-tests/src/test/resources/docker-compose.yml acceptance-tests/src/test/resources/docker-compose-ci-network.yml | \
44 | sed -e s/"%network_placeholder%"/"${docker_network_name}"/ > acceptance-tests/src/test/resources/docker-compose-ci.yml
45 |
46 |
47 | clean verify
48 | Default
49 | acceptance-tests/pom.xml
50 | false
51 |
52 |
53 | false
54 |
55 |
56 |
57 |
58 |
59 |
60 | SerenityBDD
61 | acceptance-tests/target/site/serenity
62 | index.html
63 | true
64 | SerenityBDD
65 | true
66 | false
67 | **/*
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | deploy,
78 | SUCCESS
79 | false
80 | false
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------