From f1c11281dc67e5c61a5d0c66cfdb6d6aca9c1eba Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Fri, 21 Jul 2017 09:26:37 -0600 Subject: [PATCH 01/16] Initial commit --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..4fa332f5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build +.gradle +.idea +*.iml +ci/variables.yml From 8f16c686a2e5cd1722e5b22a883684870ad8f413 Mon Sep 17 00:00:00 2001 From: Yuliya Dzikovich Date: Wed, 17 Jan 2018 07:53:55 -0800 Subject: [PATCH 02/16] Initial commit - basic spring boot app --- build.gradle | 12 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54333 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 172 ++++++++++++++++++ gradlew.bat | 84 +++++++++ settings.gradle | 1 + .../pal/tracker/PalTrackerApplication.java | 12 ++ .../pal/tracker/WelcomeController.java | 13 ++ 8 files changed, 299 insertions(+) create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java create mode 100644 src/main/java/io/pivotal/pal/tracker/WelcomeController.java diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..edb330bac --- /dev/null +++ b/build.gradle @@ -0,0 +1,12 @@ +plugins { + id "java" + id "org.springframework.boot" version "1.5.4.RELEASE" +} + +repositories { + mavenCentral() +} + +dependencies { + compile("org.springframework.boot:spring-boot-starter-web") +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..99340b4ad18d3c7e764794d300ffd35017036793 GIT binary patch literal 54333 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 D&=%~j literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..2c2bbe5f9 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..cccdd3d51 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..e95643d6a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..ef961960e --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = "pal-tracker" \ No newline at end of file diff --git a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java new file mode 100644 index 000000000..80f2a72a5 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java @@ -0,0 +1,12 @@ +package io.pivotal.pal.tracker; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class PalTrackerApplication { + + public static void main(String[] args) { + SpringApplication.run(PalTrackerApplication.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/io/pivotal/pal/tracker/WelcomeController.java b/src/main/java/io/pivotal/pal/tracker/WelcomeController.java new file mode 100644 index 000000000..2d477ae71 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/WelcomeController.java @@ -0,0 +1,13 @@ +package io.pivotal.pal.tracker; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class WelcomeController { + + @GetMapping("/") + public String sayHello() { + return "hello"; + } +} \ No newline at end of file From 8a37e14667527abae30eaaf72636c60a070b5eef Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Thu, 20 Jul 2017 13:56:50 -0600 Subject: [PATCH 03/16] Add tests for deployment lab --- .../pal/tracker/EnvControllerTest.java | 28 +++++++++++++++++++ .../pal/tracker/WelcomeControllerTest.java | 16 +++++++++++ .../pal/trackerapi/WelcomeApiTest.java | 26 +++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 src/test/java/test/pivotal/pal/tracker/EnvControllerTest.java create mode 100644 src/test/java/test/pivotal/pal/tracker/WelcomeControllerTest.java create mode 100644 src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java diff --git a/src/test/java/test/pivotal/pal/tracker/EnvControllerTest.java b/src/test/java/test/pivotal/pal/tracker/EnvControllerTest.java new file mode 100644 index 000000000..fda0f0f34 --- /dev/null +++ b/src/test/java/test/pivotal/pal/tracker/EnvControllerTest.java @@ -0,0 +1,28 @@ +package test.pivotal.pal.tracker; + +import org.junit.Test; + +import java.util.Map; +import io.pivotal.pal.tracker.EnvController; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EnvControllerTest { + @Test + public void getEnv() throws Exception { + EnvController controller = new EnvController( + "8675", + "12G", + "34", + "123.sesame.street" + ); + + Map env = controller.getEnv(); + + assertThat(env.get("PORT")).isEqualTo("8675"); + assertThat(env.get("MEMORY_LIMIT")).isEqualTo("12G"); + assertThat(env.get("CF_INSTANCE_INDEX")).isEqualTo("34"); + assertThat(env.get("CF_INSTANCE_ADDR")).isEqualTo("123.sesame.street"); + } + +} diff --git a/src/test/java/test/pivotal/pal/tracker/WelcomeControllerTest.java b/src/test/java/test/pivotal/pal/tracker/WelcomeControllerTest.java new file mode 100644 index 000000000..bfa8271a0 --- /dev/null +++ b/src/test/java/test/pivotal/pal/tracker/WelcomeControllerTest.java @@ -0,0 +1,16 @@ +package test.pivotal.pal.tracker; + +import io.pivotal.pal.tracker.WelcomeController; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class WelcomeControllerTest { + + @Test + public void itSaysHello() throws Exception { + WelcomeController controller = new WelcomeController("A welcome message"); + + assertThat(controller.sayHello()).isEqualTo("A welcome message"); + } +} diff --git a/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java new file mode 100644 index 000000000..cc7091ed4 --- /dev/null +++ b/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java @@ -0,0 +1,26 @@ +package test.pivotal.pal.trackerapi; + +import io.pivotal.pal.tracker.PalTrackerApplication; +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.boot.test.web.client.TestRestTemplate; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) +public class WelcomeApiTest { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + public void exampleTest() { + String body = this.restTemplate.getForObject("/", String.class); + assertThat(body).isEqualTo("Hello from test"); + } +} From 17c8d63cc914feaf98316b4b8bbae34439929fa6 Mon Sep 17 00:00:00 2001 From: Yuliya Dzikovich Date: Wed, 17 Jan 2018 10:10:09 -0800 Subject: [PATCH 04/16] deploy-a-spring-boot-app-to-cloud-foundry: added a manifest, and an env endpoint --- build.gradle | 11 +++++- manifest.yml | 7 ++++ .../io/pivotal/pal/tracker/EnvController.java | 39 +++++++++++++++++++ .../pal/tracker/WelcomeController.java | 9 ++++- 4 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 manifest.yml create mode 100644 src/main/java/io/pivotal/pal/tracker/EnvController.java diff --git a/build.gradle b/build.gradle index edb330bac..64c81fc43 100644 --- a/build.gradle +++ b/build.gradle @@ -9,4 +9,13 @@ repositories { dependencies { compile("org.springframework.boot:spring-boot-starter-web") -} \ No newline at end of file + testCompile("org.springframework.boot:spring-boot-starter-test") +} + +bootRun.environment([ + "WELCOME_MESSAGE": "hello", +]) + +test.environment([ + "WELCOME_MESSAGE": "Hello from test", +]) \ No newline at end of file diff --git a/manifest.yml b/manifest.yml new file mode 100644 index 000000000..7f06f91ee --- /dev/null +++ b/manifest.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: pal-tracker + path: build/libs/pal-tracker.jar + random-route: true + env: + WELCOME_MESSAGE: Hello from Cloud Foundry \ No newline at end of file diff --git a/src/main/java/io/pivotal/pal/tracker/EnvController.java b/src/main/java/io/pivotal/pal/tracker/EnvController.java new file mode 100644 index 000000000..2236b6e99 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/EnvController.java @@ -0,0 +1,39 @@ +package io.pivotal.pal.tracker; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; + +@RestController +public class EnvController { + + + private final String port; + private final String mem_limit; + private final String cf_instance_index; + private final String cf_instance_addr; + + public EnvController(@Value("${PORT:NOT SET}") String port, + @Value("${MEMORY_LIMIT:NOT SET}") String mem_limit, + @Value("${CF_INSTANCE_INDEX:NOT SET}") String cf_instance_index, + @Value("${CF_INSTANCE_ADDR:NOT SET}") String cf_instance_addr) { + + this.port = port; + this.mem_limit = mem_limit; + this.cf_instance_index = cf_instance_index; + this.cf_instance_addr = cf_instance_addr; + } + + @GetMapping("/env") + public Map getEnv() { + HashMap env = new HashMap<>(); + env.put("PORT", port); + env.put("MEMORY_LIMIT", mem_limit); + env.put("CF_INSTANCE_INDEX", cf_instance_index); + env.put("CF_INSTANCE_ADDR", cf_instance_addr); + return env; + } +} diff --git a/src/main/java/io/pivotal/pal/tracker/WelcomeController.java b/src/main/java/io/pivotal/pal/tracker/WelcomeController.java index 2d477ae71..8207e3c75 100644 --- a/src/main/java/io/pivotal/pal/tracker/WelcomeController.java +++ b/src/main/java/io/pivotal/pal/tracker/WelcomeController.java @@ -1,13 +1,20 @@ package io.pivotal.pal.tracker; +import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class WelcomeController { + private String message; + + public WelcomeController(@Value("${WELCOME_MESSAGE}") String message) { + this.message = message; + } + @GetMapping("/") public String sayHello() { - return "hello"; + return message; } } \ No newline at end of file From e07130e3fd7b184f6dd077727a801503882a4c63 Mon Sep 17 00:00:00 2001 From: Mike Gehard Date: Wed, 3 Jan 2018 16:17:48 -0700 Subject: [PATCH 05/16] Add deployment pipeline --- ci/build.yml | 24 ++++++++++++++ ci/pipeline.yml | 83 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 ci/build.yml create mode 100644 ci/pipeline.yml diff --git a/ci/build.yml b/ci/build.yml new file mode 100644 index 000000000..4ba28db53 --- /dev/null +++ b/ci/build.yml @@ -0,0 +1,24 @@ +platform: linux + +image_resource: + type: docker-image + source: + repository: openjdk + tag: '8-jdk' + +inputs: + - name: pal-tracker + - name: version + +outputs: + - name: build-output + +run: + path: bash + args: + - -exc + - | + cd pal-tracker + chmod +x gradlew + ./gradlew -P version=$(cat ../version/number) build + cp build/libs/pal-tracker-*.jar ../build-output diff --git a/ci/pipeline.yml b/ci/pipeline.yml new file mode 100644 index 000000000..c34c3b2a9 --- /dev/null +++ b/ci/pipeline.yml @@ -0,0 +1,83 @@ +--- +resources: +- name: pal-tracker + type: git + source: + uri: {{github-repository}} + branch: master + private_key: {{github-private-key}} + +- name: pal-tracker-artifacts + type: s3 + source: + bucket: {{aws-bucket}} + regexp: releases/pal-tracker-(.*).jar + access_key_id: {{aws-access-key-id}} + secret_access_key: {{aws-secret-access-key}} + +- name: version + type: semver + source: + bucket: {{aws-bucket}} + key: pal-tracker/version + access_key_id: {{aws-access-key-id}} + secret_access_key: {{aws-secret-access-key}} + +- name: review-deployment + type: cf + source: + api: {{cf-api-url}} + username: {{cf-username}} + password: {{cf-password}} + organization: {{cf-org}} + space: review + +- name: production-deployment + type: cf + source: + api: {{cf-api-url}} + username: {{cf-username}} + password: {{cf-password}} + organization: {{cf-org}} + space: production + +jobs: +- name: build + plan: + - get: pal-tracker + trigger: true + - get: version + params: {bump: patch} + - task: build and test + file: pal-tracker/ci/build.yml + - put: pal-tracker-artifacts + params: + file: build-output/pal-tracker-*.jar + - put: version + params: + file: version/number + +- name: deploy-review + plan: + - get: pal-tracker + - get: pal-tracker-artifacts + trigger: true + passed: [build] + - put: review-deployment + params: + manifest: pal-tracker/manifest-review.yml + path: pal-tracker-artifacts/pal-tracker-*.jar + environment_variables: + WELCOME_MESSAGE: "Hello from the review environment" + +- name: deploy-production + plan: + - get: pal-tracker + - get: pal-tracker-artifacts + passed: [deploy-review] + - put: production-deployment + params: + manifest: pal-tracker/manifest-production.yml + path: pal-tracker-artifacts/pal-tracker-*.jar + environment_variables: + WELCOME_MESSAGE: "Hello from the production environment" \ No newline at end of file From b999ac451c01922dfea695ba4175dea28784076b Mon Sep 17 00:00:00 2001 From: Yuliya Dzikovich Date: Wed, 17 Jan 2018 15:18:36 -0700 Subject: [PATCH 06/16] Added prod and review manifests --- manifest.yml => manifest-production.yml | 4 +--- manifest-review.yml | 5 +++++ 2 files changed, 6 insertions(+), 3 deletions(-) rename manifest.yml => manifest-production.yml (50%) create mode 100644 manifest-review.yml diff --git a/manifest.yml b/manifest-production.yml similarity index 50% rename from manifest.yml rename to manifest-production.yml index 7f06f91ee..1a021d516 100644 --- a/manifest.yml +++ b/manifest-production.yml @@ -2,6 +2,4 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar - random-route: true - env: - WELCOME_MESSAGE: Hello from Cloud Foundry \ No newline at end of file + host: yc-yd-pal-tracker diff --git a/manifest-review.yml b/manifest-review.yml new file mode 100644 index 000000000..967eb05c4 --- /dev/null +++ b/manifest-review.yml @@ -0,0 +1,5 @@ +--- +applications: +- name: pal-tracker + path: build/libs/pal-tracker.jar + host: yc-yd-pal-tracker-review From 7da923764e616a699b925d69b16d06c5f97e54fa Mon Sep 17 00:00:00 2001 From: Yuliya Dzikovich Date: Wed, 17 Jan 2018 16:10:27 -0700 Subject: [PATCH 07/16] Add slack integration --- ci/pipeline.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/ci/pipeline.yml b/ci/pipeline.yml index c34c3b2a9..35ef9ca21 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -1,4 +1,11 @@ --- +resource_types: +- name: slack-notification + type: docker-image + source: + repository: cfcommunity/slack-notification-resource + tag: latest + resources: - name: pal-tracker type: git @@ -41,6 +48,11 @@ resources: organization: {{cf-org}} space: production +- name: slack-alert + type: slack-notification + source: + url: https://hooks.slack.com/services/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 + jobs: - name: build plan: @@ -80,4 +92,7 @@ jobs: manifest: pal-tracker/manifest-production.yml path: pal-tracker-artifacts/pal-tracker-*.jar environment_variables: - WELCOME_MESSAGE: "Hello from the production environment" \ No newline at end of file + WELCOME_MESSAGE: "Hello from the production environment" + - put: slack-alert + params: + text: "Pal tracker was pushed to prod!" \ No newline at end of file From 41a32e63a87f39dec407c2440d773b38fa2f4525 Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Thu, 20 Jul 2017 15:04:20 -0600 Subject: [PATCH 08/16] Add tests for MVC lab --- .../InMemoryTimeEntryRepositoryTest.java | 72 ++++++++++ .../pal/tracker/TimeEntryControllerTest.java | 106 +++++++++++++++ .../pal/trackerapi/TimeEntryApiTest.java | 126 ++++++++++++++++++ 3 files changed, 304 insertions(+) create mode 100644 src/test/java/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.java create mode 100644 src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java create mode 100644 src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java diff --git a/src/test/java/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.java b/src/test/java/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.java new file mode 100644 index 000000000..03e804956 --- /dev/null +++ b/src/test/java/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.java @@ -0,0 +1,72 @@ +package test.pivotal.pal.tracker; + +import io.pivotal.pal.tracker.TimeEntryRepository; +import io.pivotal.pal.tracker.InMemoryTimeEntryRepository; +import io.pivotal.pal.tracker.TimeEntry; +import org.junit.Test; + +import java.time.LocalDate; +import java.util.List; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +public class InMemoryTimeEntryRepositoryTest { + @Test + public void create() throws Exception { + TimeEntryRepository repo = new InMemoryTimeEntryRepository(); + TimeEntry createdTimeEntry = repo.create(new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8)); + + TimeEntry expected = new TimeEntry(1L, 123, 456, LocalDate.parse("2017-01-08"), 8); + assertThat(createdTimeEntry).isEqualTo(expected); + + TimeEntry readEntry = repo.find(createdTimeEntry.getId()); + assertThat(readEntry).isEqualTo(expected); + } + + @Test + public void find() throws Exception { + TimeEntryRepository repo = new InMemoryTimeEntryRepository(); + repo.create(new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8)); + + TimeEntry expected = new TimeEntry(1L, 123, 456, LocalDate.parse("2017-01-08"), 8); + TimeEntry readEntry = repo.find(1L); + assertThat(readEntry).isEqualTo(expected); + } + + @Test + public void list() throws Exception { + TimeEntryRepository repo = new InMemoryTimeEntryRepository(); + repo.create(new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8)); + repo.create(new TimeEntry(789, 654, LocalDate.parse("2017-01-07"), 4)); + + List expected = asList( + new TimeEntry(1L, 123, 456, LocalDate.parse("2017-01-08"), 8), + new TimeEntry(2L, 789, 654, LocalDate.parse("2017-01-07"), 4) + ); + assertThat(repo.list()).isEqualTo(expected); + } + + @Test + public void update() throws Exception { + TimeEntryRepository repo = new InMemoryTimeEntryRepository(); + TimeEntry created = repo.create(new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8)); + + TimeEntry updatedEntry = repo.update( + created.getId(), + new TimeEntry(321, 654, LocalDate.parse("2017-01-09"), 5)); + + TimeEntry expected = new TimeEntry(created.getId(), 321, 654, LocalDate.parse("2017-01-09"), 5); + assertThat(updatedEntry).isEqualTo(expected); + assertThat(repo.find(created.getId())).isEqualTo(expected); + } + + @Test + public void delete() throws Exception { + TimeEntryRepository repo = new InMemoryTimeEntryRepository(); + TimeEntry created = repo.create(new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8)); + + repo.delete(created.getId()); + assertThat(repo.list()).isEmpty(); + } +} diff --git a/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java b/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java new file mode 100644 index 000000000..f7c0090e3 --- /dev/null +++ b/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java @@ -0,0 +1,106 @@ +package test.pivotal.pal.tracker; + +import io.pivotal.pal.tracker.TimeEntry; +import io.pivotal.pal.tracker.TimeEntryController; +import io.pivotal.pal.tracker.TimeEntryRepository; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.time.LocalDate; +import java.util.List; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.*; + +public class TimeEntryControllerTest { + private TimeEntryRepository timeEntryRepository; + private TimeEntryController controller; + + @Before + public void setUp() throws Exception { + timeEntryRepository = mock(TimeEntryRepository.class); + controller = new TimeEntryController(timeEntryRepository); + } + + @Test + public void testCreate() throws Exception { + TimeEntry expected = new TimeEntry(1L, 123, 456, LocalDate.parse("2017-01-08"), 8); + doReturn(expected) + .when(timeEntryRepository) + .create(any(TimeEntry.class)); + + ResponseEntity response = controller.create(new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8)); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat(response.getBody()).isEqualTo(expected); + } + + @Test + public void testRead() throws Exception { + TimeEntry expected = new TimeEntry(1L, 123, 456, LocalDate.parse("2017-01-08"), 8); + doReturn(expected) + .when(timeEntryRepository) + .find(1L); + + ResponseEntity response = controller.read(1L); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(expected); + } + + @Test + public void testRead_NotFound() throws Exception { + doReturn(null) + .when(timeEntryRepository) + .find(1L); + + ResponseEntity response = controller.read(1L); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + public void testList() throws Exception { + List expected = asList( + new TimeEntry(1, 123, 456, LocalDate.parse("2017-01-08"), 8), + new TimeEntry(2, 789, 321, LocalDate.parse("2017-01-07"), 4) + ); + doReturn(expected).when(timeEntryRepository).list(); + + ResponseEntity> response = controller.list(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(expected); + } + + @Test + public void testUpdate() throws Exception { + TimeEntry expected = new TimeEntry(1, 987, 654, LocalDate.parse("2017-01-07"), 4); + doReturn(expected) + .when(timeEntryRepository) + .update(eq(1L), any(TimeEntry.class)); + + ResponseEntity response = controller.update(1L, expected); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(expected); + } + + @Test + public void testUpdate_NotFound() throws Exception { + doReturn(null) + .when(timeEntryRepository) + .update(eq(1L), any(TimeEntry.class)); + + ResponseEntity response = controller.update(1L, new TimeEntry()); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + public void testDelete() throws Exception { + ResponseEntity response = controller.delete(1L); + verify(timeEntryRepository).delete(1L); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + } +} diff --git a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java new file mode 100644 index 000000000..b742e5447 --- /dev/null +++ b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java @@ -0,0 +1,126 @@ +package test.pivotal.pal.trackerapi; + +import com.jayway.jsonpath.DocumentContext; +import io.pivotal.pal.tracker.PalTrackerApplication; +import io.pivotal.pal.tracker.TimeEntry; +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.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringRunner; + +import java.time.LocalDate; +import java.util.Collection; + +import static com.jayway.jsonpath.JsonPath.parse; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) +public class TimeEntryApiTest { + + @Autowired + private TestRestTemplate restTemplate; + + private TimeEntry timeEntry = new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8); + + @Test + public void testCreate() throws Exception { + ResponseEntity createResponse = restTemplate.postForEntity("/time-entries", timeEntry, String.class); + + + assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED); + + DocumentContext createJson = parse(createResponse.getBody()); + assertThat(createJson.read("$.id", Long.class)).isGreaterThan(0); + assertThat(createJson.read("$.projectId", Long.class)).isEqualTo(123L); + assertThat(createJson.read("$.userId", Long.class)).isEqualTo(456L); + assertThat(createJson.read("$.date", String.class)).isEqualTo("2017-01-08"); + assertThat(createJson.read("$.hours", Long.class)).isEqualTo(8); + } + + @Test + public void testList() throws Exception { + Long id = createTimeEntry(); + + + ResponseEntity listResponse = restTemplate.getForEntity("/time-entries", String.class); + + + assertThat(listResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + + DocumentContext listJson = parse(listResponse.getBody()); + + Collection timeEntries = listJson.read("$[*]", Collection.class); + assertThat(timeEntries.size()).isEqualTo(1); + + Long readId = listJson.read("$[0].id", Long.class); + assertThat(readId).isEqualTo(id); + } + + @Test + public void testRead() throws Exception { + Long id = createTimeEntry(); + + + ResponseEntity readResponse = this.restTemplate.getForEntity("/time-entries/" + id, String.class); + + + assertThat(readResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + DocumentContext readJson = parse(readResponse.getBody()); + assertThat(readJson.read("$.id", Long.class)).isEqualTo(id); + assertThat(readJson.read("$.projectId", Long.class)).isEqualTo(123L); + assertThat(readJson.read("$.userId", Long.class)).isEqualTo(456L); + assertThat(readJson.read("$.date", String.class)).isEqualTo("2017-01-08"); + assertThat(readJson.read("$.hours", Long.class)).isEqualTo(8); + } + + @Test + public void testUpdate() throws Exception { + Long id = createTimeEntry(); + TimeEntry updatedTimeEntry = new TimeEntry(2, 3, LocalDate.parse("2017-01-09"), 9); + + + ResponseEntity updateResponse = restTemplate.exchange("/time-entries/" + id, HttpMethod.PUT, new HttpEntity<>(updatedTimeEntry, null), String.class); + + + assertThat(updateResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + + DocumentContext updateJson = parse(updateResponse.getBody()); + assertThat(updateJson.read("$.id", Long.class)).isEqualTo(id); + assertThat(updateJson.read("$.projectId", Long.class)).isEqualTo(2L); + assertThat(updateJson.read("$.userId", Long.class)).isEqualTo(3L); + assertThat(updateJson.read("$.date", String.class)).isEqualTo("2017-01-09"); + assertThat(updateJson.read("$.hours", Long.class)).isEqualTo(9); + } + + @Test + public void testDelete() throws Exception { + Long id = createTimeEntry(); + + + ResponseEntity deleteResponse = restTemplate.exchange("/time-entries/" + id, HttpMethod.DELETE, null, String.class); + + + assertThat(deleteResponse.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + + ResponseEntity deletedReadResponse = this.restTemplate.getForEntity("/time-entries/" + id, String.class); + assertThat(deletedReadResponse.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + private Long createTimeEntry() { + HttpEntity entity = new HttpEntity<>(timeEntry); + + ResponseEntity response = restTemplate.exchange("/time-entries", HttpMethod.POST, entity, TimeEntry.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + + return response.getBody().getId(); + } +} From 0224d85f01b85cac21f430f3546b1caec680dbc7 Mon Sep 17 00:00:00 2001 From: Yuliya Dzikovich Date: Thu, 18 Jan 2018 10:57:58 -0700 Subject: [PATCH 09/16] Added MVC structure to add/create/update/delete TimeEntries. Storing them in an InMemory DB for now --- build.gradle | 1 + .../tracker/InMemoryTimeEntryRepository.java | 45 ++++++++++++ .../pal/tracker/PalTrackerApplication.java | 20 +++++ .../io/pivotal/pal/tracker/TimeEntry.java | 73 +++++++++++++++++++ .../pal/tracker/TimeEntryController.java | 52 +++++++++++++ .../pal/tracker/TimeEntryRepository.java | 15 ++++ .../InMemoryTimeEntryRepositoryTest.java | 19 +++++ 7 files changed, 225 insertions(+) create mode 100644 src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java create mode 100644 src/main/java/io/pivotal/pal/tracker/TimeEntry.java create mode 100644 src/main/java/io/pivotal/pal/tracker/TimeEntryController.java create mode 100644 src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java diff --git a/build.gradle b/build.gradle index 64c81fc43..bde7f2f19 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,7 @@ repositories { dependencies { compile("org.springframework.boot:spring-boot-starter-web") + compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.1") testCompile("org.springframework.boot:spring-boot-starter-test") } diff --git a/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java b/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java new file mode 100644 index 000000000..0fb16bf13 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java @@ -0,0 +1,45 @@ +package io.pivotal.pal.tracker; + +import java.util.ArrayList; +import java.util.List; + +public class InMemoryTimeEntryRepository implements TimeEntryRepository { + + private List timeEntries = new ArrayList(); + private Long runningIdCounter = 1L; + + @Override + public TimeEntry create(TimeEntry timeEntry) { + timeEntry.setId(runningIdCounter++); + timeEntries.add(timeEntry); + return timeEntry; + } + + @Override + public TimeEntry find(long id) { + return timeEntries.stream().filter(timeEntry -> timeEntry.getId() == id).findFirst().orElse(null); + } + + @Override + public List list() { + return timeEntries; + } + + @Override + public TimeEntry update(long id, TimeEntry updatedTimeEntry) { + TimeEntry existingEntry = find(id); + if(existingEntry == null) + return null; + + updatedTimeEntry.setId(id); + timeEntries.replaceAll( timeEntry -> timeEntry.getId() == id ? updatedTimeEntry : timeEntry); + + return updatedTimeEntry; + } + + @Override + public void delete(long id) { + TimeEntry savedTimeEntry = this.find(id); + timeEntries.remove(savedTimeEntry); + } +} diff --git a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java index 80f2a72a5..f8230c75d 100644 --- a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java +++ b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java @@ -1,7 +1,13 @@ package io.pivotal.pal.tracker; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; @SpringBootApplication public class PalTrackerApplication { @@ -9,4 +15,18 @@ public class PalTrackerApplication { public static void main(String[] args) { SpringApplication.run(PalTrackerApplication.class, args); } + + @Bean + public TimeEntryRepository getInMemoryTimeEntryRepository(){ + return new InMemoryTimeEntryRepository(); + } + + @Bean + public ObjectMapper getObjectMapper(){ + return Jackson2ObjectMapperBuilder.json() + .serializationInclusion(JsonInclude.Include.NON_NULL) // Don’t include null values + .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) //ISODate + .modules(new JavaTimeModule()) + .build(); + } } \ No newline at end of file diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntry.java b/src/main/java/io/pivotal/pal/tracker/TimeEntry.java new file mode 100644 index 000000000..f6b2e6a5f --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntry.java @@ -0,0 +1,73 @@ +package io.pivotal.pal.tracker; + +import java.time.LocalDate; +import java.util.Objects; + +public class TimeEntry { + private long id; + private long projectId; + private long userId; + private LocalDate date; + private int hours; + + public TimeEntry(long projectId, long userId, LocalDate date, int hours) { + this.projectId = projectId; + this.userId = userId; + this.date = date; + this.hours = hours; + } + + public TimeEntry(long id, long projectId, long userId, LocalDate date, int hours) { + this.id = id; + this.projectId = projectId; + this.userId = userId; + this.date = date; + this.hours = hours; + } + + public TimeEntry() { + + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public long getProjectId() { + return projectId; + } + + public long getUserId() { + return userId; + } + + public LocalDate getDate() { + return date; + } + + public int getHours() { + return hours; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TimeEntry timeEntry = (TimeEntry) o; + return id == timeEntry.id && + projectId == timeEntry.projectId && + userId == timeEntry.userId && + hours == timeEntry.hours && + Objects.equals(date, timeEntry.date); + } + + @Override + public int hashCode() { + + return Objects.hash(id, projectId, userId, date, hours); + } +} diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java b/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java new file mode 100644 index 000000000..613e21f09 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java @@ -0,0 +1,52 @@ +package io.pivotal.pal.tracker; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +public class TimeEntryController { + private TimeEntryRepository timeEntryRepository; + + public TimeEntryController(TimeEntryRepository timeEntryRepository) { + this.timeEntryRepository = timeEntryRepository; + } + + @RequestMapping(value = "/time-entries", method = RequestMethod.POST) + public ResponseEntity create(@RequestBody TimeEntry timeEntry) { + TimeEntry createdTimeEntry = timeEntryRepository.create(timeEntry); + return new ResponseEntity(createdTimeEntry, HttpStatus.CREATED); + } + + @RequestMapping(value = "/time-entries/{id}", method = RequestMethod.GET) + public ResponseEntity read(@PathVariable long id) { + TimeEntry timeEntry = timeEntryRepository.find(id); + if (timeEntry == null) + return new ResponseEntity(HttpStatus.NOT_FOUND); + + return new ResponseEntity(timeEntry, HttpStatus.OK); + } + + @RequestMapping(value = "/time-entries", method = RequestMethod.GET) + public ResponseEntity> list() { + List timeEntries = timeEntryRepository.list(); + return new ResponseEntity(timeEntries, HttpStatus.OK); + } + + @RequestMapping(value = "/time-entries/{id}", method = RequestMethod.PUT) + public ResponseEntity update(@PathVariable long id, @RequestBody TimeEntry timeEntry) { + TimeEntry updatedTimeEntry = timeEntryRepository.update(id, timeEntry); + if (updatedTimeEntry == null) + return new ResponseEntity(HttpStatus.NOT_FOUND); + + return new ResponseEntity(updatedTimeEntry, HttpStatus.OK); + } + + @RequestMapping(value = "/time-entries/{id}", method = RequestMethod.DELETE) + public ResponseEntity delete(@PathVariable long id) { + timeEntryRepository.delete(id); + return new ResponseEntity(HttpStatus.NO_CONTENT); + } +} diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java b/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java new file mode 100644 index 000000000..233663424 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java @@ -0,0 +1,15 @@ +package io.pivotal.pal.tracker; + +import java.util.List; + +public interface TimeEntryRepository { + public TimeEntry create(TimeEntry timeEntry); + + public TimeEntry find(long id); + + public List list(); + + public TimeEntry update(long id, TimeEntry timeEntry); + + public void delete(long id); +} diff --git a/src/test/java/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.java b/src/test/java/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.java index 03e804956..6bd182c4b 100644 --- a/src/test/java/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.java +++ b/src/test/java/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.java @@ -34,6 +34,14 @@ public void find() throws Exception { assertThat(readEntry).isEqualTo(expected); } + @Test + public void find_notFound() throws Exception { + TimeEntryRepository repo = new InMemoryTimeEntryRepository(); + + TimeEntry readEntry = repo.find(1L); + assertThat(readEntry).isEqualTo(null); + } + @Test public void list() throws Exception { TimeEntryRepository repo = new InMemoryTimeEntryRepository(); @@ -61,6 +69,17 @@ public void update() throws Exception { assertThat(repo.find(created.getId())).isEqualTo(expected); } + @Test + public void update_notFound() throws Exception { + TimeEntryRepository repo = new InMemoryTimeEntryRepository(); + + TimeEntry updatedEntry = repo.update( + 1L, + new TimeEntry(321, 654, LocalDate.parse("2017-01-09"), 5)); + + assertThat(updatedEntry).isEqualTo(null); + } + @Test public void delete() throws Exception { TimeEntryRepository repo = new InMemoryTimeEntryRepository(); From b90ebb142bae9f1b8ed864735f11a32950b8f732 Mon Sep 17 00:00:00 2001 From: Yuliya Dzikovich Date: Thu, 18 Jan 2018 15:07:21 -0700 Subject: [PATCH 10/16] Remove slack notification from the pipeline --- ci/pipeline.yml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/ci/pipeline.yml b/ci/pipeline.yml index 35ef9ca21..928173bf6 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -1,11 +1,4 @@ --- -resource_types: -- name: slack-notification - type: docker-image - source: - repository: cfcommunity/slack-notification-resource - tag: latest - resources: - name: pal-tracker type: git @@ -48,11 +41,6 @@ resources: organization: {{cf-org}} space: production -- name: slack-alert - type: slack-notification - source: - url: https://hooks.slack.com/services/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 - jobs: - name: build plan: @@ -93,6 +81,3 @@ jobs: path: pal-tracker-artifacts/pal-tracker-*.jar environment_variables: WELCOME_MESSAGE: "Hello from the production environment" - - put: slack-alert - params: - text: "Pal tracker was pushed to prod!" \ No newline at end of file From d1a42f069babd922bbea38a3709236446778e189 Mon Sep 17 00:00:00 2001 From: Yuliya Dzikovich Date: Thu, 18 Jan 2018 15:22:13 -0700 Subject: [PATCH 11/16] Minor refactoring for the TimeEntryController --- .../io/pivotal/pal/tracker/TimeEntryController.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java b/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java index 613e21f09..0fbdfafa2 100644 --- a/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java @@ -7,6 +7,7 @@ import java.util.List; @RestController +@RequestMapping("/time-entries") public class TimeEntryController { private TimeEntryRepository timeEntryRepository; @@ -14,13 +15,13 @@ public TimeEntryController(TimeEntryRepository timeEntryRepository) { this.timeEntryRepository = timeEntryRepository; } - @RequestMapping(value = "/time-entries", method = RequestMethod.POST) + @PostMapping public ResponseEntity create(@RequestBody TimeEntry timeEntry) { TimeEntry createdTimeEntry = timeEntryRepository.create(timeEntry); return new ResponseEntity(createdTimeEntry, HttpStatus.CREATED); } - @RequestMapping(value = "/time-entries/{id}", method = RequestMethod.GET) + @GetMapping("/{id}") public ResponseEntity read(@PathVariable long id) { TimeEntry timeEntry = timeEntryRepository.find(id); if (timeEntry == null) @@ -29,13 +30,13 @@ public ResponseEntity read(@PathVariable long id) { return new ResponseEntity(timeEntry, HttpStatus.OK); } - @RequestMapping(value = "/time-entries", method = RequestMethod.GET) + @GetMapping public ResponseEntity> list() { List timeEntries = timeEntryRepository.list(); return new ResponseEntity(timeEntries, HttpStatus.OK); } - @RequestMapping(value = "/time-entries/{id}", method = RequestMethod.PUT) + @PutMapping("/{id}") public ResponseEntity update(@PathVariable long id, @RequestBody TimeEntry timeEntry) { TimeEntry updatedTimeEntry = timeEntryRepository.update(id, timeEntry); if (updatedTimeEntry == null) @@ -44,7 +45,7 @@ public ResponseEntity update(@PathVariable long id, @RequestBody TimeEntry timeE return new ResponseEntity(updatedTimeEntry, HttpStatus.OK); } - @RequestMapping(value = "/time-entries/{id}", method = RequestMethod.DELETE) + @DeleteMapping("/{id}") public ResponseEntity delete(@PathVariable long id) { timeEntryRepository.delete(id); return new ResponseEntity(HttpStatus.NO_CONTENT); From 752d63bffa1b7501a9afb376129b0bbb2542a7f2 Mon Sep 17 00:00:00 2001 From: Mike Gehard Date: Tue, 9 Jan 2018 09:47:21 -0700 Subject: [PATCH 12/16] Add task for migrating databases --- ci/migrateDatabase.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 ci/migrateDatabase.yml diff --git a/ci/migrateDatabase.yml b/ci/migrateDatabase.yml new file mode 100644 index 000000000..b2af8c5ef --- /dev/null +++ b/ci/migrateDatabase.yml @@ -0,0 +1,29 @@ +platform: linux + +image_resource: + type: docker-image + source: + repository: openjdk + tag: '8-jdk' + +inputs: + - name: pal-tracker +# - name: version + +#outputs: +# - name: build-output + +run: + path: bash + args: + - -exc + - | + cd pal-tracker + curl -L "https://packages.cloudfoundry.org/stable?release=linux64-binary&source=github" | tar -zx + chmod +x cf + curl -L "https://repo1.maven.org/maven2/org/flywaydb/flyway-commandline/5.0.5/flyway-commandline-5.0.5-linux-x64.tar.gz" | tar -zx + chmod +x flyway-5.0.5/flyway + ./cf login -a $CF_API_URL -u $CF_USERNAME -p $CF_PASSWORD -o $CF_ORG -s $CF_SPACE + ./cf ssh -N -L 63306:$MYSQL_IP:3306 pal-tracker & + sleep 2 + ./flyway-5.0.5/flyway -url="jdbc:mysql://127.0.0.1:63306/$DATABASE_NAME" -locations=filesystem:databases/tracker -user=$DATABASE_USERNAME -password=$DATABASE_PASSWORD clean migrate From 7f349a7ae3240201b0200bc4c7b707b9c77ebf36 Mon Sep 17 00:00:00 2001 From: Yuliya Dzikovich Date: Thu, 18 Jan 2018 16:19:05 -0700 Subject: [PATCH 13/16] Add db migrations --- ci/pipeline.yml | 26 +++++++++++++++++++ databases/tracker/create_databases.sql | 10 +++++++ .../tracker/migrations/V1__initial_schema.sql | 11 ++++++++ 3 files changed, 47 insertions(+) create mode 100644 databases/tracker/create_databases.sql create mode 100644 databases/tracker/migrations/V1__initial_schema.sql diff --git a/ci/pipeline.yml b/ci/pipeline.yml index 928173bf6..51c33c6f2 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -60,9 +60,22 @@ jobs: - name: deploy-review plan: - get: pal-tracker + passed: [build] - get: pal-tracker-artifacts trigger: true passed: [build] + - task: migrate database + file: pal-tracker/ci/migrateDatabase.yml + params: + CF_API_URL: {{cf-api-url}} + CF_USERNAME: {{cf-username}} + CF_PASSWORD: {{cf-password}} + CF_ORG: {{cf-org}} + CF_SPACE: review + MYSQL_IP: {{mysql-ip}} + DATABASE_NAME: {{review-database-name}} + DATABASE_USERNAME: {{review-database-username}} + DATABASE_PASSWORD: {{review-database-password}} - put: review-deployment params: manifest: pal-tracker/manifest-review.yml @@ -73,6 +86,19 @@ jobs: - name: deploy-production plan: - get: pal-tracker + passed: [deploy-review] + - task: migrate database + file: pal-tracker/ci/migrateDatabase.yml + params: + CF_API_URL: {{cf-api-url}} + CF_USERNAME: {{cf-username}} + CF_PASSWORD: {{cf-password}} + CF_ORG: {{cf-org}} + CF_SPACE: production + MYSQL_IP: {{mysql-ip}} + DATABASE_NAME: {{production-database-name}} + DATABASE_USERNAME: {{production-database-username}} + DATABASE_PASSWORD: {{production-database-password}} - get: pal-tracker-artifacts passed: [deploy-review] - put: production-deployment diff --git a/databases/tracker/create_databases.sql b/databases/tracker/create_databases.sql new file mode 100644 index 000000000..6f1e86abf --- /dev/null +++ b/databases/tracker/create_databases.sql @@ -0,0 +1,10 @@ +DROP DATABASE IF EXISTS tracker_dev; +DROP DATABASE IF EXISTS tracker_test; + +CREATE DATABASE tracker_dev; +CREATE DATABASE tracker_test; + +CREATE USER IF NOT EXISTS 'tracker'@'localhost' + IDENTIFIED BY ''; +GRANT ALL PRIVILEGES ON tracker_dev.* TO 'tracker' @'localhost'; +GRANT ALL PRIVILEGES ON tracker_test.* TO 'tracker' @'localhost'; \ No newline at end of file diff --git a/databases/tracker/migrations/V1__initial_schema.sql b/databases/tracker/migrations/V1__initial_schema.sql new file mode 100644 index 000000000..daca8c4e3 --- /dev/null +++ b/databases/tracker/migrations/V1__initial_schema.sql @@ -0,0 +1,11 @@ +CREATE TABLE time_entries ( + id BIGINT(20) NOT NULL AUTO_INCREMENT, + project_id BIGINT(20), + user_id BIGINT(20), + date DATE, + hours INT, + + PRIMARY KEY (id) +) + ENGINE = innodb + DEFAULT CHARSET = utf8; \ No newline at end of file From 6673650ff25f1bce1acf75884435d11336781ca9 Mon Sep 17 00:00:00 2001 From: Yuliya Dzikovich Date: Thu, 18 Jan 2018 16:22:16 -0700 Subject: [PATCH 14/16] db migrateion, updated manifests --- manifest-production.yml | 2 +- manifest-review.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/manifest-production.yml b/manifest-production.yml index 1a021d516..5dcc6512c 100644 --- a/manifest-production.yml +++ b/manifest-production.yml @@ -2,4 +2,4 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar - host: yc-yd-pal-tracker + host: yc-jd-pal-tracker-db diff --git a/manifest-review.yml b/manifest-review.yml index 967eb05c4..58845ed2f 100644 --- a/manifest-review.yml +++ b/manifest-review.yml @@ -2,4 +2,4 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar - host: yc-yd-pal-tracker-review + host: yc-jd-pal-tracker-review-db From 82a6531fd4930026664b8e77bb0ca8314d794789 Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Wed, 26 Jul 2017 11:45:04 -0600 Subject: [PATCH 15/16] Add tests for JDBC lab --- .../tracker/JdbcTimeEntryRepositoryTest.java | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 src/test/java/test/pivotal/pal/tracker/JdbcTimeEntryRepositoryTest.java diff --git a/src/test/java/test/pivotal/pal/tracker/JdbcTimeEntryRepositoryTest.java b/src/test/java/test/pivotal/pal/tracker/JdbcTimeEntryRepositoryTest.java new file mode 100644 index 000000000..e1eac20fc --- /dev/null +++ b/src/test/java/test/pivotal/pal/tracker/JdbcTimeEntryRepositoryTest.java @@ -0,0 +1,159 @@ +package test.pivotal.pal.tracker; + + +import com.mysql.cj.jdbc.MysqlDataSource; +import io.pivotal.pal.tracker.JdbcTimeEntryRepository; +import io.pivotal.pal.tracker.TimeEntry; +import io.pivotal.pal.tracker.TimeEntryRepository; +import org.junit.Before; +import org.junit.Test; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.sql.Date; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JdbcTimeEntryRepositoryTest { + private TimeEntryRepository subject; + private JdbcTemplate jdbcTemplate; + + @Before + public void setUp() throws Exception { + MysqlDataSource dataSource = new MysqlDataSource(); + dataSource.setUrl(System.getenv("SPRING_DATASOURCE_URL")); + + subject = new JdbcTimeEntryRepository(dataSource); + + jdbcTemplate = new JdbcTemplate(dataSource); + jdbcTemplate.execute("DELETE FROM time_entries"); + + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + } + + @Test + public void createInsertsATimeEntryRecord() throws Exception { + TimeEntry newTimeEntry = new TimeEntry(123, 321, LocalDate.parse("2017-01-09"), 8); + TimeEntry entry = subject.create(newTimeEntry); + + Map foundEntry = jdbcTemplate.queryForMap("Select * from time_entries where id = ?", entry.getId()); + + assertThat(foundEntry.get("id")).isEqualTo(entry.getId()); + assertThat(foundEntry.get("project_id")).isEqualTo(123L); + assertThat(foundEntry.get("user_id")).isEqualTo(321L); + assertThat(((Date)foundEntry.get("date")).toLocalDate()).isEqualTo(LocalDate.parse("2017-01-09")); + assertThat(foundEntry.get("hours")).isEqualTo(8); + } + + @Test + public void createReturnsTheCreatedTimeEntry() throws Exception { + TimeEntry newTimeEntry = new TimeEntry(123, 321, LocalDate.parse("2017-01-09"), 8); + TimeEntry entry = subject.create(newTimeEntry); + + assertThat(entry.getId()).isNotNull(); + assertThat(entry.getProjectId()).isEqualTo(123); + assertThat(entry.getUserId()).isEqualTo(321); + assertThat(entry.getDate()).isEqualTo(LocalDate.parse("2017-01-09")); + assertThat(entry.getHours()).isEqualTo(8); + } + + @Test + public void findFindsATimeEntry() throws Exception { + jdbcTemplate.execute( + "INSERT INTO time_entries (id, project_id, user_id, date, hours) " + + "VALUES (999, 123, 321, '2017-01-09', 8)" + ); + + TimeEntry timeEntry = subject.find(999L); + + assertThat(timeEntry.getId()).isEqualTo(999L); + assertThat(timeEntry.getProjectId()).isEqualTo(123L); + assertThat(timeEntry.getUserId()).isEqualTo(321L); + assertThat(timeEntry.getDate()).isEqualTo(LocalDate.parse("2017-01-09")); + assertThat(timeEntry.getHours()).isEqualTo(8); + } + + @Test + public void findReturnsNullWhenNotFound() throws Exception { + TimeEntry timeEntry = subject.find(999L); + + assertThat(timeEntry).isNull(); + } + + @Test + public void listFindsAllTimeEntries() throws Exception { + jdbcTemplate.execute( + "INSERT INTO time_entries (id, project_id, user_id, date, hours) " + + "VALUES (999, 123, 321, '2017-01-09', 8), (888, 456, 678, '2017-01-08', 9)" + ); + + List timeEntries = subject.list(); + assertThat(timeEntries.size()).isEqualTo(2); + + TimeEntry timeEntry = timeEntries.get(0); + assertThat(timeEntry.getId()).isEqualTo(888L); + assertThat(timeEntry.getProjectId()).isEqualTo(456L); + assertThat(timeEntry.getUserId()).isEqualTo(678L); + assertThat(timeEntry.getDate()).isEqualTo(LocalDate.parse("2017-01-08")); + assertThat(timeEntry.getHours()).isEqualTo(9); + + timeEntry = timeEntries.get(1); + assertThat(timeEntry.getId()).isEqualTo(999L); + assertThat(timeEntry.getProjectId()).isEqualTo(123L); + assertThat(timeEntry.getUserId()).isEqualTo(321L); + assertThat(timeEntry.getDate()).isEqualTo(LocalDate.parse("2017-01-09")); + assertThat(timeEntry.getHours()).isEqualTo(8); + } + + @Test + public void updateReturnsTheUpdatedRecord() throws Exception { + jdbcTemplate.execute( + "INSERT INTO time_entries (id, project_id, user_id, date, hours) " + + "VALUES (1000, 123, 321, '2017-01-09', 8)"); + + TimeEntry timeEntryUpdates = new TimeEntry(456, 987, LocalDate.parse("2017-01-10"), 10); + + TimeEntry updatedTimeEntry = subject.update(1000L, timeEntryUpdates); + + assertThat(updatedTimeEntry.getId()).isEqualTo(1000L); + assertThat(updatedTimeEntry.getProjectId()).isEqualTo(456L); + assertThat(updatedTimeEntry.getUserId()).isEqualTo(987L); + assertThat(updatedTimeEntry.getDate()).isEqualTo(LocalDate.parse("2017-01-10")); + assertThat(updatedTimeEntry.getHours()).isEqualTo(10); + } + + @Test + public void updateUpdatesTheRecord() throws Exception { + jdbcTemplate.execute( + "INSERT INTO time_entries (id, project_id, user_id, date, hours) " + + "VALUES (1000, 123, 321, '2017-01-09', 8)"); + + TimeEntry updatedTimeEntry = new TimeEntry(456, 322, LocalDate.parse("2017-01-10"), 10); + + TimeEntry timeEntry = subject.update(1000L, updatedTimeEntry); + + Map foundEntry = jdbcTemplate.queryForMap("Select * from time_entries where id = ?", timeEntry.getId()); + + assertThat(foundEntry.get("id")).isEqualTo(timeEntry.getId()); + assertThat(foundEntry.get("project_id")).isEqualTo(456L); + assertThat(foundEntry.get("user_id")).isEqualTo(322L); + assertThat(((Date)foundEntry.get("date")).toLocalDate()).isEqualTo(LocalDate.parse("2017-01-10")); + assertThat(foundEntry.get("hours")).isEqualTo(10); + } + + @Test + public void deleteRemovesTheRecord() throws Exception { + jdbcTemplate.execute( + "INSERT INTO time_entries (id, project_id, user_id, date, hours) " + + "VALUES (999, 123, 321, '2017-01-09', 8)" + ); + + subject.delete(999L); + + Map foundEntry = jdbcTemplate.queryForMap("Select count(*) count from time_entries where id = ?", 999); + assertThat(foundEntry.get("count")).isEqualTo(0L); + } +} From 31baebf8af99003df35af323129143d291a673a2 Mon Sep 17 00:00:00 2001 From: Yuliya Dzikovich Date: Fri, 19 Jan 2018 14:31:14 -0700 Subject: [PATCH 16/16] JDBC shenanigans --- build.gradle | 22 ++++- ci/build.yml | 15 +++- manifest-production.yml | 2 + manifest-review.yml | 2 + .../pal/tracker/JdbcTimeEntryRepository.java | 80 +++++++++++++++++++ .../pal/tracker/PalTrackerApplication.java | 6 +- .../io/pivotal/pal/tracker/TimeEntry.java | 16 ++++ .../pal/tracker/TimeEntryRepository.java | 10 +-- .../pal/trackerapi/TimeEntryApiTest.java | 15 ++++ 9 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java diff --git a/build.gradle b/build.gradle index bde7f2f19..f39d2bace 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,9 @@ +import org.flywaydb.gradle.task.FlywayMigrateTask + plugins { id "java" id "org.springframework.boot" version "1.5.4.RELEASE" + id "org.flywaydb.flyway" version "4.2.0" } repositories { @@ -10,13 +13,30 @@ repositories { dependencies { compile("org.springframework.boot:spring-boot-starter-web") compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.1") + compile("org.springframework.boot:spring-boot-starter-jdbc") + compile("mysql:mysql-connector-java:6.0.6") testCompile("org.springframework.boot:spring-boot-starter-test") } +def developmentDbUrl = "jdbc:mysql://localhost:3306/tracker_dev?user=tracker&useSSL=false&useTimezone=true&serverTimezone=UTC&useLegacyDatetimeCode=false" bootRun.environment([ "WELCOME_MESSAGE": "hello", + "SPRING_DATASOURCE_URL": developmentDbUrl, ]) +def testDbUrl = "jdbc:mysql://localhost:3306/tracker_test?user=tracker&useSSL=false&useTimezone=true&serverTimezone=UTC&useLegacyDatetimeCode=false" test.environment([ "WELCOME_MESSAGE": "Hello from test", -]) \ No newline at end of file + "SPRING_DATASOURCE_URL": testDbUrl, +]) + +flyway { + url = developmentDbUrl + user = "tracker" + password = "" + locations = ["filesystem:databases/tracker/migrations"] +} + +task testMigrate(type: FlywayMigrateTask) { + url = testDbUrl +} \ No newline at end of file diff --git a/ci/build.yml b/ci/build.yml index 4ba28db53..2455d052b 100644 --- a/ci/build.yml +++ b/ci/build.yml @@ -18,7 +18,18 @@ run: args: - -exc - | + + export DEBIAN_FRONTEND="noninteractive" + + apt-get update + + apt-get -y install mysql-server + service mysql start + cd pal-tracker + mysql -uroot < databases/tracker/create_databases.sql chmod +x gradlew - ./gradlew -P version=$(cat ../version/number) build - cp build/libs/pal-tracker-*.jar ../build-output + ./gradlew -P version=$(cat ../version/number) testMigrate clean build || (service mysql stop && exit 1) + service mysql stop + + cp build/libs/pal-tracker-*.jar ../build-output \ No newline at end of file diff --git a/manifest-production.yml b/manifest-production.yml index 5dcc6512c..aac8fa130 100644 --- a/manifest-production.yml +++ b/manifest-production.yml @@ -3,3 +3,5 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar host: yc-jd-pal-tracker-db + services: + - tracker-database diff --git a/manifest-review.yml b/manifest-review.yml index 58845ed2f..e075c0a84 100644 --- a/manifest-review.yml +++ b/manifest-review.yml @@ -3,3 +3,5 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar host: yc-jd-pal-tracker-review-db + services: + - tracker-database diff --git a/src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java b/src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java new file mode 100644 index 000000000..bf47cf4b8 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java @@ -0,0 +1,80 @@ +package io.pivotal.pal.tracker; + +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; + +import javax.sql.DataSource; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.util.List; + + +public class JdbcTimeEntryRepository implements TimeEntryRepository{ + + private final JdbcTemplate jdbcTemplate; + + public JdbcTimeEntryRepository(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Override + public TimeEntry create(TimeEntry timeEntry) { + + KeyHolder generatedKeyHolder = new GeneratedKeyHolder(); + String createStatement = "INSERT INTO time_entries ( `date`, hours, project_id, user_id) VALUES ( ?,?,?,? )"; + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(createStatement, + Statement.RETURN_GENERATED_KEYS); + ps.setDate(1, Date.valueOf(timeEntry.getDate())); + ps.setInt(2, timeEntry.getHours()); + ps.setLong(3, timeEntry.getProjectId()); + ps.setLong(4, timeEntry.getUserId()); + return ps; + }, generatedKeyHolder); + + return this.find(generatedKeyHolder.getKey().longValue()); + } + + @Override + public TimeEntry find(long id) { + try { + return this.jdbcTemplate.queryForObject("SELECT * FROM time_entries WHERE id = ?", mapper, id); + } catch (EmptyResultDataAccessException e ) { + return null; + } + } + + @Override + public List list() { + return jdbcTemplate.query("SELECT * FROM time_entries", mapper); + } + + @Override + public TimeEntry update(long id, TimeEntry timeEntry) { + String updateStatement = "UPDATE time_entries SET `date` = ?, hours = ?, project_id = ?, user_id = ? WHERE id = ?"; + jdbcTemplate.update(updateStatement, Date.valueOf(timeEntry.getDate()), timeEntry.getHours(), timeEntry.getProjectId(), timeEntry.getUserId(), id); + + return find(id); + } + + @Override + public void delete(long id) { + jdbcTemplate.update("DELETE FROM time_entries WHERE id=?", id); + } + + private RowMapper mapper = (rs, rowNum) -> { + TimeEntry timeEntry = new TimeEntry(); + timeEntry.setId(rs.getLong("id")); + timeEntry.setProjectId(rs.getLong("project_id")); + timeEntry.setUserId(rs.getLong("user_id")); + timeEntry.setDate(rs.getDate("date").toLocalDate()); + timeEntry.setHours(rs.getInt("hours")); + return timeEntry; + }; + +} diff --git a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java index f8230c75d..e5409e0ff 100644 --- a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java +++ b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java @@ -9,6 +9,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import javax.sql.DataSource; + @SpringBootApplication public class PalTrackerApplication { @@ -17,8 +19,8 @@ public static void main(String[] args) { } @Bean - public TimeEntryRepository getInMemoryTimeEntryRepository(){ - return new InMemoryTimeEntryRepository(); + public TimeEntryRepository getInMemoryTimeEntryRepository(DataSource dataSource){ + return new JdbcTimeEntryRepository(dataSource); } @Bean diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntry.java b/src/main/java/io/pivotal/pal/tracker/TimeEntry.java index f6b2e6a5f..3d6cb4cb8 100644 --- a/src/main/java/io/pivotal/pal/tracker/TimeEntry.java +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntry.java @@ -53,6 +53,22 @@ public int getHours() { return hours; } + public void setProjectId(long projectId) { + this.projectId = projectId; + } + + public void setUserId(long userId) { + this.userId = userId; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + public void setHours(int hours) { + this.hours = hours; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java b/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java index 233663424..04b4a5d7b 100644 --- a/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java @@ -3,13 +3,13 @@ import java.util.List; public interface TimeEntryRepository { - public TimeEntry create(TimeEntry timeEntry); + TimeEntry create(TimeEntry timeEntry); - public TimeEntry find(long id); + TimeEntry find(long id); - public List list(); + List list(); - public TimeEntry update(long id, TimeEntry timeEntry); + TimeEntry update(long id, TimeEntry timeEntry); - public void delete(long id); + void delete(long id); } diff --git a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java index b742e5447..56075d0a6 100644 --- a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java +++ b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java @@ -1,8 +1,10 @@ package test.pivotal.pal.trackerapi; import com.jayway.jsonpath.DocumentContext; +import com.mysql.cj.jdbc.MysqlDataSource; import io.pivotal.pal.tracker.PalTrackerApplication; import io.pivotal.pal.tracker.TimeEntry; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -12,10 +14,12 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.junit4.SpringRunner; import java.time.LocalDate; import java.util.Collection; +import java.util.TimeZone; import static com.jayway.jsonpath.JsonPath.parse; import static org.assertj.core.api.Assertions.assertThat; @@ -30,6 +34,17 @@ public class TimeEntryApiTest { private TimeEntry timeEntry = new TimeEntry(123, 456, LocalDate.parse("2017-01-08"), 8); + @Before + public void setUp() throws Exception { + MysqlDataSource dataSource = new MysqlDataSource(); + dataSource.setUrl(System.getenv("SPRING_DATASOURCE_URL")); + + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + jdbcTemplate.execute("TRUNCATE time_entries"); + + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + } + @Test public void testCreate() throws Exception { ResponseEntity createResponse = restTemplate.postForEntity("/time-entries", timeEntry, String.class);