Compare commits

..

2 Commits

Author SHA1 Message Date
Tom Hombergs
d8fa325ae0 updated README 2018-03-17 21:24:18 +01:00
Tom Hombergs
fceb1d898d added pact publish task 2018-03-17 21:21:22 +01:00
880 changed files with 1343 additions and 49711 deletions

3
.gitignore vendored
View File

@@ -1,3 +0,0 @@
**/.idea/
**/*.iml

View File

@@ -1,18 +1,7 @@
before_install:
- chmod +x build-all.sh
- |
if ! git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '(.md)|^(LICENSE)'
then
echo "Not running CI since only docs were changed."
exit
fi
install: skip
script:
- ./build-all.sh
- chmod +x gradlew
language: java
jdk:
- oraclejdk11
- oraclejdk8

View File

@@ -2,20 +2,7 @@
[![Travis CI Status](https://travis-ci.org/thombergs/code-examples.svg?branch=master)](https://travis-ci.org/thombergs/code-examples)
This repo contains example projects which show how to use different (not only) Java technologies.
This repo contains example projects which show how to use different java technologies.
The examples are usually accompanied by a blog post on [https://reflectoring.io](https://reflectoring.io).
See the READMEs in each subdirectory of this repo for more information on each module.
## Java Modules
All Java modules require **Java 11** to compile and run.
### Building with Gradle
Each module should be an independent build and can be built by calling `./gradlew clean build` in the module directory.
All modules are listed in [build-all.sh](build-all.sh) to run in the CI pipeline.
### Non-Java Modules
Some folders contain non-Java projects. For those, refer to the README within the module folder.
See the READMEs in each subdirectory of this repo for more information.

View File

@@ -1,32 +0,0 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**
!**/src/test/**
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/

View File

@@ -1,5 +0,0 @@
FROM openjdk:8-jdk-alpine
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
EXPOSE 8080

View File

@@ -1,24 +0,0 @@
plugins {
id 'org.springframework.boot' version '2.2.4.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
}
group = 'io.reflectoring'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
test {
useJUnitPlatform()
}

View File

@@ -1,172 +0,0 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

View File

@@ -1 +0,0 @@
rootProject.name = 'aws-hello-world'

View File

@@ -1,13 +0,0 @@
package io.reflectoring.awshelloworld;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AwsHelloWorldApplication {
public static void main(String[] args) {
SpringApplication.run(AwsHelloWorldApplication.class, args);
}
}

View File

@@ -1,14 +0,0 @@
package io.reflectoring.awshelloworld;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloWorldController {
@GetMapping("/hello")
public String helloWorld(){
return "Hello AWS!";
}
}

View File

@@ -1,13 +0,0 @@
package io.reflectoring.awshelloworld;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class AwsHelloWorldApplicationTests {
@Test
void contextLoads() {
}
}

View File

@@ -1,32 +0,0 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**
!**/src/test/**
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/

View File

@@ -1,5 +0,0 @@
FROM openjdk:8-jdk-alpine
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
EXPOSE 8080

View File

@@ -1,25 +0,0 @@
# RDS Hello World Application
This is a simple Spring Boot application which requires access to a PostgreSQL database.
The application has a single endpoint `/hello` which prints out if the database connection was successful.
Get it in a Docker image via `docker pull reflectoring/aws-rds-hello-world`.
Use the image instead of your real application to test AWS CloudFormation stacks which need access to a database.
## Testing AWS RDS connectivity with this application
1. Create an RDS PostgreSQL database with the AWS console.
2. Note the endpoint of your RDS database in the AWS console.
3. Deploy the Docker container `reflectoring/aws-rds-hello-world` into AWS instead of your real application (this could be via a CloudFormation stack, manually, or however you are deploying your app).
4. Configure your deployment in a way that Docker will pass the coordinates to your RDS database as environment variables, equivalent to this command:
```
docker run \
-e SPRING_DATASOURCE_URL=':'<RDS-ENDPOINT>:5432/postgres \
-e SPRING_DATASOURCE_USERNAME=<USERNAME> \
-e SPRING_DATASOURCE_PASSWORD=<PASSWORD> \
-p 8080:8080 reflectoring/aws-rds-hello-world
```
5. If the Spring Boot application can connect to the database, it will start up sucessfully and serve a message on the endpoint `/hello`.

View File

@@ -1,31 +0,0 @@
plugins {
id 'org.springframework.boot' version '2.2.4.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
}
group = 'io.reflectoring'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
// database
implementation 'org.flywaydb:flyway-core'
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
runtimeOnly 'org.postgresql:postgresql'
testRuntimeOnly 'org.postgresql:postgresql'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
test {
useJUnitPlatform()
}

View File

@@ -1,18 +0,0 @@
version: '3.7'
services:
postgres:
container_name: "rds-hello-world"
image: postgres
volumes:
- rds-hello-world:/var/lib/postgresql/data
ports:
- 5432:5432
environment:
- POSTGRES_USER=hello
- POSTGRES_PASSWORD=hello
volumes:
rds-hello-world:
driver: local

View File

@@ -1,5 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -1,172 +0,0 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

View File

@@ -1 +0,0 @@
rootProject.name = 'aws-rds-hello-world'

View File

@@ -1,13 +0,0 @@
package io.reflectoring.awshelloworld;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AwsHelloWorldApplication {
public static void main(String[] args) {
SpringApplication.run(AwsHelloWorldApplication.class, args);
}
}

View File

@@ -1,23 +0,0 @@
package io.reflectoring.awshelloworld;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class HelloWorldController {
private final UserRepository userRepository;
HelloWorldController(UserRepository userRepository) {
this.userRepository = userRepository;
}
@GetMapping("/hello")
String helloWorld(){
Iterable<User> users = userRepository.findAll();
return "Hello AWS! Successfully connected to the database!";
}
}

View File

@@ -1,38 +0,0 @@
package io.reflectoring.awshelloworld;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
@Table("hello_user")
public class User {
@Id
private Long id;
private String name;
public User(Long id, String name) {
this.id = id;
this.name = name;
}
public User() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -1,6 +0,0 @@
package io.reflectoring.awshelloworld;
import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends CrudRepository<User, Long> {
}

View File

@@ -1,5 +0,0 @@
spring:
datasource:
url: jdbc:postgresql://localhost:5432/hello
username: hello
password: hello

View File

@@ -1,5 +0,0 @@
create table hello_user (
id varchar(36) not null unique,
name varchar(100) not null,
primary key(id)
);

View File

@@ -1,8 +0,0 @@
# Overview
![ECS in two public subnets](ecs-in-two-public-subnets.svg)
# Companion Blog Post
[The AWS Journey Part 2: Deploying a Docker image from the Command Line with CloudFormation](https://reflectoring.io/aws-cloudformation-deploy-docker-image/)

View File

@@ -1 +0,0 @@
<mxfile modified="2020-04-30T21:28:18.347Z" host="app.diagrams.net" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36" etag="X9Eef2gSXNvxkCnmY-_0" version="13.0.4" type="device"><diagram id="Ht1M8jgEwFfnCIfOTk4-" name="Page-1">7Vpbb+o4EP41PC7K/fLIrWcr9UiVWO05+4RM4garIc4aU6C/fseJc7ND6ekB9nQXVKmZsTMez3yf7TEM7Ml6/4WhfPWVxjgdWEa8H9jTgWWZphXCP6E5lBrPcEpFwkgsOzWKOXnFUmlI7ZbEeNPpyClNOcm7yohmGY54R4cYo7tutyeadkfNUYI1xTxCqa79RmK+qublhU3D75gkKzl0YPllwxpVneVMNisU011LZc8G9oRRysun9X6CUxG8Ki7le3dHWmvHGM74e15Y4cjav97736avy2fP+Rr/vVj+Vjn3gtKtnPGfjxPpMD9UUcgpyXgRSXcMf/DSxBi40DIR0tByFYUq+12FqUvCRlehyn5XYarmTWV8U3WwpdCkjnlDGd9oOQh/9phueUoyPKkxZ4AyYSgmkIsJTSkDXUYziN54xdcpSCY87laE43mOIhHVHfAFdE804xL1plXJMvDiHUBNLp7X+0QQbIh2G2eYMLrNiyHvAfe9rYuXPBKvc0afceXSwLItJwhMRwxE0lRx9QUzTgD6o5QkwiqnYhAkpRQ/cWER/CdZ8lBIU9uQPreGGI3G/jgAfYw2KxzL8EicwRB4fxTBZs0LWFAwXWPODtClesGTaJVrSQXeXUNMO5S6VYuTjiGVSC4GSW264Qs8SMr8AH3MUKPP43aZkkjQZ7vMML9R6fNTaYOjLSP8sGg6zwteSXffRzLQz8I7e+adj2n1OOdmmm0pTLPCoR1obKsZ2Gab6ZkFSC5COPvGtxvf/vt8c4Buftj6uL8A90xfI99sMgfFHLMXAshQqddzxNBS5468SeC1Q2oezZcKMCU7talzJMTvJsTvXf9MuycHVnip04arxX9geamA7hIeEvEwynNYDREnwK2qjVWNDxTFuhZogLIIs6oFXKvNaSntXT46bKhS94CWOH2kG1L4Yk+XlHO6PsnFCBIEvnQWn56FxB6iZqaLFCa2WFbz0JaBOzdwbef4OncGvMAZs4MX0w+GgQ6Yqmps48W3LgSX4DRc7kWwi13TQ2uRhmy5yYuIqCj5gjjeocMnAwmR81sk0v1+bHgXxUa9SrSw4esL+lWxodct5VL+B9o8vy+f6tZoW3difC2OIsZ+MDPa/JsSBobKlGeUiQioeZka7gQ2nJ4d46n4XAFRp843aJOX4Xgie+FH/4GH4Q3dsgiXx50xiH0HHxxtzgO2utatwGb07lx+z8blX6xKdm5o+3+gzXL/fbRZeo2oYQzHCa6CKxBBE5qhdNZo25nBWTwSN8kivymNnoUqXRZyleYCOYjxqp/cPODNO5LWO5R29nXcsSeQCmVOFtfggtizw/e28JcQoKqT4nTfbpwe2tIjZgTiKNDVKk7EfN9OMYSnAM5bgZWbBkw0wfzUwUPHTPvgXOGG4RSOcC9d5/ogIc09imq+wV+owM9Uap9yTvKl9t24YsdU6i9bvTEs56wZKgBaT/EnMOvdMPsRzJ6Eons1KFrqUvhhLAYKFs0rY1Ev8z8ZFveEf6/GhucWEkFqgCiEQ0u44NLpvhOu5UnpKng1TfXKyfgYXq1Tho7gFXIt6sm6m7yoPeqw7fYT7JhfWv/QUehSenBe8vQV3TfyXIU84dW4Y1ff+9VfjzjDIDDqj2LwvUxyHLdr1hOn6aNmfxFemc41eKVfWLx1mfUp76isH7+dqn9T0nwtUv1KxT7TxaaS7MDvu7vyw6FUtks81zlOrCMlHojNL2JK8DS/K7Jn/wA=</diagram></mxfile>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -1,247 +0,0 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: A network stack for deploying containers in AWS ECS.
This stack creates a VPC with two public subnets and a loadbalancer to balance traffic between those subnets.
Derived from a template at https://github.com/nathanpeck/aws-cloudformation-fargate.
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: '10.0.0.0/16'
PublicSubnetOne:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone:
Fn::Select:
- 0
- Fn::GetAZs: {Ref: 'AWS::Region'}
VpcId: !Ref 'VPC'
CidrBlock: '10.0.1.0/24'
MapPublicIpOnLaunch: true
PublicSubnetTwo:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone:
Fn::Select:
- 1
- Fn::GetAZs: {Ref: 'AWS::Region'}
VpcId: !Ref 'VPC'
CidrBlock: '10.0.2.0/24'
MapPublicIpOnLaunch: true
InternetGateway:
Type: AWS::EC2::InternetGateway
GatewayAttachement:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref 'VPC'
InternetGatewayId: !Ref 'InternetGateway'
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref 'VPC'
PublicSubnetOneRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetOne
RouteTableId: !Ref PublicRouteTable
PublicSubnetTwoRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetTwo
RouteTableId: !Ref PublicRouteTable
PublicRoute:
Type: AWS::EC2::Route
DependsOn: GatewayAttachement
Properties:
RouteTableId: !Ref 'PublicRouteTable'
DestinationCidrBlock: '0.0.0.0/0'
GatewayId: !Ref 'InternetGateway'
PublicLoadBalancerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Access to the public facing load balancer
VpcId: !Ref 'VPC'
SecurityGroupIngress:
# Allow access to ALB from anywhere on the internet
- CidrIp: 0.0.0.0/0
IpProtocol: -1
PublicLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Scheme: internet-facing
Subnets:
# The load balancer is placed into the public subnets, so that traffic
# from the internet can reach the load balancer directly via the internet gateway
- !Ref PublicSubnetOne
- !Ref PublicSubnetTwo
SecurityGroups: [!Ref 'PublicLoadBalancerSecurityGroup']
DummyTargetGroupPublic:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 6
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
Name: "no-op"
Port: 80
Protocol: HTTP
UnhealthyThresholdCount: 2
VpcId: !Ref 'VPC'
PublicLoadBalancerListener:
Type: AWS::ElasticLoadBalancingV2::Listener
DependsOn:
- PublicLoadBalancer
Properties:
DefaultActions:
- TargetGroupArn: !Ref 'DummyTargetGroupPublic'
Type: 'forward'
LoadBalancerArn: !Ref 'PublicLoadBalancer'
Port: 80
Protocol: HTTP
ECSCluster:
Type: AWS::ECS::Cluster
ECSSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Access to the ECS containers
VpcId: !Ref 'VPC'
ECSSecurityGroupIngressFromPublicALB:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Ingress from the public ALB
GroupId: !Ref 'ECSSecurityGroup'
IpProtocol: -1
SourceSecurityGroupId: !Ref 'PublicLoadBalancerSecurityGroup'
ECSSecurityGroupIngressFromSelf:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Ingress from other containers in the same security group
GroupId: !Ref 'ECSSecurityGroup'
IpProtocol: -1
SourceSecurityGroupId: !Ref 'ECSSecurityGroup'
ECSRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: [ecs.amazonaws.com]
Action: ['sts:AssumeRole']
Path: /
Policies:
- PolicyName: ecs-service
PolicyDocument:
Statement:
- Effect: Allow
Action:
# Rules which allow ECS to attach network interfaces to instances
# on your behalf in order for awsvpc networking mode to work right
- 'ec2:AttachNetworkInterface'
- 'ec2:CreateNetworkInterface'
- 'ec2:CreateNetworkInterfacePermission'
- 'ec2:DeleteNetworkInterface'
- 'ec2:DeleteNetworkInterfacePermission'
- 'ec2:Describe*'
- 'ec2:DetachNetworkInterface'
# Rules which allow ECS to update load balancers on your behalf
# with the information sabout how to send traffic to your containers
- 'elasticloadbalancing:DeregisterInstancesFromLoadBalancer'
- 'elasticloadbalancing:DeregisterTargets'
- 'elasticloadbalancing:Describe*'
- 'elasticloadbalancing:RegisterInstancesWithLoadBalancer'
- 'elasticloadbalancing:RegisterTargets'
Resource: '*'
ECSTaskExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: [ecs-tasks.amazonaws.com]
Action: ['sts:AssumeRole']
Path: /
Policies:
- PolicyName: AmazonECSTaskExecutionRolePolicy
PolicyDocument:
Statement:
- Effect: Allow
Action:
# Allow the ECS Tasks to download images from ECR
- 'ecr:GetAuthorizationToken'
- 'ecr:BatchCheckLayerAvailability'
- 'ecr:GetDownloadUrlForLayer'
- 'ecr:BatchGetImage'
# Allow the ECS tasks to upload logs to CloudWatch
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
Resource: '*'
Outputs:
ClusterName:
Description: The name of the ECS cluster
Value: !Ref 'ECSCluster'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ClusterName' ] ]
ExternalUrl:
Description: The url of the external load balancer
Value: !Join ['', ['http://', !GetAtt 'PublicLoadBalancer.DNSName']]
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ExternalUrl' ] ]
ECSRole:
Description: The ARN of the ECS role
Value: !GetAtt 'ECSRole.Arn'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ECSRole' ] ]
ECSTaskExecutionRole:
Description: The ARN of the ECS role
Value: !GetAtt 'ECSTaskExecutionRole.Arn'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ECSTaskExecutionRole' ] ]
PublicListener:
Description: The ARN of the public load balancer's Listener
Value: !Ref PublicLoadBalancerListener
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicListener' ] ]
VPCId:
Description: The ID of the VPC that this stack is deployed in
Value: !Ref 'VPC'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'VPCId' ] ]
PublicSubnetOne:
Description: Public subnet one
Value: !Ref 'PublicSubnetOne'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicSubnetOne' ] ]
PublicSubnetTwo:
Description: Public subnet two
Value: !Ref 'PublicSubnetTwo'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicSubnetTwo' ] ]
ECSSecurityGroup:
Description: A security group used to allow ECS containers to receive traffic
Value: !Ref 'ECSSecurityGroup'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ECSSecurityGroup' ] ]

View File

@@ -1,139 +0,0 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: Deploy a service on AWS Fargate, hosted in two public subnets and accessible via a public load balancer.
Derived from a template at https://github.com/nathanpeck/aws-cloudformation-fargate.
Parameters:
StackName:
Type: String
Description: The name of the networking stack that
these resources are put into.
ServiceName:
Type: String
Description: A human-readable name for the service.
HealthCheckPath:
Type: String
Default: /health
Description: Path to perform the healthcheck on each instance.
HealthCheckIntervalSeconds:
Type: Number
Default: 5
Description: Number of seconds to wait between each health check.
ImageUrl:
Type: String
Description: The url of a docker image that will handle incoming traffic.
ContainerPort:
Type: Number
Default: 80
Description: The port number the application inside the docker container
is binding to.
ContainerCpu:
Type: Number
Default: 256
Description: How much CPU to give the container. 1024 is 1 CPU.
ContainerMemory:
Type: Number
Default: 512
Description: How much memory in megabytes to give the container.
Path:
Type: String
Default: "*"
Description: A path on the public load balancer that this service
should be connected to.
DesiredCount:
Type: Number
Default: 2
Description: How many copies of the service task to run.
Resources:
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: !Ref 'HealthCheckIntervalSeconds'
HealthCheckPath: !Ref 'HealthCheckPath'
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
TargetType: ip
Name: !Ref 'ServiceName'
Port: !Ref 'ContainerPort'
Protocol: HTTP
UnhealthyThresholdCount: 2
VpcId:
Fn::ImportValue:
!Join [':', [!Ref 'StackName', 'VPCId']]
LoadBalancerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- TargetGroupArn: !Ref 'TargetGroup'
Type: 'forward'
Conditions:
- Field: path-pattern
Values: [!Ref 'Path']
ListenerArn:
Fn::ImportValue:
!Join [':', [!Ref 'StackName', 'PublicListener']]
Priority: 1
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Ref 'ServiceName'
RetentionInDays: 1
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Ref 'ServiceName'
Cpu: !Ref 'ContainerCpu'
Memory: !Ref 'ContainerMemory'
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
ExecutionRoleArn:
Fn::ImportValue:
!Join [':', [!Ref 'StackName', 'ECSTaskExecutionRole']]
ContainerDefinitions:
- Name: !Ref 'ServiceName'
Cpu: !Ref 'ContainerCpu'
Memory: !Ref 'ContainerMemory'
Image: !Ref 'ImageUrl'
PortMappings:
- ContainerPort: !Ref 'ContainerPort'
LogConfiguration:
LogDriver: 'awslogs'
Options:
awslogs-group: !Ref 'ServiceName'
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: !Ref 'ServiceName'
Service:
Type: AWS::ECS::Service
DependsOn: LoadBalancerRule
Properties:
ServiceName: !Ref 'ServiceName'
Cluster:
Fn::ImportValue:
!Join [':', [!Ref 'StackName', 'ClusterName']]
LaunchType: FARGATE
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 50
DesiredCount: !Ref 'DesiredCount'
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
SecurityGroups:
- Fn::ImportValue:
!Join [':', [!Ref 'StackName', 'ECSSecurityGroup']]
Subnets:
- Fn::ImportValue:
!Join [':', [!Ref 'StackName', 'PublicSubnetOne']]
- Fn::ImportValue:
!Join [':', [!Ref 'StackName', 'PublicSubnetTwo']]
TaskDefinition: !Ref 'TaskDefinition'
LoadBalancers:
- ContainerName: !Ref 'ServiceName'
ContainerPort: !Ref 'ContainerPort'
TargetGroupArn: !Ref 'TargetGroup'

View File

@@ -1,7 +0,0 @@
# Overview
![RDS in private subnet](rds-in-private-subnet.svg)
# Companion Blog Post
TO DO

View File

@@ -1,88 +0,0 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: A stack that creates an RDS instance and places it into two subnets
Parameters:
NetworkStackName:
Type: String
Description: The name of the networking stack that this stack will build upon.
DBInstanceClass:
Type: String
Description: The ID of the second subnet to place the RDS instance into.
Default: 'db.t2.micro'
DBName:
Type: String
Description: The name of the database that is created within the PostgreSQL instance.
DBUsername:
Type: String
Description: The master user name for the PostgreSQL instance.
Resources:
Secret:
Type: "AWS::SecretsManager::Secret"
Properties:
Name: !Ref 'DBUsername'
GenerateSecretString:
# This will generate a JSON object with the keys "username" and password.
SecretStringTemplate: !Join ['', ['{"username": "', !Ref 'DBUsername' ,'"}']]
GenerateStringKey: "password"
PasswordLength: 32
ExcludeCharacters: '"@/\'
DBSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: Subnet group for the RDS instance
DBSubnetGroupName: DBSubnetGroup
SubnetIds:
- Fn::ImportValue:
!Join [':', [!Ref 'NetworkStackName', 'PrivateSubnetOne']]
- Fn::ImportValue:
!Join [':', [!Ref 'NetworkStackName', 'PrivateSubnetTwo']]
PostgresInstance:
Type: AWS::RDS::DBInstance
Properties:
AllocatedStorage: 20
AvailabilityZone:
Fn::Select:
- 0
- Fn::GetAZs: {Ref: 'AWS::Region'}
DBInstanceClass: !Ref 'DBInstanceClass'
DBName: !Ref 'DBName'
DBSubnetGroupName: !Ref 'DBSubnetGroup'
Engine: postgres
EngineVersion: 11.5
MasterUsername: !Ref 'DBUsername'
MasterUserPassword: !Join ['', ['{{resolve:secretsmanager:', !Ref Secret, ':SecretString:password}}' ]]
PubliclyAccessible: false
VPCSecurityGroups:
- Fn::ImportValue:
!Join [':', [!Ref 'NetworkStackName', 'DBSecurityGroupId']]
SecretRDSInstanceAttachment:
Type: "AWS::SecretsManager::SecretTargetAttachment"
Properties:
SecretId: !Ref Secret
TargetId: !Ref PostgresInstance
TargetType: AWS::RDS::DBInstance
Outputs:
EndpointAddress:
Description: Address of the RDS endpoint.
Value: !GetAtt 'PostgresInstance.Endpoint.Address'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'EndpointAddress' ] ]
EndpointPort:
Description: Port of the RDS endpoint.
Value: !GetAtt 'PostgresInstance.Endpoint.Port'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'EndpointPort' ] ]
DBName:
Description: The name of the database that is created within the PostgreSQL instance.
Value: !Ref DBName
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'DBName' ] ]
Secret:
Description: Reference to the secret containing the password to the database.
Value: !Ref 'Secret'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'Secret' ] ]

View File

@@ -1,298 +0,0 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: A network stack for deploying containers in AWS ECS.
This stack creates a VPC with two public subnets and a loadbalancer to balance traffic between those subnets.
Derived from a template at https://github.com/nathanpeck/aws-cloudformation-fargate.
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: '10.0.0.0/16'
PublicSubnetOne:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone:
Fn::Select:
- 0
- Fn::GetAZs: {Ref: 'AWS::Region'}
VpcId: !Ref 'VPC'
CidrBlock: '10.0.1.0/24'
MapPublicIpOnLaunch: true
PublicSubnetTwo:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone:
Fn::Select:
- 1
- Fn::GetAZs: {Ref: 'AWS::Region'}
VpcId: !Ref 'VPC'
CidrBlock: '10.0.2.0/24'
MapPublicIpOnLaunch: true
InternetGateway:
Type: AWS::EC2::InternetGateway
GatewayAttachement:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref 'VPC'
InternetGatewayId: !Ref 'InternetGateway'
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref 'VPC'
PublicSubnetOneRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetOne
RouteTableId: !Ref PublicRouteTable
PublicSubnetTwoRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetTwo
RouteTableId: !Ref PublicRouteTable
PublicRoute:
Type: AWS::EC2::Route
DependsOn: GatewayAttachement
Properties:
RouteTableId: !Ref 'PublicRouteTable'
DestinationCidrBlock: '0.0.0.0/0'
GatewayId: !Ref 'InternetGateway'
PublicLoadBalancerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Access to the public facing load balancer
VpcId: !Ref 'VPC'
SecurityGroupIngress:
# Allow access to ALB from anywhere on the internet
- CidrIp: 0.0.0.0/0
IpProtocol: -1
PublicLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Scheme: internet-facing
Subnets:
# The load balancer is placed into the public subnets, so that traffic
# from the internet can reach the load balancer directly via the internet gateway
- !Ref PublicSubnetOne
- !Ref PublicSubnetTwo
SecurityGroups: [!Ref 'PublicLoadBalancerSecurityGroup']
DummyTargetGroupPublic:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 6
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
Name: "no-op"
Port: 80
Protocol: HTTP
UnhealthyThresholdCount: 2
VpcId: !Ref 'VPC'
PublicLoadBalancerListener:
Type: AWS::ElasticLoadBalancingV2::Listener
DependsOn:
- PublicLoadBalancer
Properties:
DefaultActions:
- TargetGroupArn: !Ref 'DummyTargetGroupPublic'
Type: 'forward'
LoadBalancerArn: !Ref 'PublicLoadBalancer'
Port: 80
Protocol: HTTP
ECSCluster:
Type: AWS::ECS::Cluster
ECSSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Access to the ECS containers
VpcId: !Ref 'VPC'
ECSSecurityGroupIngressFromPublicALB:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Ingress from the public ALB
GroupId: !Ref 'ECSSecurityGroup'
IpProtocol: -1
SourceSecurityGroupId: !Ref 'PublicLoadBalancerSecurityGroup'
ECSSecurityGroupIngressFromSelf:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Ingress from other containers in the same security group
GroupId: !Ref 'ECSSecurityGroup'
IpProtocol: -1
SourceSecurityGroupId: !Ref 'ECSSecurityGroup'
ECSRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: [ecs.amazonaws.com]
Action: ['sts:AssumeRole']
Path: /
Policies:
- PolicyName: ecs-service
PolicyDocument:
Statement:
- Effect: Allow
Action:
# Rules which allow ECS to attach network interfaces to instances
# on your behalf in order for awsvpc networking mode to work right
- 'ec2:AttachNetworkInterface'
- 'ec2:CreateNetworkInterface'
- 'ec2:CreateNetworkInterfacePermission'
- 'ec2:DeleteNetworkInterface'
- 'ec2:DeleteNetworkInterfacePermission'
- 'ec2:Describe*'
- 'ec2:DetachNetworkInterface'
# Rules which allow ECS to update load balancers on your behalf
# with the information sabout how to send traffic to your containers
- 'elasticloadbalancing:DeregisterInstancesFromLoadBalancer'
- 'elasticloadbalancing:DeregisterTargets'
- 'elasticloadbalancing:Describe*'
- 'elasticloadbalancing:RegisterInstancesWithLoadBalancer'
- 'elasticloadbalancing:RegisterTargets'
Resource: '*'
ECSTaskExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: [ecs-tasks.amazonaws.com]
Action: ['sts:AssumeRole']
Path: /
Policies:
- PolicyName: AmazonECSTaskExecutionRolePolicy
PolicyDocument:
Statement:
- Effect: Allow
Action:
# Allow the ECS Tasks to download images from ECR
- 'ecr:GetAuthorizationToken'
- 'ecr:BatchCheckLayerAvailability'
- 'ecr:GetDownloadUrlForLayer'
- 'ecr:BatchGetImage'
# Allow the ECS tasks to upload logs to CloudWatch
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
Resource: '*'
PrivateSubnetOne:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone:
Fn::Select:
- 0
- Fn::GetAZs: {Ref: 'AWS::Region'}
VpcId: !Ref 'VPC'
CidrBlock: '10.0.101.0/24'
MapPublicIpOnLaunch: false
PrivateSubnetTwo:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone:
Fn::Select:
- 1
- Fn::GetAZs: {Ref: 'AWS::Region'}
VpcId: !Ref 'VPC'
CidrBlock: '10.0.102.0/24'
MapPublicIpOnLaunch: false
DBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Access to the RDS instance
VpcId: !Ref 'VPC'
DBSecurityGroupIngressFromECS:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Ingress from the ECS containers to the RDS instance
GroupId: !Ref 'DBSecurityGroup'
IpProtocol: -1
SourceSecurityGroupId: !Ref 'ECSSecurityGroup'
Outputs:
PrivateSubnetOne:
Description: Private subnet one
Value: !Ref 'PrivateSubnetOne'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PrivateSubnetOne' ] ]
PrivateSubnetTwo:
Description: Private subnet two
Value: !Ref 'PrivateSubnetTwo'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PrivateSubnetTwo' ] ]
DBSecurityGroupId:
Description: ID of the security group that an RDS instance can be placed into.
Value: !Ref 'DBSecurityGroup'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'DBSecurityGroupId' ] ]
ClusterName:
Description: The name of the ECS cluster
Value: !Ref 'ECSCluster'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ClusterName' ] ]
ExternalUrl:
Description: The url of the external load balancer
Value: !Join ['', ['http://', !GetAtt 'PublicLoadBalancer.DNSName']]
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ExternalUrl' ] ]
ECSRole:
Description: The ARN of the ECS role
Value: !GetAtt 'ECSRole.Arn'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ECSRole' ] ]
ECSTaskExecutionRole:
Description: The ARN of the ECS role
Value: !GetAtt 'ECSTaskExecutionRole.Arn'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ECSTaskExecutionRole' ] ]
PublicListener:
Description: The ARN of the public load balancer's Listener
Value: !Ref PublicLoadBalancerListener
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicListener' ] ]
VPCId:
Description: The ID of the VPC that this stack is deployed in
Value: !Ref 'VPC'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'VPCId' ] ]
PublicSubnetOne:
Description: Public subnet one
Value: !Ref 'PublicSubnetOne'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicSubnetOne' ] ]
PublicSubnetTwo:
Description: Public subnet two
Value: !Ref 'PublicSubnetTwo'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicSubnetTwo' ] ]
ECSSecurityGroup:
Description: A security group used to allow ECS containers to receive traffic
Value: !Ref 'ECSSecurityGroup'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ECSSecurityGroup' ] ]

View File

@@ -1 +0,0 @@
<mxfile modified="2020-05-11T21:18:00.281Z" host="app.diagrams.net" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36" etag="d22WT4Cb11cg7E6o-Mdr" version="13.0.9" type="device"><diagram id="Ht1M8jgEwFfnCIfOTk4-" name="Page-1">7VvZcuI4FP0aHpuyJK+PYUk6VemaTNHTyxNlsGI8MRYtiy1fP5KRwZbEkkBI00OKqljXspZ7z7mLDA3UHi/uaDgZfSERThvQihYN1GlACJDj8H9CslxJPM9eCWKaRLLTRtBLXrAUWlI6TSKc1zoyQlKWTOrCIckyPGQ1WUgpmde7PZG0PuskjLEm6A3DVJd+TyI2klLgBpsbn3ESj+TUPvRWN8Zh2VnuJB+FEZlXRKjbQG1KCFtdjRdtnArllXpZPXe75e56YRRn7JAHHuJp6+UXX9i3PB/cv7TDr5/+/SRHmYXpVG74cTpIk6FY73SQYSbXzpalQiYkyVihVKfFP3zOttVw+J22aDWhowjUtlcXAL0lxqgL1LZXFwB1eKDMD9QFVgRaqza8pcxvVRbIP6hFpixNMtxew8/iwpiGUcLN0iYpoVyWkYxrrzVi45S3AL+cjxKGe5NwKLQ659ThsieSMUkAAMu2VLx4hgNoIq7Hi1hwrRnOc7sZUzKdFFPecwoY7/ZzPJzShC37m849RskzlsvNi0a52AZE0PZ9YIslJGlakXeDW9R1uXyGKUs4SW7SJBaTMiKGDWUrxU9MDMu3l2TxQ9HqIEtuyTRPFOYjHMnlSEjyKfBiK9bBmkHc9WAyxowueRf5AHIk6aTXQaU7mVc47EvZqEJf4IICIYX7kK4jXg+/YRe/kAQzk22Eh3Dxcu9977wMnl37S/SrP/hUeoIK2749tq8Mu3yGzSbDg3kkl3oiCt3ctLyWb6aQxhcDq7ZSCLh1CgFLp5BrGyiEAut4/hiDla0HK5rMQoav0eoP4tL/L1o5wQdHq0U6ve3jv2af7eXPX2zwzwu+m5pSwyvbrmz7Hdh2VGBT2QbgmdlmjG2OxrYO36zVk0Sz7grzqIQz5BWawZwbt+27VUWCrVZSYaXYZD2UYs0T+EAXKlZxDFYptV9LN8AJ0g1juo6utfHV//15/k+tjc/u/4xkA57Gtm67JzwgprOEQ+G3dn3HGcQ/wPUhU6X1bq5Pj0YN6KZivwN+EYuLm8mE+8KQJZxZ5T1a3nwgYaRLue7CbIhpeYcvbT2cZl+j86hxobTjQzjA6SPJk2ItqDMgjJHxXiYOucX4Wmqux+BGUDPc7LSf8o31B+U+NCdw6/gOsrd7uROgBdp1tEDXagKkl+aBjhcPvhNc/P1wuRfKLmKmG46FGbJBPik0oqLkjpcY83B5YSBJ5P76sVy+GRvuu2Jj7SU22PhYYARbnPrXMH8+zJhqVETwVsyvKVEo2PO7VpV8nYTygVb2zggVGlCN0rGcNg89htjxVPydAU77Upswn6zU8ZQsxDrMuQ7FOZnSIV5lOi3eNOU8eJifBmk2UGKWryPNM4Qs770iFtSzdQ1gOIpxqVkBBxKTLEy7G2nVLDiLbsT7Q2HclAyfhSgdFO3SxgVsQsrKfpLI/MnbJF17Cy0psZ2WK2DKE84sWiOL24Iuf1QbP0WD59ey2VlUb3aW1dYjpgnXo4DW7jRxBZJdepSU5fuKMdvv84VOdwKmmsSUR8gUpzyczuqvWE2QkMM9irpqAz6lgIeBq+Wlq33K56ovRZWhgFJ2Il/PcVeq0MYqYLre6BHIda/IPQC5ewHpfBwgoXVCRCrVAArOj0hTPndJiHw7iIKPAxGyAhVEb0OQXX7npkSQbTWtyh84CEzcECIJX3eTZ1vblw81DuxcpdYfOAqWVys4LbL1hHRXpXKRBQh8femx/mrS5sSr/LITOlHVqhYmgZ4uBk3D8a5jH58wGl9vwf1I4NZiMcW9vx94x/uM+6dMHERdFCxolOt4gF0PFV8aeL9S1LXtmsWBZ6/DWNXoUDe5v8N1HmXySy8Rtpplb1ZvSOp3keJDwh9Qwh+0m77/tgjoAnNsOXXMc3bHsNf2PzrmGd8s6qi/nsH8LmcwR70yVc9gkGs3TXH1nY5hjGC7+Fp2kbAf5dz8ulLJ8tamkBWNZaVhLmMVP/o23+3ovns7zz/EVSPrrZWK6g8PLHTPXZvY1hn8tP4C9MKoc/pjoNfyZ1f8Oyz3OQt/7KBekEOVBE0L2bYPPcd3oeW9MQdybGVYHh38wHUc4PHQgdR3PqdKidz6bhTq7O2/L4VS64ojUyje3PwCZ9V98zsm1P0P</diagram></mxfile>

View File

@@ -1,164 +0,0 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: Deploy a service on AWS Fargate, hosted in two public subnets and accessible via a public load balancer.
Derived from a template at https://github.com/nathanpeck/aws-cloudformation-fargate.
Parameters:
NetworkStackName:
Type: String
Description: The name of the networking stack that
these resources are put into.
DatabaseStackName:
Type: String
Description: The name of the database stack with the database this service should connect to.
ServiceName:
Type: String
Description: A human-readable name for the service.
HealthCheckPath:
Type: String
Default: /health
Description: Path to perform the healthcheck on each instance.
HealthCheckIntervalSeconds:
Type: Number
Default: 5
Description: Number of seconds to wait between each health check.
ImageUrl:
Type: String
Description: The url of a docker image that will handle incoming traffic.
ContainerPort:
Type: Number
Default: 80
Description: The port number the application inside the docker container
is binding to.
ContainerCpu:
Type: Number
Default: 256
Description: How much CPU to give the container. 1024 is 1 CPU.
ContainerMemory:
Type: Number
Default: 512
Description: How much memory in megabytes to give the container.
Path:
Type: String
Default: "*"
Description: A path on the public load balancer that this service
should be connected to.
DesiredCount:
Type: Number
Default: 2
Description: How many copies of the service task to run.
Resources:
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: !Ref 'HealthCheckIntervalSeconds'
HealthCheckPath: !Ref 'HealthCheckPath'
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
TargetType: ip
Name: !Ref 'ServiceName'
Port: !Ref 'ContainerPort'
Protocol: HTTP
UnhealthyThresholdCount: 2
VpcId:
Fn::ImportValue:
!Join [':', [!Ref 'NetworkStackName', 'VPCId']]
LoadBalancerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- TargetGroupArn: !Ref 'TargetGroup'
Type: 'forward'
Conditions:
- Field: path-pattern
Values: [!Ref 'Path']
ListenerArn:
Fn::ImportValue:
!Join [':', [!Ref 'NetworkStackName', 'PublicListener']]
Priority: 1
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Ref 'ServiceName'
RetentionInDays: 1
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Ref 'ServiceName'
Cpu: !Ref 'ContainerCpu'
Memory: !Ref 'ContainerMemory'
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
ExecutionRoleArn:
Fn::ImportValue:
!Join [':', [!Ref 'NetworkStackName', 'ECSTaskExecutionRole']]
ContainerDefinitions:
- Name: !Ref 'ServiceName'
Cpu: !Ref 'ContainerCpu'
Memory: !Ref 'ContainerMemory'
Image: !Ref 'ImageUrl'
Environment:
- Name: SPRING_DATASOURCE_URL
Value: !Join
- ''
- - 'jdbc:postgresql://'
- Fn::ImportValue: !Join [':', [!Ref 'DatabaseStackName', 'EndpointAddress']]
- ':'
- Fn::ImportValue: !Join [':', [!Ref 'DatabaseStackName', 'EndpointPort']]
- '/'
- Fn::ImportValue: !Join [':', [!Ref 'DatabaseStackName', 'DBName']]
- Name: SPRING_DATASOURCE_USERNAME
Value: !Join
- ''
- - '{{resolve:secretsmanager:'
- Fn::ImportValue: !Join [':', [!Ref 'DatabaseStackName', 'Secret']]
- ':SecretString:username}}'
- Name: SPRING_DATASOURCE_PASSWORD
Value: !Join
- ''
- - '{{resolve:secretsmanager:'
- Fn::ImportValue: !Join [':', [!Ref 'DatabaseStackName', 'Secret']]
- ':SecretString:password}}'
PortMappings:
- ContainerPort: !Ref 'ContainerPort'
LogConfiguration:
LogDriver: 'awslogs'
Options:
awslogs-group: !Ref 'ServiceName'
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: !Ref 'ServiceName'
Service:
Type: AWS::ECS::Service
DependsOn: LoadBalancerRule
Properties:
ServiceName: !Ref 'ServiceName'
Cluster:
Fn::ImportValue:
!Join [':', [!Ref 'NetworkStackName', 'ClusterName']]
LaunchType: FARGATE
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 50
DesiredCount: !Ref 'DesiredCount'
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
SecurityGroups:
- Fn::ImportValue:
!Join [':', [!Ref 'NetworkStackName', 'ECSSecurityGroup']]
Subnets:
- Fn::ImportValue:
!Join [':', [!Ref 'NetworkStackName', 'PublicSubnetOne']]
- Fn::ImportValue:
!Join [':', [!Ref 'NetworkStackName', 'PublicSubnetTwo']]
TaskDefinition: !Ref 'TaskDefinition'
LoadBalancers:
- ContainerName: !Ref 'ServiceName'
ContainerPort: !Ref 'ContainerPort'
TargetGroupArn: !Ref 'TargetGroup'

View File

@@ -1,101 +0,0 @@
#!/bin/bash
MAIN_DIR=$PWD
build_gradle_module() {
MODULE_PATH=$1
echo ""
echo "+++"
echo "+++ BUILDING MODULE $MODULE_PATH"
echo "+++"
cd $MODULE_PATH && {
chmod +x gradlew
./gradlew clean build
if [ $? -ne 0 ]
then
echo ""
echo "+++"
echo "+++ BUILDING MODULE $MODULE_PATH FAILED"
echo "+++"
exit 1
else
echo ""
echo "+++"
echo "+++ BUILDING MODULE $MODULE_PATH SUCCESSFUL"
echo "+++"
fi
cd $MAIN_DIR
}
}
build_maven_module() {
MODULE_PATH=$1
echo ""
echo "+++"
echo "+++ BUILDING MODULE $MODULE_PATH"
echo "+++"
cd $MODULE_PATH && {
chmod +x mvnw
./mvnw clean package
if [ $? -ne 0 ]
then
echo ""
echo "+++"
echo "+++ BUILDING MODULE $MODULE_PATH FAILED"
echo "+++"
exit 1
else
echo ""
echo "+++"
echo "+++ BUILDING MODULE $MODULE_PATH SUCCESSFUL"
echo "+++"
fi
cd $MAIN_DIR
}
}
build_maven_module "spring-boot/dependency-injection"
build_maven_module "spring-boot/spring-boot-openapi"
build_maven_module "spring-boot/data-migration/liquibase"
build_gradle_module "spring-boot/boundaries"
build_gradle_module "spring-boot/argumentresolver"
build_gradle_module "spring-data/spring-data-jdbc-converter"
build_gradle_module "solid"
build_gradle_module "spring-boot/data-migration/flyway"
build_gradle_module "reactive"
build_gradle_module "junit/assumptions"
build_gradle_module "logging"
build_gradle_module "pact/pact-feign-consumer"
# currently disabled since the consumer build won't run
# build_gradle_module "pact/pact-message-consumer"
# build_gradle_module "pact/pact-message-producer"
build_gradle_module "pact/pact-spring-provider"
build_gradle_module "patterns"
build_gradle_module "spring-boot/conditionals"
build_gradle_module "spring-boot/configuration"
build_gradle_module "spring-boot/mocking"
build_gradle_module "spring-boot/modular"
build_gradle_module "spring-boot/paging"
build_gradle_module "spring-boot/rabbitmq-event-brokering"
build_gradle_module "spring-boot/spring-boot-logging"
build_gradle_module "spring-boot/spring-boot-testing"
build_gradle_module "spring-boot/starter"
build_gradle_module "spring-boot/startup"
build_gradle_module "spring-boot/static"
build_gradle_module "spring-boot/validation"
build_gradle_module "spring-boot/profiles"
build_gradle_module "spring-boot/password-encoding"
build_gradle_module "spring-boot/testcontainers"
build_gradle_module "spring-cloud/feign-with-spring-data-rest"
build_gradle_module "spring-cloud/sleuth-downstream-service"
build_gradle_module "spring-cloud/sleuth-upstream-service"
build_gradle_module "spring-cloud/spring-cloud-contract-consumer"
build_gradle_module "spring-cloud/spring-cloud-contract-provider"
build_gradle_module "spring-data/spring-data-rest-associations"
build_gradle_module "spring-data/spring-data-rest-springfox"
build_gradle_module "tools/jacoco"
echo ""
echo "+++"
echo "+++ ALL MODULES SUCCESSFUL"
echo "+++"

View File

@@ -0,0 +1,15 @@
# Consumer-Driven-Contract Test for a Feign Consumer
This repo contains an example of consumer-driven-contract testing for a Feign client
that consumes a REST API provided by the module `pact-spring-data-rest-provider`.
The contract is created and verified with [Pact](https://docs.pact.io/).
## Companion Blog Post
The Companion Blog Post to this project can be found [here](https://reflectoring.io/consumer-driven-contracts-with-pact-feign-spring-data-rest/).
## Running the application
The interesting part in this code base is the class `ConsumerPactVerificationTest`.
You can run the tests with `gradlew test` on Windows or `./gradlew test` on Unix.

View File

@@ -0,0 +1,46 @@
buildscript {
ext {
springBootVersion = '1.5.4.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenLocal()
mavenCentral()
}
ext {
springCloudVersion = 'Dalston.SR2'
}
dependencies {
compile('org.springframework.cloud:spring-cloud-starter-feign')
// locking transitive guava version to work around "java.lang.IllegalAccessError: tried to access method com.google.common.collect.Lists.cartesianProduct"
compile('com.google.guava:guava:22.0')
compile('org.springframework.boot:spring-boot-starter-hateoas')
testCompile group: 'au.com.dius', name: 'pact-jvm-consumer-junit_2.11', version: '3.5.2'
testCompile('org.springframework.boot:spring-boot-starter-test')
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
bootRun{
jvmArgs = ["-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5006"]
}

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.2-bin.zip

View File

View File

@@ -0,0 +1,24 @@
package com.example.demo;
public class Address {
private long id;
private String street;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
}

View File

@@ -0,0 +1,19 @@
package com.example.demo;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@FeignClient(value = "addresses", path = "/addresses")
public interface AddressClient {
@RequestMapping(method = RequestMethod.GET, path = "/")
Resources<Address> getAddresses();
@RequestMapping(method = RequestMethod.GET, path = "/{id}")
Resource<Address> getAddress(@PathVariable("id") long id);
}

View File

@@ -0,0 +1,24 @@
package com.example.demo;
public class Customer {
private long id;
private String name;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,19 @@
package com.example.demo;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@FeignClient(value = "customers", path = "/customers_mto")
public interface CustomerClient {
@RequestMapping(method = RequestMethod.GET, value = "/")
Resources<Customer> getCustomers();
@RequestMapping(method = RequestMethod.GET, value = "/{id}")
Resource<Customer> getCustomer(@PathVariable("id") long id);
}

View File

@@ -0,0 +1,16 @@
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.hateoas.config.EnableHypermediaSupport;
@SpringBootApplication
@EnableFeignClients
@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

View File

@@ -0,0 +1,15 @@
package com.example.demo;
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfiguration {
@Bean
public Logger.Level logLevel(){
return Logger.Level.FULL;
}
}

View File

@@ -0,0 +1,17 @@
server.port: 8081
logging.level.com.example.demo.CustomerClient: DEBUG
logging.level.com.example.demo.AddressClient: DEBUG
logging.level.org.hibernate.SQL: DEBUG
customers:
ribbon:
eureka:
enabled: false
listOfServers: localhost:8080
addresses:
ribbon:
eureka:
enabled: false
listOfServers: localhost:8080

View File

@@ -0,0 +1,137 @@
package com.example.demo;
import au.com.dius.pact.consumer.Pact;
import au.com.dius.pact.consumer.PactProviderRuleMk2;
import au.com.dius.pact.consumer.PactVerification;
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
import au.com.dius.pact.model.RequestResponsePact;
import org.apache.http.entity.ContentType;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(properties = {
// overriding provider address
"addresses.ribbon.listOfServers: localhost:8888"
})
public class ConsumerPactVerificationTest {
@Rule
public PactProviderRuleMk2 stubProvider = new PactProviderRuleMk2("customerServiceProvider", "localhost", 8888, this);
@Autowired
private AddressClient addressClient;
@Pact(state = "a collection of 2 addresses", provider = "customerServiceProvider", consumer = "addressClient")
public RequestResponsePact createAddressCollectionResourcePact(PactDslWithProvider builder) {
return builder
.given("a collection of 2 addresses")
.uponReceiving("a request to the address collection resource")
.path("/addresses/")
.method("GET")
.willRespondWith()
.status(200)
.body("{\n" +
" \"_embedded\": {\n" +
" \"addresses\": [\n" +
" {\n" +
" \"street\": \"Elm Street\",\n" +
" \"_links\": {\n" +
" \"self\": {\n" +
" \"href\": \"http://localhost:8080/addresses/1\"\n" +
" },\n" +
" \"address\": {\n" +
" \"href\": \"http://localhost:8080/addresses/1\"\n" +
" },\n" +
" \"customer\": {\n" +
" \"href\": \"http://localhost:8080/addresses/1/customer\"\n" +
" }\n" +
" }\n" +
" },\n" +
" {\n" +
" \"street\": \"High Street\",\n" +
" \"_links\": {\n" +
" \"self\": {\n" +
" \"href\": \"http://localhost:8080/addresses/2\"\n" +
" },\n" +
" \"address\": {\n" +
" \"href\": \"http://localhost:8080/addresses/2\"\n" +
" },\n" +
" \"customer\": {\n" +
" \"href\": \"http://localhost:8080/addresses/2/customer\"\n" +
" }\n" +
" }\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"_links\": {\n" +
" \"self\": {\n" +
" \"href\": \"http://localhost:8080/addresses{?page,size,sort}\",\n" +
" \"templated\": true\n" +
" },\n" +
" \"profile\": {\n" +
" \"href\": \"http://localhost:8080/profile/addresses\"\n" +
" },\n" +
" \"search\": {\n" +
" \"href\": \"http://localhost:8080/addresses/search\"\n" +
" }\n" +
" },\n" +
" \"page\": {\n" +
" \"size\": 20,\n" +
" \"totalElements\": 2,\n" +
" \"totalPages\": 1,\n" +
" \"number\": 0\n" +
" }\n" +
"}", "application/hal+json")
.toPact();
}
@Pact(state = "a single address", provider = "customerServiceProvider", consumer = "addressClient")
public RequestResponsePact createAddressResourcePact(PactDslWithProvider builder) {
return builder
.given("a single address")
.uponReceiving("a request to the address resource")
.path("/addresses/1")
.method("GET")
.willRespondWith()
.status(200)
.body("{\n" +
" \"street\": \"Elm Street\",\n" +
" \"_links\": {\n" +
" \"self\": {\n" +
" \"href\": \"http://localhost:8080/addresses/1\"\n" +
" },\n" +
" \"address\": {\n" +
" \"href\": \"http://localhost:8080/addresses/1\"\n" +
" },\n" +
" \"customer\": {\n" +
" \"href\": \"http://localhost:8080/addresses/1/customer\"\n" +
" }\n" +
" }\n" +
"}", "application/hal+json")
.toPact();
}
@Test
@PactVerification(fragment = "createAddressCollectionResourcePact")
public void verifyAddressCollectionPact() {
Resources<Address> addresses = addressClient.getAddresses();
assertThat(addresses).hasSize(2);
}
@Test
@PactVerification(fragment = "createAddressResourcePact")
public void verifyAddressPact() {
Resource<Address> address = addressClient.getAddress(1L);
assertThat(address).isNotNull();
}
}

View File

@@ -1,4 +1,4 @@
package io.reflectoring.validation;
package com.example.demo;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -7,7 +7,7 @@ import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ValidationApplicationTests {
public class DemoApplicationTests {
@Test
public void contextLoads() {

View File

@@ -0,0 +1,16 @@
# Consumer-Driven-Contract Test for a Spring Data Rest Provider
This repo contains an example of consumer-driven-contract testing for a Spring
Data REST API provider. The corresponding consumer to the contract is
implemented in the module `pact-feign-consumer`.
The contract is created and verified with [Pact](https://docs.pact.io/).
## Companion Blog Post
The Companion Blog Post to this project can be found [here](https://reflectoring.io/consumer-driven-contracts-with-pact-feign-spring-data-rest/).
## Running the application
The interesting part in this code base is the class `ProviderPactVerificationTest`.
You can run the tests with `gradlew test` on Windows or `./gradlew test` on Unix.

View File

@@ -0,0 +1,35 @@
buildscript {
ext {
springBootVersion = '1.5.4.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-data-rest')
compile group: 'au.com.dius', name: 'pact-jvm-provider-junit_2.11', version: '3.5.2'
compile('com.h2database:h2:1.4.196')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
bootRun{
jvmArgs = ["-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005"]
}

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.2-bin.zip

View File

@@ -0,0 +1,43 @@
package com.example.demo;
import javax.persistence.*;
@Entity
public class Address {
@GeneratedValue
@Id
private Long id;
@Column
private String street;
@ManyToOne
private Customer customer;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
}

View File

@@ -0,0 +1,14 @@
package com.example.demo;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.hateoas.Resources;
import java.util.List;
@RepositoryRestResource(path = "addresses")
public interface AddressRepository extends PagingAndSortingRepository<Address, Long> {
List<Address> findByCustomerId(long customerId);
}

View File

@@ -0,0 +1,30 @@
package com.example.demo;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Customer {
@Id
@GeneratedValue
private long id;
@Column
private String name;
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,9 @@
package com.example.demo;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource(path = "customers")
public interface CustomerRepository extends CrudRepository<Customer, Long> {
}

View File

@@ -1,13 +1,12 @@
package io.reflectoring;
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
SpringApplication.run(DemoApplication.class, args);
}
}

View File

@@ -0,0 +1,7 @@
spring.datasource.url=jdbc:h2:mem:AZ;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
logging.level.org.hibernate.SQL=OFF

View File

@@ -0,0 +1,41 @@
package com.example.demo;
import au.com.dius.pact.provider.junit.PactRunner;
import au.com.dius.pact.provider.junit.Provider;
import au.com.dius.pact.provider.junit.State;
import au.com.dius.pact.provider.junit.loader.PactFolder;
import au.com.dius.pact.provider.junit.target.HttpTarget;
import au.com.dius.pact.provider.junit.target.Target;
import au.com.dius.pact.provider.junit.target.TestTarget;
import com.example.framework.DatabaseStateHolder;
import com.example.framework.SpringBootStarter;
import org.junit.ClassRule;
import org.junit.runner.RunWith;
@RunWith(PactRunner.class)
@Provider("customerServiceProvider")
@PactFolder("../pact-feign-consumer/target/pacts")
public class ProviderPactVerificationTest {
@ClassRule
public static SpringBootStarter appStarter = SpringBootStarter.builder()
.withApplicationClass(DemoApplication.class)
.withArgument("--spring.config.location=classpath:/application-pact.properties")
.withDatabaseState("single-address", "/initial-schema.sql", "/single-address.sql")
.withDatabaseState("address-collection", "/initial-schema.sql", "/address-collection.sql")
.build();
@State("a single address")
public void toSingleAddressState() {
DatabaseStateHolder.setCurrentDatabaseState("single-address");
}
@State("a collection of 2 addresses")
public void toAddressCollectionState() {
DatabaseStateHolder.setCurrentDatabaseState("address-collection");
}
@TestTarget
public final Target target = new HttpTarget(8080);
}

View File

@@ -0,0 +1,32 @@
package com.example.framework;
/**
* Defines a state of the database, which is defined by a set of SQL scripts.
*/
public class DatabaseState {
private final String stateName;
private final String[] sqlscripts;
/**
* Constructor.
*
* @param stateName unique name of this database state.
* @param sqlscripts paths to SQL scripts within the classpath. These scripts will be executed to put the
* database into the database state described by this object.
*/
public DatabaseState(String stateName, String... sqlscripts) {
this.stateName = stateName;
this.sqlscripts = sqlscripts;
}
public String getStateName() {
return stateName;
}
public String[] getSqlscripts() {
return sqlscripts;
}
}

