├── 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 | --------------------------------------------------------------------------------