View File

@@ -0,0 +1,36 @@
package com.example.framework;
/**
* Holds the current database state.
* <p/>
* TODO: replace the static state variable with a thread-safe alternative. Potentially use a special HTTP header
* that is intercepted by a Bean defined in {@link PactDatabaseStatesAutoConfiguration} and sets the database
* state in a {@link ThreadLocal} variable.
*/
public class DatabaseStateHolder {
private static String currentDatabaseState;
/**
* Sets the database to the state with the specified name.
* <p/>
* <strong>WARNING:</strong> the database state is not thread safe. If there are multiple threads accessing
* the database in different states at the same time, apocalypse will come!
*
* @param databaseStateName the name of the {@link DatabaseState} to put the database in.
*/
public static void setCurrentDatabaseState(String databaseStateName) {
currentDatabaseState = databaseStateName;
}
/**
* Returns the name of the current {@link DatabaseState}.
* <p/>
* <strong>WARNING:</strong> the database state is not thread safe. If there are multiple threads accessing
* the database in different states at the same time, apocalypse will come!
*/
public static String getCurrentDatabaseState() {
return currentDatabaseState;
}
}

View File

@@ -0,0 +1,50 @@
package com.example.framework;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import java.util.List;
/**
* Initializes a {@link DataSource} to a specified List of {@link DatabaseState}s. It is assumed that the {@link DataSource}
* can switch between several states.
*/
public class DatabaseStatesInitializer {
private final DataSource dataSource;
private final List<DatabaseState> databaseStates;
/**
* Constructor.
*
* @param dataSource the {@link DataSource} to execute SQL scripts against.
* @param databaseStates the {@link DatabaseState}s to create within the {@link DataSource}.
*/
public DatabaseStatesInitializer(DataSource dataSource, List<DatabaseState> databaseStates) {
this.dataSource = dataSource;
this.databaseStates = databaseStates;
}
/**
* Executes SQL scripts to initialize the {@link DataSource} with several states.
* <p/>
* For each {@link DatabaseState}, the {@link DatabaseStateHolder} will be called to set the {@link DataSource}
* into that state. Then, the SQL scripts of that {@link DatabaseState} are executed against the {@link DataSource}
* to initialize that state.
*/
@PostConstruct
public void initialize() {
for (DatabaseState databaseState : this.databaseStates) {
DatabaseStateHolder.setCurrentDatabaseState(databaseState.getStateName());
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
for (String script : databaseState.getSqlscripts()) {
populator.addScript(new ClassPathResource(script));
}
populator.execute(this.dataSource);
}
}
}

View File

@@ -0,0 +1,70 @@
package com.example.framework;
import au.com.dius.pact.provider.junit.PactRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* <p>
* {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration AutoConfiguration} that is activated when
* the pact-jvm-provider-junit module is in the classpath.
* </p>
* <p>
* This configuration provides a {@link DataSource} which allows to switch between multiple database states.
* Each database state is defined by a name and a set of SQL scripts which set the database into the desired state.
* The database states are configured via properties:
* <pre>
* pact.databaseStates.&lt;NAME&gt;=/path/to/script1.sql,/path/to/script2.sql,...
* </pre>
* The NAME of the databaseState can be used with {@link DatabaseStateHolder#setCurrentDatabaseState(String)}
* to set the {@link DataSource} into that state.
* </p>
*/
@Configuration
@ConditionalOnClass(PactRunner.class)
@EnableConfigurationProperties(PactProperties.class)
public class PactDatabaseStatesAutoConfiguration {
@Bean
public DataSource dataSource(PactProperties pactProperties) {
AbstractRoutingDataSource dataSource = new AbstractRoutingDataSource() {
@Override
protected Object determineCurrentLookupKey() {
return DatabaseStateHolder.getCurrentDatabaseState();
}
};
Map<Object, Object> targetDataSources = new HashMap<>();
dataSource.setTargetDataSources(targetDataSources);
// create a DataSource for each DatabaseState
for (DatabaseState databaseState : pactProperties.getDatabaseStatesList()) {
DataSource ds = DataSourceBuilder
.create()
.url(String.format("jdbc:h2:mem:%s;DB_CLOSE_ON_EXIT=FALSE", databaseState.getStateName()))
.driverClassName("org.h2.Driver")
.username("sa")
.password("")
.build();
targetDataSources.put(databaseState.getStateName(), ds);
DatabaseStateHolder.setCurrentDatabaseState(databaseState.getStateName());
}
return dataSource;
}
@Bean
public DatabaseStatesInitializer databaseStatesInitializer(DataSource routingDataSource, PactProperties pactProperties) {
return new DatabaseStatesInitializer(routingDataSource, pactProperties.getDatabaseStatesList());
}
}

View File

@@ -0,0 +1,64 @@
package com.example.framework;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Loads the properties "pact.databaseStates.&lt;NAME&gt;" into the Spring environment.
*/
@ConfigurationProperties("pact")
public class PactProperties {
private Map<String, String> databaseStates;
/**
* Retrieves a map with the names of the configured database states as keys and {@link DatabaseState} objects
* as values.
*/
public List<DatabaseState> getDatabaseStatesList() {
List<DatabaseState> databaseStatesList = new ArrayList<>();
for (Map.Entry<String, String> entry : databaseStates.entrySet()) {
String stateName = entry.getKey();
// When reading a property as a Map, as is done for databaseStates, Spring Boot automatically adds numeric
// keys and moves the actual key into the value, separated by a comma. Thus, we have all entries duplicated
// and have to remove the entries with numeric keys.
if (!stateName.matches("^[0-9]+$")) {
String sqlScriptsString = entry.getValue();
String[] sqlScripts = sqlScriptsString.split(",");
databaseStatesList.add(new DatabaseState(stateName, sqlScripts));
}
}
return databaseStatesList;
}
static List<String> toCommandLineArguments(List<DatabaseState> databaseStates) {
List<String> args = new ArrayList<>();
for (DatabaseState databaseState : databaseStates) {
String argString = String.format("--pact.databaseStates.%s=", databaseState.getStateName());
int i = 0;
for (String scriptPath : databaseState.getSqlscripts()) {
argString += String.format("%s", scriptPath);
if (i < databaseState.getSqlscripts().length - 1) {
argString += ",";
}
i++;
}
args.add(argString);
}
return args;
}
public Map<String, String> getDatabaseStates() {
return databaseStates;
}
public void setDatabaseStates(Map<String, String> databaseStates) {
this.databaseStates = databaseStates;
}
}

View File

@@ -0,0 +1,111 @@
package com.example.framework;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;
import java.util.ArrayList;
import java.util.List;
/**
* <p>
* Starts a Spring Boot application.
* </p>
* <p>
* When included in a JUnit test with the {@link org.junit.ClassRule} annotation as in the example below, the Spring Boot application will be
* started before any of the test methods are run.
* <pre>
* public class MyTest {
*
* &#064;ClassRule
* public static SpringBootStarter starter = SpringBootStarter.builder()
* .withApplicationClass(MyApplication.class)
* ...
* .build();
*
* &#064;Test
* public void test(){
* ...
* }
*
* }
* </pre>
* </p>
*/
public class SpringBootStarter implements TestRule {
private final Class<?> applicationClass;
private final List<String> args;
private final List<DatabaseState> databaseStates;
/**
* Constructor.
*
* @param applicationClass the Spring Boot application class.
* @param databaseStates list containing {@link DatabaseState} objects, each describing a database state
* in form of one or more SQL scripts.
* @param args the command line arguments the application should be started with.
*/
public SpringBootStarter(Class<?> applicationClass, List<DatabaseState> databaseStates, List<String> args) {
this.args = args;
this.applicationClass = applicationClass;
this.databaseStates = databaseStates;
}
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
List<String> args = new ArrayList<>();
args.addAll(SpringBootStarter.this.args);
args.addAll(PactProperties.toCommandLineArguments(SpringBootStarter.this.databaseStates));
ApplicationContext context = SpringApplication.run(SpringBootStarter.this.applicationClass, args.toArray(new String[]{}));
base.evaluate();
SpringApplication.exit(context);
}
};
}
/**
* Creates a builder that provides a fluent API to create a new {@link SpringBootStarter} instance.
*/
public static SpringBootStarterBuilder builder() {
return new SpringBootStarterBuilder();
}
public static class SpringBootStarterBuilder {
private Class<?> applicationClass;
private List<String> args = new ArrayList<>();
private List<DatabaseState> databaseStates = new ArrayList<>();
public SpringBootStarterBuilder withApplicationClass(Class<?> clazz) {
this.applicationClass = clazz;
return this;
}
public SpringBootStarterBuilder withArgument(String argument) {
this.args.add(argument);
return this;
}
public SpringBootStarterBuilder withDatabaseState(String stateName, String... sqlScripts) {
this.databaseStates.add(new DatabaseState(stateName, sqlScripts));
return this;
}
public SpringBootStarter build() {
return new SpringBootStarter(this.applicationClass, this.databaseStates, args);
}
}
}

View File

@@ -0,0 +1 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.framework.PactDatabaseStatesAutoConfiguration

View File

@@ -0,0 +1,2 @@
insert into address (id, street) values (1, 'Elm Street');
insert into address (id, street) values (2, 'High Street');

View File

@@ -0,0 +1,6 @@
server.port=8080
#spring.jpa.generate-ddl=false
spring.jpa.hibernate.ddl-auto=none
pact.databaseStates[0]=single-address,/initial-schema.sql,/single-address.sql
pact.databaseStates[1]=address-collection,/initial-schema.sql,/address-collection.sql

View File

@@ -0,0 +1,11 @@
create table CUSTOMER (
id NUMBER,
name VARCHAR
);
create table ADDRESS (
id NUMBER,
customer_id NUMBER,
street VARCHAR,
FOREIGN KEY (customer_id) REFERENCES CUSTOMER(id)
);

View File

@@ -0,0 +1 @@
insert into address (id, street) values (1, 'Elm Street');

View File

@@ -1,5 +1,6 @@
#Sun Jul 30 16:58:54 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.2-all.zip

View File

View File

@@ -7,7 +7,7 @@ buildscript {
apply plugin: 'java'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 11
sourceCompatibility = 1.8
repositories {
mavenLocal()
@@ -23,6 +23,3 @@ dependencies {
testCompile 'junit:junit:4.12'
}
test {
useJUnitPlatform()
}

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.2-bin.zip

View File

@@ -1,7 +0,0 @@
# Logging Code Examples
## Related Blog Articles
* [Use Logging Levels Consistently](https://reflectoring.io/logging-levels/)
* [Use a Human-Readable Logging Format](https://reflectoring.io/logging-format/)
* [Configuring a Human-Readable Logging Format with Logback and Descriptive Logger](https://reflectoring.io/logging-format-logback/)

View File

@@ -1,20 +0,0 @@
apply plugin: 'java'
buildscript {
repositories {
mavenLocal()
jcenter()
}
}
repositories {
mavenLocal()
jcenter()
}
dependencies {
compile("ch.qos.logback:logback-classic:1.2.3")
compile("io.reflectoring:descriptive-logger:1.0")
testCompile("org.junit.jupiter:junit-jupiter-engine:5.0.1")
}

View File

@@ -1,5 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-bin.zip

View File

@@ -1,18 +0,0 @@
package io.reflectoring.logging;
import io.reflectoring.descriptivelogger.LoggerFactory;
import org.junit.jupiter.api.Test;
public class LoggingFormatTest {
private MyLogger logger = LoggerFactory.getLogger(MyLogger.class, LoggingFormatTest.class);
@Test
public void testLogPattern(){
Thread.currentThread().setName("very-long-thread-name");
logger.logDebugMessage();
Thread.currentThread().setName("short");
logger.logInfoMessage();
logger.logMessageWithLongId();
}
}

View File

@@ -1,19 +0,0 @@
package io.reflectoring.logging;
import io.reflectoring.descriptivelogger.DescriptiveLogger;
import io.reflectoring.descriptivelogger.LogMessage;
import org.slf4j.event.Level;
@DescriptiveLogger
public interface MyLogger {
@LogMessage(level=Level.DEBUG, message="This is a DEBUG message.", id=14556)
void logDebugMessage();
@LogMessage(level=Level.INFO, message="This is an INFO message.", id=5456)
void logInfoMessage();
@LogMessage(level=Level.ERROR, message="This is an ERROR message with a very long ID.", id=1548654)
void logMessageWithLongId();
}

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<conversionRule conversionWord="truncatedThread"
converterClass="io.reflectoring.logging.TruncatedThreadConverter" />
<conversionRule conversionWord="truncatedLogger"
converterClass="io.reflectoring.logging.TruncatedLoggerConverter" />
<!-- Appender to log to console -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd} | %d{HH:mm:ss.SSS} | %-20.20thread | %5p | %-25.25logger{25} | %12(ID: %8mdc{id}) | %m%n</pattern>
<charset>utf8</charset>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>

View File

@@ -4,9 +4,6 @@ This example project shows how to setup an Angular application to use [Pact](htt
in order to create Pact files from a consumer test and validate the
a consumer against the Pact.
## Relevant Blog Post
[Creating a Consumer-Driven Contract with Angular and Pact](https://reflectoring.io/consumer-driven-contracts-with-angular-and-pact/)
## Key Files
* [`user.service.ts`](src/app/user.service.ts): Angular service that calls a REST

View File

@@ -1,11 +1,11 @@
# Testing a Spring Boot REST API Consumer against a Contract with Pact
# Creating a Consumer-Driven Contract with Feign and Pact
## Companion Blog Article
Read the [companion blog article](http://localhost:4000/consumer-driven-contract-feign-pact/) to this repository.
Read the [companion blog article](https://reflectoring.io/consumer-driven-contract-feign-pact/) to this repository.
## Getting Started
* have a look at the [feign client](src/main/java/io/reflectoring/UserClient.java)
* have a look at the [consumer test](src/test/java/io/reflectoring/UserServiceConsumerTest.java)
* run `./gradlew build` in this project to create a pact and run the consumer test
* afterwards, find the pact contract file in the folder `target/pacts`
* have a look at the [feign client](/src/main/java/io/reflectoring/UserClient.java)
* have a look at the [consumer test](/src/test/java/io/reflectoring/UserServiceConsumerTest.java)
* run `./gradlew build` to run all tests and create pact files into the folder `target/pacts`
* run `./gradlew pactPublish` to publish the pact files to a Pact Broker (must specify Pact Broker location and credentials in `build.gradle`)

View File

@@ -0,0 +1,5 @@
userservice:
ribbon:
eureka:
enabled: false
listOfServers: localhost:8080

View File

@@ -9,12 +9,10 @@ buildscript {
}
plugins {
id "au.com.dius.pact" version "3.5.20"
id "au.com.dius.pact" version "3.5.13"
}
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
version '1.0.0.SNAPSHOT'
@@ -23,41 +21,21 @@ repositories {
jcenter()
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springcloud_version}"
}
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.cloud:spring-cloud-starter-openfeign')
compile('org.springframework.cloud:spring-cloud-starter-netflix-ribbon')
compile("org.springframework.boot:spring-boot-starter-data-jpa:${springboot_version}")
compile("org.springframework.boot:spring-boot-starter-web:${springboot_version}")
compile("org.springframework.cloud:spring-cloud-starter-feign:1.4.1.RELEASE")
compile('com.h2database:h2:1.4.196')
// add jaxb since it's no longer available in Java 11
runtime('javax.xml.bind:jaxb-api:2.3.1')
// add javassist >= 3.23.1-GA since earlier versions are broken in Java 11
// see https://github.com/jboss-javassist/javassist/issues/194
runtime('org.javassist:javassist:3.23.1-GA')
testCompile('org.codehaus.groovy:groovy-all:2.4.6')
testCompile("au.com.dius:pact-jvm-consumer-junit5_2.12:${pact_version}")
testCompile('org.springframework.boot:spring-boot-starter-test')
testRuntimeOnly( 'org.junit.jupiter:junit-jupiter-engine:5.1.0')
testCompile("au.com.dius:pact-jvm-consumer-junit_2.11:3.5.2")
testCompile("org.springframework.boot:spring-boot-starter-test:${springboot_version}")
}
pact {
publish {
pactDirectory = 'target/pacts'
pactBrokerUrl = 'TODO'
pactBrokerUsername = 'TODO'
pactBrokerPassword = 'TODO'
pactBrokerUrl = 'URL'
pactBrokerUsername = 'USERNAME'
pactBrokerPassword = 'PASSWORD'
}
}
test {
useJUnitPlatform()
}

View File

@@ -1,3 +1,2 @@
springboot_version=2.0.4.RELEASE
springcloud_version=Finchley.SR1
pact_version=3.5.20
springboot_version=1.5.9.RELEASE
verifier_version=1.2.2.RELEASE

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.2-bin.zip

View File

@@ -2,12 +2,10 @@ package io.reflectoring;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
@RibbonClient(name = "userservice", configuration = RibbonConfiguration.class)
public class ConsumerApplication {
public static void main(String[] args) {

Some files were not shown because too many files have changed in this diff Show More