From 1d05afd22ebacc1e30369061d82ce0fa7b2d8ee0 Mon Sep 17 00:00:00 2001 From: emanuelhjertzen Date: Thu, 22 Mar 2018 13:39:27 +0100 Subject: [PATCH 01/86] adapted for AWS deploy --- Dockerfile | 28 +++++++++++++--------------- Dockerfile_old | 17 +++++++++++++++++ buildspec.yml | 40 ++++++++++++++++++++++++++++++++++++++++ docker_buildspec.yml | 20 ++++++++++++++++++++ pom.xml | 2 +- 5 files changed, 91 insertions(+), 16 deletions(-) create mode 100644 Dockerfile_old create mode 100644 buildspec.yml create mode 100644 docker_buildspec.yml diff --git a/Dockerfile b/Dockerfile index f4b9621..a74d21a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,15 @@ -### Build stage -FROM maven:3.5.2-jdk-8 as builder -WORKDIR /tmp/build-dir -COPY . . -RUN cd /tmp/build-dir && mvn package - -### Production stage -FROM java:8-jre +FROM openjdk:8u151-jre-alpine3.7 +# FROM java:8-jre LABEL maintainer="Oliver Hoogvliet , Raimar Falke " -RUN groupadd -r app && useradd --no-log-init -r -g app app -WORKDIR /home/app -COPY --from=builder /tmp/build-dir/container_start.sh /app/container_start.sh -RUN chmod 755 /app/container_start.sh -USER app -ENTRYPOINT ["/app/container_start.sh"] +ARG APP_FILE_PATH +# RUN groupadd -r app && useradd --no-log-init -r -g app app +# WORKDIR /home/app +# COPY container_start.sh container_start.sh +# RUN chmod 755 container_start.sh +# USER app +# ENTRYPOINT ["/app/container_start.sh"] EXPOSE 8080 -COPY --from=builder /tmp/build-dir/target/app.jar /app/app.jar +COPY $APP_FILE_PATH app.jar +# COPY --from=builder /tmp/build-dir/target/app.jar /app/app.jar +CMD ["java", "-jar", "app.jar"] +# CMD ["container_start.sh"] diff --git a/Dockerfile_old b/Dockerfile_old new file mode 100644 index 0000000..f4b9621 --- /dev/null +++ b/Dockerfile_old @@ -0,0 +1,17 @@ +### Build stage +FROM maven:3.5.2-jdk-8 as builder +WORKDIR /tmp/build-dir +COPY . . +RUN cd /tmp/build-dir && mvn package + +### Production stage +FROM java:8-jre +LABEL maintainer="Oliver Hoogvliet , Raimar Falke " +RUN groupadd -r app && useradd --no-log-init -r -g app app +WORKDIR /home/app +COPY --from=builder /tmp/build-dir/container_start.sh /app/container_start.sh +RUN chmod 755 /app/container_start.sh +USER app +ENTRYPOINT ["/app/container_start.sh"] +EXPOSE 8080 +COPY --from=builder /tmp/build-dir/target/app.jar /app/app.jar diff --git a/buildspec.yml b/buildspec.yml new file mode 100644 index 0000000..adc2d1c --- /dev/null +++ b/buildspec.yml @@ -0,0 +1,40 @@ +version: 0.2 + +#This is the buildspec for building the application source using maven. +#The result is +# - a jar-file (e.g. example-0.0.2-SNAPSHOT.jar) +# - a app_info.txt file with informaion about the above file, and version +# (this is used whenin the CodeBuild project that builds and pushes the docker image) +# - the Dockerfile and docker_buildspec for the CodeBuild project that builds and pushes the docker image. +# (simply copied from the source files) + +phases: + build: + commands: + - echo Build started on `date` + - mvn package + post_build: + commands: + - echo Build completed on `date` + + # The following commands read the artifactId, version and packaging tags from the maven POM.xml + # and passes the info to the next stage in app_info.txt +# - APP_ARTIFACT_ID=$(grep '^archivesBaseName' build.gradle | cut -d'=' -f2|sed "s/[' ]//g") +# - APP_VERSION=$(grep '^version' build.gradle | cut -d'=' -f2|sed "s/[' ]//g") + - APP_ARTIFACT_ID=$(mvn -q -Dexec.executable="echo" -Dexec.args='${project.build.finalName}' --non-recursive exec:exec) + - APP_VERSION=$(mvn -q -Dexec.executable="echo" -Dexec.args='${project.version}' --non-recursive exec:exec) + - printf "/target/%s.jar;%s" "$APP_ARTIFACT_ID" "$APP_VERSION" > app_info.txt + +artifacts: + files: + - 'target/*.jar' + - 'container_start.sh' + - 'app_info.txt' + - 'Dockerfile' + - 'docker_buildspec.yml' +cache: + paths: + #Maven + - '/root/.m2/**/*' + #Gradle + #- '/root/.gradle/caches/**/*' diff --git a/docker_buildspec.yml b/docker_buildspec.yml new file mode 100644 index 0000000..54a5480 --- /dev/null +++ b/docker_buildspec.yml @@ -0,0 +1,20 @@ +version: 0.2 + +phases: + pre_build: + commands: + - $(aws ecr get-login) + - APP_FILE_PATH="$(cat app_info.txt|cut -d';' -f1)" + - APP_VERSION="$(cat app_info.txt|cut -d';' -f2)" + - TAG=$APP_VERSION + - IMAGE_URI="${REPOSITORY_URI}:${TAG}" + + build: + commands: + - docker build --build-arg APP_FILE_PATH=$APP_FILE_PATH --tag "$IMAGE_URI" . + post_build: + commands: + - docker push "$IMAGE_URI" + - printf '[{"name":"product-service","imageUri":"%s"}]' "$IMAGE_URI" > images.json +artifacts: + files: images.json diff --git a/pom.xml b/pom.xml index e195096..c2c6f0f 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ de.codecentric aws-codepipelines-dashboard - 1.2-SNAPSHOT + 1.3 ${project.artifactId} Shows the status of your AWS pipelines in a dashboard From c2b8fe9b573394bd2b525aad33a0cffd5afb10ea Mon Sep 17 00:00:00 2001 From: emanuelhjertzen Date: Thu, 22 Mar 2018 13:41:25 +0100 Subject: [PATCH 02/86] changed region --- src/main/java/de/codecentric/App.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/codecentric/App.java b/src/main/java/de/codecentric/App.java index 5ccb256..e7f75c9 100644 --- a/src/main/java/de/codecentric/App.java +++ b/src/main/java/de/codecentric/App.java @@ -16,6 +16,6 @@ public static void main(String[] args) { @Bean public AWSCodePipeline getAwsCodePipelineClient() { - return AWSCodePipelineClientBuilder.standard().withRegion(Regions.EU_CENTRAL_1).build(); + return AWSCodePipelineClientBuilder.standard().withRegion(Regions.US_EAST_1).build(); } } From 673e4cf0d050bea9f2344344fe2e7fcb530cb40f Mon Sep 17 00:00:00 2001 From: emanuelhjertzen Date: Thu, 22 Mar 2018 16:04:52 +0100 Subject: [PATCH 03/86] changed to port 80 --- Dockerfile | 4 ++-- container_start.sh | 0 2 files changed, 2 insertions(+), 2 deletions(-) mode change 100644 => 100755 container_start.sh diff --git a/Dockerfile b/Dockerfile index a74d21a..386fedd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,8 +8,8 @@ ARG APP_FILE_PATH # RUN chmod 755 container_start.sh # USER app # ENTRYPOINT ["/app/container_start.sh"] -EXPOSE 8080 +EXPOSE 80 COPY $APP_FILE_PATH app.jar # COPY --from=builder /tmp/build-dir/target/app.jar /app/app.jar -CMD ["java", "-jar", "app.jar"] +CMD ["java", "-jar", "-Dserver.port=80", "app.jar"] # CMD ["container_start.sh"] diff --git a/container_start.sh b/container_start.sh old mode 100644 new mode 100755 From 67237de32c93f175991a17f6340fcc07c1d42251 Mon Sep 17 00:00:00 2001 From: emanuelhjertzen Date: Thu, 22 Mar 2018 17:55:09 +0100 Subject: [PATCH 04/86] correct container name --- docker_buildspec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker_buildspec.yml b/docker_buildspec.yml index 54a5480..40badd3 100644 --- a/docker_buildspec.yml +++ b/docker_buildspec.yml @@ -15,6 +15,6 @@ phases: post_build: commands: - docker push "$IMAGE_URI" - - printf '[{"name":"product-service","imageUri":"%s"}]' "$IMAGE_URI" > images.json + - printf '[{"name":"aws-dashboard","imageUri":"%s"}]' "$IMAGE_URI" > images.json artifacts: files: images.json From b706741ed97a8708089fdeb1406948d6e628fa23 Mon Sep 17 00:00:00 2001 From: Robin Andeer Date: Tue, 10 Apr 2018 14:39:17 +0200 Subject: [PATCH 05/86] added bootstrap version of UI --- src/main/resources/static/css/main.css | 81 -------- src/main/resources/static/js/pipelineBox.js | 204 ++++++++++++-------- src/main/resources/templates/index.html | 63 +++--- 3 files changed, 160 insertions(+), 188 deletions(-) delete mode 100644 src/main/resources/static/css/main.css diff --git a/src/main/resources/static/css/main.css b/src/main/resources/static/css/main.css deleted file mode 100644 index 6019853..0000000 --- a/src/main/resources/static/css/main.css +++ /dev/null @@ -1,81 +0,0 @@ -body { - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 10pt; - background-color: black; -} - -.body_contrast { - color: white; -} - -.side { - -webkit-transform: rotate(90deg); /* Safari and Chrome */ - -moz-transform: rotate(90deg); /* Firefox */ - -ms-transform: rotate(90deg); /* IE 9 */ - -o-transform: rotate(90deg); /* Opera */ - transform: rotate(90deg); - position: absolute; - right: 0; - top: 70px; -} - -h2 { - color: #000000; - font-size: 10pt; -} - -.pipeline { - background-color: lightgray; - border: 1px solid #aaaaaa; - margin: 5px; - padding: 10px; - width: 300px; - height: 140px; - position: relative; - float: left; -} - -.stage { - font-size: 8pt; - border: 1px solid #aaaaaa; - margin: 2px; - height: 13px; -} - -.stat_succeeded { - border-color: lightgreen; - background-color: lightgreen; -} - -.stat_failed { - border-color: red; - background-color: red; -} - -.stat_inprogress { - border-color: lightblue; - background-color: lightblue; -} - -.stage_name { - width: 50%; - float: left; -} - -.stage_latestexecution { - font-size: 6pt; - text-align: right; - width: 50%; - float: right; -} - -.dateinfo { - background-color: gainsboro; - text-align: center; - font-size: 10px; - margin: 2px; - bottom: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} diff --git a/src/main/resources/static/js/pipelineBox.js b/src/main/resources/static/js/pipelineBox.js index 7d89994..826c670 100644 --- a/src/main/resources/static/js/pipelineBox.js +++ b/src/main/resources/static/js/pipelineBox.js @@ -1,94 +1,140 @@ let pipelineService = PipelineService($); -Vue.component('pipeline', { - props: ['pipeline'], // attribute of tag - template: ` -
{{ pipeline }} - -
Start: {{startDate}}
Duration: {{duration}} min
{{commitMessage}}
-
`, - data: function () { - return { - stages: [], - duration: 0, - startDate: "-", - commitMessage: "" - }; - }, - methods: { - getPipelineDetails: function (pipelineName) { - let componentScope = this; - pipelineService.getPipelineDetails(pipelineName, function (stages) { - componentScope.stages = stages; - let min = 0, max = 0; - let commitMessage = ""; - for (let i = 0; i < stages.length; i++) { - let lastUpdate = parseInt(stages[i].lastStatusChange); - if (min === 0) { - min = lastUpdate; - } - if (max === 0) { - max = lastUpdate; - } - if (lastUpdate > max) { - max = lastUpdate; - } - if (lastUpdate < min) { - min = lastUpdate; - } - if (commitMessage === "") { - commitMessage = stages[i].commitMessage; - } - } - componentScope.duration = ((max - min) / 60000).toFixed(1); - componentScope.startDate = moment(min).format('DD.MM.YYYY HH:mm:ss'); - componentScope.commitMessage = commitMessage; - }); +Vue.component("pipeline", { + props: ["pipeline"], // attribute of tag + template: ` +
+
+
{{ pipelineName }}
+

+ + Started {{ startDate }} + {{ duration }} min +
+ {{ commitMessage }} +

+
+
    +
  • + +
  • +
+
+ `, + data: function() { + return { + stages: [], + duration: 0, + startDate: "-", + commitMessage: "" + }; + }, + methods: { + getPipelineDetails: function(pipelineName) { + let componentScope = this; + pipelineService.getPipelineDetails(pipelineName, function(stages) { + componentScope.stages = stages; + let min = 0, + max = 0; + let commitMessage = ""; + for (let i = 0; i < stages.length; i++) { + let lastUpdate = parseInt(stages[i].lastStatusChange); + if (min === 0) { + min = lastUpdate; + } + if (max === 0) { + max = lastUpdate; + } + if (lastUpdate > max) { + max = lastUpdate; + } + if (lastUpdate < min) { + min = lastUpdate; + } + if (commitMessage === "") { + commitMessage = stages[i].commitMessage; + } } + componentScope.duration = ((max - min) / 60000).toFixed(1); + componentScope.startDate = moment(min).fromNow(); + componentScope.commitMessage = commitMessage; + }); + } + }, + mounted() { + this.getPipelineDetails(this.pipeline); + window.setInterval(() => this.getPipelineDetails(this.pipeline), 60000); + }, + computed: { + pipelineName: function() { + return this.pipeline.split("-", 3).join("-"); }, - mounted() { - this.getPipelineDetails(this.pipeline); - window.setInterval(() => this.getPipelineDetails(this.pipeline), 60000); + borderClass: function() { + const isFailed = + this.stages.findIndex(item => item.latestStatus === "failed") !== -1; + const isBuilding = + this.stages.findIndex(item => item.latestStatus === "inprogress") !== + -1; + if (isFailed) { + return "border-danger"; + } else if (isBuilding) { + return "border-info"; + } else { + return "border-success"; + } } + } }); -Vue.component('stage', { - props: ['stage'], - template: ` -
-
{{ stage.name }}
- -
+Vue.component("stage", { + props: ["stage"], + template: ` +
+
{{ stage.name }}
+ +
`, - methods: {}, - computed: { - isSucceeded: function () { - return this.stage.latestStatus === "succeeded"; - }, - isFailed: function () { - return this.stage.latestStatus === "failed"; - }, - isInProgress: function () { - return this.stage.latestStatus === "inprogress"; - }, - latestExecutionDate: function () { - return moment(this.stage.lastStatusChange).format('DD.MM.YYYY HH:mm:ss'); - } + methods: {}, + computed: { + isSucceeded: function() { + return this.stage.latestStatus === "succeeded"; + }, + isFailed: function() { + return this.stage.latestStatus === "failed"; + }, + isInProgress: function() { + return this.stage.latestStatus === "inprogress"; + }, + latestExecutionDate: function() { + return moment(this.stage.lastStatusChange).fromNow(); + }, + badgeType: function() { + switch (this.stage.latestStatus) { + case "succeeded": + return "badge badge-success"; + case "failed": + return "badge badge-failed"; + case "inprogress": + return "badge badge-info"; + } } - + } }); - let app = new Vue({ - el: '#app', - data: { - pipelines: [] - }, - methods: {} + el: "#app", + data: { + pipelines: [] + }, + methods: {} }); -pipelineService.getPipelines(function (names) { - for (let i = 0; i < names.length; i++) { - app.pipelines.push(names[i]); - } +pipelineService.getPipelines(function(names) { + for (let i = 0; i < names.length; i++) { + app.pipelines.push(names[i]); + } }); diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index dc0c6c2..0fa386d 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -1,45 +1,52 @@ + Dashboard - + - + - - -
-

Dashboard

-
Legend: - succeeded - in progress - failed + + + +
+
-
-
- - -
+
+
+
+ +
+
+
- + - + - + - + - + + \ No newline at end of file From eed0b86a74f357738eebd5b2f2de68f9a1b91f50 Mon Sep 17 00:00:00 2001 From: emanuelhjertzen Date: Thu, 28 Jun 2018 11:07:06 +0200 Subject: [PATCH 06/86] changed code for eu-west-1 --- Dockerfile | 9 --------- Dockerfile_old | 17 ----------------- src/main/java/de/codecentric/App.java | 2 +- 3 files changed, 1 insertion(+), 27 deletions(-) delete mode 100644 Dockerfile_old diff --git a/Dockerfile b/Dockerfile index 386fedd..0e96cc3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,6 @@ FROM openjdk:8u151-jre-alpine3.7 -# FROM java:8-jre LABEL maintainer="Oliver Hoogvliet , Raimar Falke " ARG APP_FILE_PATH -# RUN groupadd -r app && useradd --no-log-init -r -g app app -# WORKDIR /home/app -# COPY container_start.sh container_start.sh -# RUN chmod 755 container_start.sh -# USER app -# ENTRYPOINT ["/app/container_start.sh"] EXPOSE 80 COPY $APP_FILE_PATH app.jar -# COPY --from=builder /tmp/build-dir/target/app.jar /app/app.jar CMD ["java", "-jar", "-Dserver.port=80", "app.jar"] -# CMD ["container_start.sh"] diff --git a/Dockerfile_old b/Dockerfile_old deleted file mode 100644 index f4b9621..0000000 --- a/Dockerfile_old +++ /dev/null @@ -1,17 +0,0 @@ -### Build stage -FROM maven:3.5.2-jdk-8 as builder -WORKDIR /tmp/build-dir -COPY . . -RUN cd /tmp/build-dir && mvn package - -### Production stage -FROM java:8-jre -LABEL maintainer="Oliver Hoogvliet , Raimar Falke " -RUN groupadd -r app && useradd --no-log-init -r -g app app -WORKDIR /home/app -COPY --from=builder /tmp/build-dir/container_start.sh /app/container_start.sh -RUN chmod 755 /app/container_start.sh -USER app -ENTRYPOINT ["/app/container_start.sh"] -EXPOSE 8080 -COPY --from=builder /tmp/build-dir/target/app.jar /app/app.jar diff --git a/src/main/java/de/codecentric/App.java b/src/main/java/de/codecentric/App.java index e7f75c9..e39b417 100644 --- a/src/main/java/de/codecentric/App.java +++ b/src/main/java/de/codecentric/App.java @@ -16,6 +16,6 @@ public static void main(String[] args) { @Bean public AWSCodePipeline getAwsCodePipelineClient() { - return AWSCodePipelineClientBuilder.standard().withRegion(Regions.US_EAST_1).build(); + return AWSCodePipelineClientBuilder.standard().withRegion(Regions.EU_WEST_1).build(); } } From 4af2b8c8822f937a115963c423ba356f5c38d57b Mon Sep 17 00:00:00 2001 From: Mike Naik Date: Mon, 10 Sep 2018 09:35:19 -0700 Subject: [PATCH 07/86] Removing region hardcoding --- src/main/java/de/codecentric/App.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/codecentric/App.java b/src/main/java/de/codecentric/App.java index e39b417..7d9da5a 100644 --- a/src/main/java/de/codecentric/App.java +++ b/src/main/java/de/codecentric/App.java @@ -16,6 +16,6 @@ public static void main(String[] args) { @Bean public AWSCodePipeline getAwsCodePipelineClient() { - return AWSCodePipelineClientBuilder.standard().withRegion(Regions.EU_WEST_1).build(); + return AWSCodePipelineClientBuilder.standard().build(); } } From a739e2720193d047081b9e9a36debd752cc9f438 Mon Sep 17 00:00:00 2001 From: gyachuk Date: Mon, 10 Sep 2018 10:35:30 -0700 Subject: [PATCH 08/86] Display the entire pipeline name. Don't truncate to first 3 sections of the name. --- src/main/resources/static/js/pipelineBox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/static/js/pipelineBox.js b/src/main/resources/static/js/pipelineBox.js index 826c670..6fabb4e 100644 --- a/src/main/resources/static/js/pipelineBox.js +++ b/src/main/resources/static/js/pipelineBox.js @@ -67,7 +67,7 @@ Vue.component("pipeline", { }, computed: { pipelineName: function() { - return this.pipeline.split("-", 3).join("-"); + return this.pipeline; }, borderClass: function() { const isFailed = From 9751ad4fe0eca364ff40aa6f08932e51c510ceca Mon Sep 17 00:00:00 2001 From: gyachuk Date: Mon, 10 Sep 2018 14:18:29 -0700 Subject: [PATCH 09/86] Highlight the entire "Human" line if it is "inprogress", to reinforce that a human action is required. --- src/main/resources/static/js/pipelineBox.js | 13 +++++++++++-- src/main/resources/static/js/pipelineService.js | 2 ++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/main/resources/static/js/pipelineBox.js b/src/main/resources/static/js/pipelineBox.js index 6fabb4e..39882d4 100644 --- a/src/main/resources/static/js/pipelineBox.js +++ b/src/main/resources/static/js/pipelineBox.js @@ -89,7 +89,7 @@ Vue.component("pipeline", { Vue.component("stage", { props: ["stage"], template: ` -
+
{{ stage.name }}
@@ -98,7 +98,11 @@ Vue.component("stage", {
`, - methods: {}, + methods: { + isActionRequired: function() { + return this.stage.latestStatus === "inprogress" && this.stage.name === "Human"; + } + }, computed: { isSucceeded: function() { return this.stage.latestStatus === "succeeded"; @@ -112,6 +116,11 @@ Vue.component("stage", { latestExecutionDate: function() { return moment(this.stage.lastStatusChange).fromNow(); }, + statusType: function() { + const classNames = "d-flex justify-content-between align-items-center"; + + return classNames + (this.isActionRequired() ? " badge-info" : ""); + }, badgeType: function() { switch (this.stage.latestStatus) { case "succeeded": diff --git a/src/main/resources/static/js/pipelineService.js b/src/main/resources/static/js/pipelineService.js index 409e96b..75c516a 100644 --- a/src/main/resources/static/js/pipelineService.js +++ b/src/main/resources/static/js/pipelineService.js @@ -36,7 +36,9 @@ let PipelineService = function (jquery) { getPipelineDetailFromAWS(pipelineName, function (response) { for (let i = 0; i < response.stageStates.length; i++) { + if (response.stageStates[i].actionStates[0].latestExecution) { stages.push(parsePipelineState(response.stageStates[i], response.commitMessage)); + } } responseHandler(stages); }); From e723a9253bd3ddab434d1a031743f7da4f287958 Mon Sep 17 00:00:00 2001 From: gyachuk Date: Tue, 11 Sep 2018 10:49:56 -0700 Subject: [PATCH 10/86] Highlight entire line. Move configurable stuff to index.html --- src/main/resources/static/js/pipelineBox.js | 20 ++++++++++------ src/main/resources/templates/index.html | 26 +++++++++++++++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/main/resources/static/js/pipelineBox.js b/src/main/resources/static/js/pipelineBox.js index 39882d4..e2206b2 100644 --- a/src/main/resources/static/js/pipelineBox.js +++ b/src/main/resources/static/js/pipelineBox.js @@ -89,7 +89,7 @@ Vue.component("pipeline", { Vue.component("stage", { props: ["stage"], template: ` -
+
{{ stage.name }}
@@ -98,9 +98,20 @@ Vue.component("stage", {
`, + mounted() { + if (this.isActionRequired()) { + $(this.$el).parent().addClass('needs-human-action'); + } + }, methods: { isActionRequired: function() { - return this.stage.latestStatus === "inprogress" && this.stage.name === "Human"; + for (let j=0; j < needsHumanInteraction.length; j++) { + var needs = needsHumanInteraction[j]; + if (needs.status === this.stage.latestStatus && needs.stage === this.stage.name) { + return true; + } + } + return false; } }, computed: { @@ -116,11 +127,6 @@ Vue.component("stage", { latestExecutionDate: function() { return moment(this.stage.lastStatusChange).fromNow(); }, - statusType: function() { - const classNames = "d-flex justify-content-between align-items-center"; - - return classNames + (this.isActionRequired() ? " badge-info" : ""); - }, badgeType: function() { switch (this.stage.latestStatus) { case "succeeded": diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 0fa386d..6b8d61e 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -2,8 +2,34 @@ + + + + + Dashboard + + + + + + + + + + From b08772b3a32ab6d2de3503ea1c8adfc78711b2b1 Mon Sep 17 00:00:00 2001 From: gyachuk Date: Tue, 11 Sep 2018 15:22:21 -0700 Subject: [PATCH 11/86] Pick up dashboard header display from the configurable "title" element. --- src/main/resources/templates/index.html | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 6b8d61e..f4d9407 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -7,7 +7,7 @@ - Dashboard + Production CodePipeline Dashboard + @@ -73,16 +59,12 @@ + + - \ No newline at end of file From 641f6abc2a879931ef6c309e2d19b8976635ad6c Mon Sep 17 00:00:00 2001 From: gyachuk Date: Mon, 17 Sep 2018 09:37:23 -0700 Subject: [PATCH 38/86] Ignore local data files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 761d91d..7acc016 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ target ### IntelliJ IDEA ### .idea *.iml + +### Local data files ### +/src/main/resources/data/ \ No newline at end of file From b5507272d583f0ea7f52031cb037d64cc50c9586 Mon Sep 17 00:00:00 2001 From: gyachuk Date: Tue, 18 Sep 2018 09:31:34 -0700 Subject: [PATCH 39/86] Pass "stages" data to , rather than fetching data twice. --- src/main/resources/static/js/pipelineBox.js | 102 +++++++++----------- 1 file changed, 48 insertions(+), 54 deletions(-) diff --git a/src/main/resources/static/js/pipelineBox.js b/src/main/resources/static/js/pipelineBox.js index efc9af0..760076c 100644 --- a/src/main/resources/static/js/pipelineBox.js +++ b/src/main/resources/static/js/pipelineBox.js @@ -38,7 +38,7 @@ const pipelinegrid = Vue.component("pipelinegrid", {
`, mounted() { - pipelineService.getPipelines(function(names) { + pipelineService.getPipelines((names) => { for (let i = 0; i < names.length; i++) { app.pipelines.push(names[i]); } @@ -60,7 +60,7 @@ const pipeline = Vue.component("pipeline", {
  • - +
@@ -75,10 +75,7 @@ const pipeline = Vue.component("pipeline", { router.push('/card/' + this.pipelineName); }, getPipelineDetails: function(pipelineName) { - let componentScope = this; - pipelineService.getPipelineDetails(pipelineName, function(stages) { - componentScope.stages = stages; - }); + pipelineService.getPipelineDetails(pipelineName, (stages) => this.stages = stages); } }, mounted() { @@ -111,7 +108,7 @@ const pipeline = Vue.component("pipeline", { * Clicking of the body navigates to a card detail route. */ const pipelineheader = Vue.component("pipelineheader", { - props: ["pipelineName"], // attribute of tag + props: ["pipelineName", "stages"], // attribute of tag template: `
{{ pipelineName }}
@@ -131,57 +128,57 @@ const pipelineheader = Vue.component("pipelineheader", { commitMessage: "" }; }, + watch: { + stages: function(stages) { + this.getPipelineDetails(this.pipelineName, this.stages || []); + } + }, methods: { - getPipelineDetails: function(pipelineName) { + getPipelineDetails: function(pipelineName, stages) { let componentScope = this; - pipelineService.getPipelineDetails(pipelineName, function(stages) { - // Start off with the largest min value, unless there are no stages at all. - // In that case, use zero so we end up with a zero-length duration: (max - min) - let min = (stages.length) ? Number.MAX_VALUE : 0; - // Start off with the lowest max value. Anything in a stage will be greater. - let max = 0; - let commitMessage = ""; - let skipRemainingStages = false; - for (let i = 0; i < stages.length; i++) { - let stage = stages[i]; + // Start off with the largest min value, unless there are no stages at all. + // In that case, use zero so we end up with a zero-length duration: (max - min) + let min = (stages.length) ? Number.MAX_VALUE : 0; + // Start off with the lowest max value. Anything in a stage will be greater. + let max = 0; + let commitMessage = ""; + let skipRemainingStages = false; + for (let i = 0; i < stages.length; i++) { + let stage = stages[i]; - // Anything that needs to be processed for *all* stages needs to go at the top of the loop. - if (!commitMessage) { - commitMessage = stage.commitMessage; - } + // Anything that needs to be processed for *all* stages needs to go at the top of the loop. + if (!commitMessage) { + commitMessage = stage.commitMessage; + } - // After this point, only duration calculations. - // - // We really only care about stages with "succeeded" status. - // We want to compute the duration as the time from the time of the first stage, up to the time - // of the first stage that hasn't "succeeded". Note that this could be the first stage, in which - // case, it will be the only one processed and the duration will be zero. + // After this point, only duration calculations. + // + // We really only care about stages with "succeeded" status. + // We want to compute the duration as the time from the time of the first stage, up to the time + // of the first stage that hasn't "succeeded". Note that this could be the first stage, in which + // case, it will be the only one processed and the duration will be zero. - if (skipRemainingStages) { - continue; - } + if (skipRemainingStages) { + continue; + } - // At this point, we know we're not skipping remaining stages yet, but this stage could be the one - // that causes all subsequent stages to be skipped. We don't skip this one since we want to include - // it in the duration. - skipRemainingStages = (stage.latestStatus !== "succeeded"); + // At this point, we know we're not skipping remaining stages yet, but this stage could be the one + // that causes all subsequent stages to be skipped. We don't skip this one since we want to include + // it in the duration. + skipRemainingStages = (stage.latestStatus !== "succeeded"); - let lastUpdate = parseInt(stages[i].lastStatusChange); - if (lastUpdate > max) { - max = lastUpdate; - } - if (lastUpdate < min) { - min = lastUpdate; - } + let lastUpdate = parseInt(stage.lastStatusChange); + if (lastUpdate > max) { + max = lastUpdate; } - componentScope.duration = moment.duration(max - min).humanize(); - componentScope.startDate = moment(min).fromNow(); - componentScope.commitMessage = commitMessage; - }); + if (lastUpdate < min) { + min = lastUpdate; + } + } + componentScope.duration = moment.duration(max - min).humanize(); + componentScope.startDate = moment(min).fromNow(); + componentScope.commitMessage = commitMessage; } - }, - mounted() { - this.getPipelineDetails(this.pipelineName); } }); @@ -268,7 +265,7 @@ const pipelinecard = Vue.component("pipelinecard", { - +
  • @@ -287,10 +284,7 @@ const pipelinecard = Vue.component("pipelinecard", { router.back(); }, getPipelineDetails: function(pipelineName) { - let componentScope = this; - pipelineService.getPipelineDetails(pipelineName, function(stages) { - componentScope.stages = stages; - }); + pipelineService.getPipelineDetails(pipelineName, (stages) => this.stages = stages); } }, mounted() { From 7b55f04de71fc81f9295606d5b49de9455e7e527 Mon Sep 17 00:00:00 2001 From: Mike Naik Date: Tue, 18 Sep 2018 13:04:06 -0700 Subject: [PATCH 40/86] Docker Elastic Beanstalk Build V1 --- docker_ebs_buildspec.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docker_ebs_buildspec.yml diff --git a/docker_ebs_buildspec.yml b/docker_ebs_buildspec.yml new file mode 100644 index 0000000..2501bf1 --- /dev/null +++ b/docker_ebs_buildspec.yml @@ -0,0 +1,28 @@ +version: 0.2 + +environment_variables: + plaintext: + REPOSITORY_URI: OVERRIDE_THIS_VALUE + REPOSITORY_NAME: OVERRIDE_THIS_VALUE + +phases: + pre_build: + commands: + - $(aws ecr get-login --no-include-email) + - APP_FILE_PATH="$(cat app_info.txt|cut -d';' -f1)" + - APP_VERSION="$(cat app_info.txt|cut -d';' -f2)" + - TAG=$APP_VERSION + - IMAGE_URI="${REPOSITORY_NAME}:${TAG}" + + build: + commands: + - docker build --build-arg APP_FILE_PATH=$APP_FILE_PATH --tag "$REPOSITORY_NAME" . + + post_build: + commands: + - docker tag $REPOSITORY_NAME:latest $REPOSITORY_URI:latest + - docker push "$REPOSITORY_URI" + - printf '{"AWSEBDockerrunVersion":"1","Image":{"Name":"%s"},"Ports":[{"ContainerPort":"80"}]}' "$REPOSITORY_URI:latest" > Dockerrun.aws.json + +artifacts: + files: Dockerrun.aws.json From 477b1da610c9d885b2b5a4cd290ed33d02164732 Mon Sep 17 00:00:00 2001 From: Mike Naik Date: Tue, 18 Sep 2018 18:22:08 -0700 Subject: [PATCH 41/86] ElasticBeanstalk Containerized Buildspec and instructions --- README.md | 26 ++++++++++++++++++++++++-- docker_ebs_buildspec.yml | 28 ---------------------------- eb_docker_buildspec.yml | 24 ++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 30 deletions(-) delete mode 100644 docker_ebs_buildspec.yml create mode 100644 eb_docker_buildspec.yml diff --git a/README.md b/README.md index cac017d..0d70cb4 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,9 @@ After start, you can reach the application via ```http://localhost:8080/``` This configuration assumes that you've already an AWS account with a running -AWS CLI on your development host. +AWS CLI on your development host. If you're having trouble with that, see below. -## Instructions for setting up AWS +## Instructions for setting up AWS permission for Development You have to give/ensure the user mentioned in $HOME/.aws/credentials a policy. The steps are: @@ -47,3 +47,25 @@ Verify that the following entry is listed: "PolicyArn": "arn:aws:iam::aws:policy/AWSCodePipelineFullAccess" } ``` + + +## Instructions for setting up a Production Deployment +### Notes ### +* The application only has access to CodePipeline in the region that the EB is deployed to +* If this data is sensitive, you might want to restrict access in the Security Group that gets created by Elastic Beanstalk (the wizard seems to always create a Security Group) +* Choose *Generic*->*Docker* for the Platform +* You will also need GitHub connectivity as well as CodeBuild, CodePipeline, and ElasticBeanstalk roles for this (the last 3 can be generated by AWS) + +### Setup ### +1. Set up EC2 Role +2. (Optional) Set up the Security Groups +3. Create the Elastic Beanstalk Environment with the EC2 role as the Instance Profile for the VM and Security Group, if created +4. Create a CodeBuild with buildspec.yml to build the Java artifacts (use the Amazon managed Ubuntu Java Runtime, you shouldn't need a VPC or artifacts) +5. Create a CodeBuild with eb_docker_build.yml to containerize the Java artifacts (use the Amazon managed Ubuntu Docker Runtime, ensure you specify the eb_docker_buildspec.yml, you shouldn't need a VPC or artifacts) +6. Create a CodePipeline with: + 1. GitHub repo as the Source stage (you can use any version of the repo and any branch you see fit, as long as the necesssary files exist for CodeBuild to function) + 2. The Java CodeBuild as the first part of the Build stage, with output artifacts tagged something like "_JavaArtifacts_" + 3. The Containerize CodeBuild as the second part of the Build stage, with the input artifacts as the output artifats of the Java build (in this example, _JavaArtifacts_) and the output artifacts tagged as something like "_EBApp_" + 4. (Optional) Set up a Human approval step before deployment if uptime is critical + 5. Set up a Deploy stage to your Elastic Beanstlak environment from Step 3 with the Input artifacts as the output from the Contaizer step (in this example, _EBApp_) +7. Release the Change to trigger a new build and deployment \ No newline at end of file diff --git a/docker_ebs_buildspec.yml b/docker_ebs_buildspec.yml deleted file mode 100644 index 2501bf1..0000000 --- a/docker_ebs_buildspec.yml +++ /dev/null @@ -1,28 +0,0 @@ -version: 0.2 - -environment_variables: - plaintext: - REPOSITORY_URI: OVERRIDE_THIS_VALUE - REPOSITORY_NAME: OVERRIDE_THIS_VALUE - -phases: - pre_build: - commands: - - $(aws ecr get-login --no-include-email) - - APP_FILE_PATH="$(cat app_info.txt|cut -d';' -f1)" - - APP_VERSION="$(cat app_info.txt|cut -d';' -f2)" - - TAG=$APP_VERSION - - IMAGE_URI="${REPOSITORY_NAME}:${TAG}" - - build: - commands: - - docker build --build-arg APP_FILE_PATH=$APP_FILE_PATH --tag "$REPOSITORY_NAME" . - - post_build: - commands: - - docker tag $REPOSITORY_NAME:latest $REPOSITORY_URI:latest - - docker push "$REPOSITORY_URI" - - printf '{"AWSEBDockerrunVersion":"1","Image":{"Name":"%s"},"Ports":[{"ContainerPort":"80"}]}' "$REPOSITORY_URI:latest" > Dockerrun.aws.json - -artifacts: - files: Dockerrun.aws.json diff --git a/eb_docker_buildspec.yml b/eb_docker_buildspec.yml new file mode 100644 index 0000000..02d24b5 --- /dev/null +++ b/eb_docker_buildspec.yml @@ -0,0 +1,24 @@ +version: 0.2 + +environment_variables: +phases: + pre_build: + commands: + - APP_FILE_PATH="$(cat app_info.txt|cut -d';' -f1)" + # Move the file so that the Dockerfile can reference it directly + - cp .$APP_FILE_PATH . + - APP_FILE_PATH="${APP_FILE_PATH##*/}" + + build: + commands: + # Create the Elastic Beanstalk deployment file + - printf '{"AWSEBDockerrunVersion":"1","Ports":[{"ContainerPort":"80"}]}' > Dockerrun.aws.json + # Alter the Dockerfile to remove env variable refs, not accesssible during Beanstalk Dockerfile deployment + - mv Dockerfile Dockerfile.ORIG + - grep -v ARG Dockerfile.ORIG | sed "s/\$APP_FILE_PATH/$APP_FILE_PATH/g" > Dockerfile + +artifacts: + files: + - app.jar + - Dockerfile + - Dockerrun.aws.json From 88fc9a91023dcf0848ffb01c17fd87c4e26d042e Mon Sep 17 00:00:00 2001 From: Mike Naik Date: Tue, 18 Sep 2018 18:25:47 -0700 Subject: [PATCH 42/86] Updated docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d70cb4..e9b51db 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Verify that the following entry is listed: * You will also need GitHub connectivity as well as CodeBuild, CodePipeline, and ElasticBeanstalk roles for this (the last 3 can be generated by AWS) ### Setup ### -1. Set up EC2 Role +1. Set up EC2 Role with AWSCodePipelineReadOnlyAccess 2. (Optional) Set up the Security Groups 3. Create the Elastic Beanstalk Environment with the EC2 role as the Instance Profile for the VM and Security Group, if created 4. Create a CodeBuild with buildspec.yml to build the Java artifacts (use the Amazon managed Ubuntu Java Runtime, you shouldn't need a VPC or artifacts) From 4197fd33071608a6d3aa9399e0a9942da3f12162 Mon Sep 17 00:00:00 2001 From: Mike Naik Date: Tue, 18 Sep 2018 18:43:44 -0700 Subject: [PATCH 43/86] Proper support for Multistage build --- buildspec.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/buildspec.yml b/buildspec.yml index adc2d1c..769f3b1 100644 --- a/buildspec.yml +++ b/buildspec.yml @@ -32,6 +32,7 @@ artifacts: - 'app_info.txt' - 'Dockerfile' - 'docker_buildspec.yml' + - 'eb_docker_buildspec.yml' cache: paths: #Maven From 1c5221060a071b2795c0660e64a3d09446c1eba6 Mon Sep 17 00:00:00 2001 From: Mike Naik Date: Tue, 18 Sep 2018 18:47:36 -0700 Subject: [PATCH 44/86] Fixing broken cleanup. --- eb_docker_buildspec.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/eb_docker_buildspec.yml b/eb_docker_buildspec.yml index 02d24b5..d9bcd10 100644 --- a/eb_docker_buildspec.yml +++ b/eb_docker_buildspec.yml @@ -1,6 +1,5 @@ version: 0.2 -environment_variables: phases: pre_build: commands: From 13dd603bcaaa8d1e0f59c9e653357e532b8017d3 Mon Sep 17 00:00:00 2001 From: gyachuk Date: Wed, 19 Sep 2018 14:23:13 -0700 Subject: [PATCH 45/86] Add AjaxSequencer, to limit the number of simultaneous ajax requests to 3. Clear pending requests when navigating to a different route. --- src/main/resources/static/js/ajaxSequencer.js | 79 +++++++++++++++++++ src/main/resources/static/js/pipelineBox.js | 11 ++- .../resources/static/js/pipelineService.js | 31 +++----- src/main/resources/templates/index.html | 2 + 4 files changed, 104 insertions(+), 19 deletions(-) create mode 100644 src/main/resources/static/js/ajaxSequencer.js diff --git a/src/main/resources/static/js/ajaxSequencer.js b/src/main/resources/static/js/ajaxSequencer.js new file mode 100644 index 0000000..13c6cd7 --- /dev/null +++ b/src/main/resources/static/js/ajaxSequencer.js @@ -0,0 +1,79 @@ +/** + * @component AjaxSequencer + * @parameters + * $ - instance of jQuery + * limit - maximum number of simultaneous Ajax calls. Default is 3. + * + * @description Execute Ajax requests in sequence, allowing no more than `limit` to be simultaneously in-flight. + * + * @methods + * .get(url, responseHandler) - enqueues $.get(url). On complete, call `responseHandler` and start the next one. + * .post(url, responseHandler) - enqueues $.post(url). On complete, call `responseHandler` and start the next one. + * .clear() - clears all queued requests, but allows in-flight requests to complete. + */ +let AjaxSequencer = function($, limit) { + + limit = limit || 3; // Set default of 3. + + let active = 0; // Number of currently active ajax requests. + let pending = []; // Queue of pending ajax requests. + + function enqueue_ajax(pend) { + if (active < limit) { + // Under the limit. Start this request immediately. + ++active; + // Fire the request, and on success call the supplied responseHandler. + // In all cases (even on error), start the next queued request. + $.ajax(pend.url, { method: pend.method }).done(pend.responseHandler).always(function() { + // Request has finished. See if there is a subsequent one to start. + --active; + pend = pending.shift(); // Shift from the front of the array, so it forms a FIFO queue. + if (pend) { + enqueue_ajax(pend); + } + }); + } else { + // Too many active already. Push onto pending and wait for an active request to complete. + pending.push(pend); // Push onto the end of the array. + } + } + + /** + * @title get + * @description - enqueue a GET request. + */ + function get(url, responseHandler) { + enqueue_ajax({ + url: url, + method: 'GET', + responseHandler: responseHandler + }); + } + + /** + * @title post + * @description - enqueue a POST request. + */ + function post(url, responseHandler) { + enqueue_ajax({ + url: url, + method: 'POST', + responseHandler: responseHandler + }); + } + + /** + * @title clear + * @description - clear the queue of pending ajax requests. + */ + function clear() { + pending = []; + } + + // Expose clear(), get() and post(). + return { + clear: clear, + get: get, + post: post + } +} diff --git a/src/main/resources/static/js/pipelineBox.js b/src/main/resources/static/js/pipelineBox.js index 760076c..96fee3f 100644 --- a/src/main/resources/static/js/pipelineBox.js +++ b/src/main/resources/static/js/pipelineBox.js @@ -1,4 +1,6 @@ -let pipelineService = PipelineService($); +// Create a default AjaxSequencer and pass it to PipelineService. +let ajaxSequencer = AjaxSequencer($); +let pipelineService = PipelineService($, ajaxSequencer); /** * @component Page Header - pretty much static. @@ -39,6 +41,8 @@ const pipelinegrid = Vue.component("pipelinegrid", { `, mounted() { pipelineService.getPipelines((names) => { + // Empty out app.pipelines in case we're navigating back from a detail page. + app.pipelines.splice([]); for (let i = 0; i < names.length; i++) { app.pipelines.push(names[i]); } @@ -311,6 +315,11 @@ const router = new VueRouter({ routes // short for `routes: routes` }); +router.beforeEach((to, from, next) => { + ajaxSequencer.clear(); + next(); +}); + let app = new Vue({ el: "#app", router: router, diff --git a/src/main/resources/static/js/pipelineService.js b/src/main/resources/static/js/pipelineService.js index 688091c..cbe4cfb 100644 --- a/src/main/resources/static/js/pipelineService.js +++ b/src/main/resources/static/js/pipelineService.js @@ -1,25 +1,20 @@ -let PipelineService = function (jquery) { +let PipelineService = function (jquery, as) { + + // If no AjaxSequencer was passed in, use the jQuery instance directly. + as = as || jquery; let getPipelines = function (responseHandler) { - jquery.ajax({ - dataType: "json", - url: "/pipelines", - success: function (response) { - const listOfPipelineNames = []; - for (let i = 0; i < response.length; i++) { - listOfPipelineNames.push(response[i].name); - } - responseHandler(listOfPipelineNames); - } - }); - }, + as.get('/pipelines', function (response) { + const listOfPipelineNames = []; + for (let i = 0; i < response.length; i++) { + listOfPipelineNames.push(response[i].name); + } + responseHandler(listOfPipelineNames); + }); + }, getPipelineDetailFromAWS = function (pipelineName, responseHandler) { - jquery.ajax({ - dataType: "json", - url: "/pipeline/" + pipelineName, - success: function (response) { + as.get("/pipeline/" + pipelineName, function(response) { responseHandler(response); - } }); }, parsePipelineState = function (stageState, commitMessage) { diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 85f3494..2c320e9 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -61,6 +61,8 @@ + + From 52789160c68000198584aa85ab581a7cef6c1f2a Mon Sep 17 00:00:00 2001 From: gyachuk Date: Wed, 19 Sep 2018 17:04:17 -0700 Subject: [PATCH 46/86] AjaxSequencer now uses promises --- src/main/resources/static/js/ajaxSequencer.js | 40 ++++++++++++------- src/main/resources/static/js/pipelineBox.js | 1 + .../resources/static/js/pipelineService.js | 4 +- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/main/resources/static/js/ajaxSequencer.js b/src/main/resources/static/js/ajaxSequencer.js index 13c6cd7..7710e46 100644 --- a/src/main/resources/static/js/ajaxSequencer.js +++ b/src/main/resources/static/js/ajaxSequencer.js @@ -24,17 +24,29 @@ let AjaxSequencer = function($, limit) { ++active; // Fire the request, and on success call the supplied responseHandler. // In all cases (even on error), start the next queued request. - $.ajax(pend.url, { method: pend.method }).done(pend.responseHandler).always(function() { - // Request has finished. See if there is a subsequent one to start. - --active; - pend = pending.shift(); // Shift from the front of the array, so it forms a FIFO queue. - if (pend) { - enqueue_ajax(pend); - } - }); + return $.ajax(pend.url, { method: pend.method }).done(function(response) { + console.log('ajax.done', pend.promise); + if (pend.promise) { + pend.promise.resolve(response); + } + }).fail(function(response) { + console.log('ajax.fail', pend.promise); + if (pend.promise) { + pend.reject(response); + } + }).always(function() { + // Request has finished. See if there is a subsequent one to start. + --active; + pend = pending.shift(); // Shift from the front of the array, so it forms a FIFO queue. + if (pend) { + enqueue_ajax(pend); + } + }); } else { // Too many active already. Push onto pending and wait for an active request to complete. pending.push(pend); // Push onto the end of the array. + pend.promise = $.Deferred(); + return pend.promise; } } @@ -42,11 +54,10 @@ let AjaxSequencer = function($, limit) { * @title get * @description - enqueue a GET request. */ - function get(url, responseHandler) { - enqueue_ajax({ + function get(url) { + return enqueue_ajax({ url: url, - method: 'GET', - responseHandler: responseHandler + method: 'GET' }); } @@ -55,10 +66,9 @@ let AjaxSequencer = function($, limit) { * @description - enqueue a POST request. */ function post(url, responseHandler) { - enqueue_ajax({ + return enqueue_ajax({ url: url, - method: 'POST', - responseHandler: responseHandler + method: 'POST' }); } diff --git a/src/main/resources/static/js/pipelineBox.js b/src/main/resources/static/js/pipelineBox.js index 96fee3f..05754ff 100644 --- a/src/main/resources/static/js/pipelineBox.js +++ b/src/main/resources/static/js/pipelineBox.js @@ -2,6 +2,7 @@ let ajaxSequencer = AjaxSequencer($); let pipelineService = PipelineService($, ajaxSequencer); + /** * @component Page Header - pretty much static. */ diff --git a/src/main/resources/static/js/pipelineService.js b/src/main/resources/static/js/pipelineService.js index cbe4cfb..8fb90a8 100644 --- a/src/main/resources/static/js/pipelineService.js +++ b/src/main/resources/static/js/pipelineService.js @@ -4,7 +4,7 @@ let PipelineService = function (jquery, as) { as = as || jquery; let getPipelines = function (responseHandler) { - as.get('/pipelines', function (response) { + as.get('/pipelines').done(function (response) { const listOfPipelineNames = []; for (let i = 0; i < response.length; i++) { listOfPipelineNames.push(response[i].name); @@ -13,7 +13,7 @@ let PipelineService = function (jquery, as) { }); }, getPipelineDetailFromAWS = function (pipelineName, responseHandler) { - as.get("/pipeline/" + pipelineName, function(response) { + as.get("/pipeline/" + pipelineName).done(function(response) { responseHandler(response); }); }, From 812ae2cb6093023db1cd9ca35b912642b74691a2 Mon Sep 17 00:00:00 2001 From: gyachuk Date: Wed, 19 Sep 2018 21:26:03 -0700 Subject: [PATCH 47/86] Better use of promises. Rewrite AjaxSequencer to always return promises. --- src/main/resources/static/js/ajaxSequencer.js | 96 +++++++++++-------- src/main/resources/static/js/pipelineBox.js | 6 +- .../resources/static/js/pipelineService.js | 70 +++++++------- 3 files changed, 90 insertions(+), 82 deletions(-) diff --git a/src/main/resources/static/js/ajaxSequencer.js b/src/main/resources/static/js/ajaxSequencer.js index 7710e46..60c0298 100644 --- a/src/main/resources/static/js/ajaxSequencer.js +++ b/src/main/resources/static/js/ajaxSequencer.js @@ -1,58 +1,68 @@ /** - * @component AjaxSequencer - * @parameters - * $ - instance of jQuery - * limit - maximum number of simultaneous Ajax calls. Default is 3. - * + * @function AjaxSequencer + * * @description Execute Ajax requests in sequence, allowing no more than `limit` to be simultaneously in-flight. + * @param {*} $ - instance of jQuery + * @param {number} limit - maximum number of simultaneous Ajax calls. Default is 3. * * @methods - * .get(url, responseHandler) - enqueues $.get(url). On complete, call `responseHandler` and start the next one. - * .post(url, responseHandler) - enqueues $.post(url). On complete, call `responseHandler` and start the next one. - * .clear() - clears all queued requests, but allows in-flight requests to complete. + * .get(url) - enqueues $.get(url) and returns a promise for when it is done.. + * .post(url) - enqueues $.post(url) and returns a promise for when it is done. + * .clear() - clears all queued requests, but allows in-flight requests to complete. */ let AjaxSequencer = function($, limit) { limit = limit || 3; // Set default of 3. - let active = 0; // Number of currently active ajax requests. + let active = 0; // Number of currently active ajax requests (including queued requests). let pending = []; // Queue of pending ajax requests. + /** + * @function enqueue_ajax + * @description Create a deferred that, when resolved, fires the requested Ajax query, and returns a promise + * that resolves to the response to that query. Resolve it right away to start the request, if + * the number of in-flight requests is lower than `limit`. Otherwise, append it to a FIFO queue. + * Queue elements are popped off (and the corresponding request is started) when a request completes. + * @param {@} pend + * @private + */ function enqueue_ajax(pend) { - if (active < limit) { - // Under the limit. Start this request immediately. - ++active; - // Fire the request, and on success call the supplied responseHandler. - // In all cases (even on error), start the next queued request. - return $.ajax(pend.url, { method: pend.method }).done(function(response) { - console.log('ajax.done', pend.promise); - if (pend.promise) { - pend.promise.resolve(response); - } - }).fail(function(response) { - console.log('ajax.fail', pend.promise); - if (pend.promise) { - pend.reject(response); - } - }).always(function() { - // Request has finished. See if there is a subsequent one to start. - --active; - pend = pending.shift(); // Shift from the front of the array, so it forms a FIFO queue. - if (pend) { - enqueue_ajax(pend); - } - }); + // This is the $.Deferred that is resolved when it is time to fire the query. + pend.deferred = $.Deferred(); + + // This promise is the one that is returned to the caller. It will return the results of the $.ajax query. + let promise = pend.deferred.then(function() { + // When this $.Deferred is resolved, it fires the requested query and returns the results. + // Because this is a .then(), the results of the $.ajax() call are returned through promise.done(). + return $.ajax(pend.url, { method: pend.method }); + }).always(function() { + // A query has completed. Decrement the number of `active` requests (which is the sum of `limit` plus pending). + --active; + pend = pending.shift(); // Pull a request from the front of the array (FIFO queue). + if (pend) { + // If we have a pending request, resolve it now, which will invoke pend.deferred.then(), + // and will fire off the associated Ajax request. + pend.deferred.resolve(); + } + }); + + if (active++ < limit) { + // If we haven't reached `limit` yet, resolve this $.Deferred() immediately, so the query starts right away. + pend.deferred.resolve(); } else { - // Too many active already. Push onto pending and wait for an active request to complete. - pending.push(pend); // Push onto the end of the array. - pend.promise = $.Deferred(); - return pend.promise; + // We've reached the `limit` so just queue this one up. It will fire when its turn comes in .always() above. + pending.push(pend); } + + // Return the promise (with the results of the Ajax request) to the caller. If the `.then()` fails, this + // entire promise will fail. + return promise; } /** - * @title get - * @description - enqueue a GET request. + * @function get + * @description - enqueue a GET request and return a promise. + * @param {string} url */ function get(url) { return enqueue_ajax({ @@ -62,10 +72,11 @@ let AjaxSequencer = function($, limit) { } /** - * @title post + * @function post * @description - enqueue a POST request. + * @param {string} url */ - function post(url, responseHandler) { + function post(url) { return enqueue_ajax({ url: url, method: 'POST' @@ -73,11 +84,12 @@ let AjaxSequencer = function($, limit) { } /** - * @title clear + * @function clear * @description - clear the queue of pending ajax requests. */ function clear() { - pending = []; + active -= pending.length; + pending = []; } // Expose clear(), get() and post(). diff --git a/src/main/resources/static/js/pipelineBox.js b/src/main/resources/static/js/pipelineBox.js index 05754ff..8f71fe7 100644 --- a/src/main/resources/static/js/pipelineBox.js +++ b/src/main/resources/static/js/pipelineBox.js @@ -41,7 +41,7 @@ const pipelinegrid = Vue.component("pipelinegrid", { `, mounted() { - pipelineService.getPipelines((names) => { + pipelineService.getPipelines().done((names) => { // Empty out app.pipelines in case we're navigating back from a detail page. app.pipelines.splice([]); for (let i = 0; i < names.length; i++) { @@ -80,7 +80,7 @@ const pipeline = Vue.component("pipeline", { router.push('/card/' + this.pipelineName); }, getPipelineDetails: function(pipelineName) { - pipelineService.getPipelineDetails(pipelineName, (stages) => this.stages = stages); + pipelineService.getPipelineDetails(pipelineName).done((stages) => this.stages = stages); } }, mounted() { @@ -289,7 +289,7 @@ const pipelinecard = Vue.component("pipelinecard", { router.back(); }, getPipelineDetails: function(pipelineName) { - pipelineService.getPipelineDetails(pipelineName, (stages) => this.stages = stages); + pipelineService.getPipelineDetails(pipelineName).done((stages) => this.stages = stages); } }, mounted() { diff --git a/src/main/resources/static/js/pipelineService.js b/src/main/resources/static/js/pipelineService.js index 8fb90a8..cc532c7 100644 --- a/src/main/resources/static/js/pipelineService.js +++ b/src/main/resources/static/js/pipelineService.js @@ -3,45 +3,41 @@ let PipelineService = function (jquery, as) { // If no AjaxSequencer was passed in, use the jQuery instance directly. as = as || jquery; - let getPipelines = function (responseHandler) { - as.get('/pipelines').done(function (response) { - const listOfPipelineNames = []; - for (let i = 0; i < response.length; i++) { - listOfPipelineNames.push(response[i].name); - } - responseHandler(listOfPipelineNames); + function getPipelines() { + return as.get('/pipelines').then(function (response) { + const listOfPipelineNames = []; + for (let i = 0; i < response.length; i++) { + listOfPipelineNames.push(response[i].name); + } + return listOfPipelineNames; }); - }, - getPipelineDetailFromAWS = function (pipelineName, responseHandler) { - as.get("/pipeline/" + pipelineName).done(function(response) { - responseHandler(response); - }); - }, - parsePipelineState = function (stageState, commitMessage) { - const currentRevision = stageState.actionStates[0].currentRevision || {}; - const latestExecution = stageState.actionStates[0].latestExecution || {}; - const status = latestExecution.status || ''; - const errorDetails = latestExecution.errorDetails || {}; - return { - name: stageState.stageName, - revisionId: currentRevision.revisionId, - latestStatus: status.toLowerCase(), - lastStatusChange: latestExecution.lastStatusChange, - externalExecutionUrl: latestExecution.externalExecutionUrl, - errorDetails: errorDetails.message, - commitMessage: commitMessage - }; - }, - getPipelineDetails = function (pipelineName, responseHandler) { - let stages = []; - getPipelineDetailFromAWS(pipelineName, - function (response) { - for (let i = 0; i < response.stageStates.length; i++) { - stages.push(parsePipelineState(response.stageStates[i], response.commitMessage)); - } - responseHandler(stages); - }); + } + + function parsePipelineState(stageState, commitMessage) { + const currentRevision = stageState.actionStates[0].currentRevision || {}; + const latestExecution = stageState.actionStates[0].latestExecution || {}; + const status = latestExecution.status || ''; + const errorDetails = latestExecution.errorDetails || {}; + return { + name: stageState.stageName, + revisionId: currentRevision.revisionId, + latestStatus: status.toLowerCase(), + lastStatusChange: latestExecution.lastStatusChange, + externalExecutionUrl: latestExecution.externalExecutionUrl, + errorDetails: errorDetails.message, + commitMessage: commitMessage }; + } + + function getPipelineDetails(pipelineName) { + let stages = []; + return as.get("/pipeline/" + pipelineName).then(function(response) { + for (let i = 0; i < response.stageStates.length; i++) { + stages.push(parsePipelineState(response.stageStates[i], response.commitMessage)); + } + return stages; + }); + } return { getPipelines: getPipelines, From bed34c4c5a9e59c20b0cde4843b51198046441ea Mon Sep 17 00:00:00 2001 From: Mike Naik Date: Wed, 19 Sep 2018 23:49:35 -0700 Subject: [PATCH 48/86] Readme cleanup --- README.md | 84 ++++++++++++++++++++++++++----------------------------- 1 file changed, 39 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index e9b51db..be3c4be 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,65 @@ # AWS Codepipelines Dashboard -This is a spring boot app which serves a dashboard to see, the status -of your AWS Codepipelines. +This is a Spring Boot app which serves a dashboard to see the status of your AWS Codepipelines. -It uses the AWS Java client to fetch data from AWS. Please follow the -policy instructions below to provide access. This means that the computer -running the spring boot app must have network access to AWS. +It uses the AWS Java client to fetch data from AWS. Please follow the policy instructions below to provide access for development or set up a Role for Elastic Beanstlak deployment. For development, this means that the computer running the spring boot app must have network access to AWS. -You can also run the application on your local computer. For example -by running `mvn spring-boot:run` from the command line. There is also -a Dockerfile included to run the application in the cloud. -## Getting started with Docker -With the following command, you can run this application in a docker container: +## Getting Started +### With Java/Maven +You can run the application on your local computer by running `mvn spring-boot:run` from the command line after grabbing the source (assuming Maven is installed already). There is also a Dockerfile included to run the application in a container (local or remote). +After that, you can reach the application in a web browser at +```http://localhost:8080/``` +The terminal will stream the log of your application. + +### With Docker +After you have it running with Java/Maven (which builds it), assuming you have Docker installed and running, follow the guidelines in the docker_buildspec.yml to build a Docker image. To run the app in a Docker container: ``` docker run -p8080:8080 -v`echo $HOME/.aws`:/home/app/.aws:ro --name dashboard codecentric/aws-codepipelines-dashboard ``` -After start, you can reach the application via -```http://localhost:8080/``` - -This configuration assumes that you've already an AWS account with a running -AWS CLI on your development host. If you're having trouble with that, see below. +After start, you can reach the application from the same URL as above. This configuration assumes that you've already an AWS account with a running AWS CLI on your development host. If you're having trouble with that, see "_Instructions for Setting up AWS permission for Development_" below. -## Instructions for setting up AWS permission for Development - -You have to give/ensure the user mentioned in $HOME/.aws/credentials -a policy. The steps are: - -1) choose IAM -1) use "Policies" in navigation -1) search for "AWSCodePipelineFullAccess" -1) select Attach entities, select "Attach" -1) get your user -1) click "Attach policy" - -Check policies with this CLI command: +## Instructions for setting up AWS Permission for Development +You have to give/ensure the user mentioned in $HOME/.aws/credentials has the right policy. Check policies with this CLI command: ``` aws iam list-attached-user-policies --user-name ``` - Verify that the following entry is listed: ``` { - "PolicyName": "AWSCodePipelineFullAccess", - "PolicyArn": "arn:aws:iam::aws:policy/AWSCodePipelineFullAccess" + "PolicyName": "AWSCodePipelineReadOnlyAccess", + "PolicyArn": "arn:aws:iam::aws:policy/AWSCodePipelineReadOnlyAccess" } ``` +_AWSCodePipelineFullAccess_ will also work. If you do not have either of these, you need to attach the policy: +1. Log in to AWS as an Administrator or someone with IAM access +2. Choose IAM +3. Click "Policies" in Left Navigation +4. Search for "AWSCodePipelineReadOnlyAccess" +5. Select Attach entities, select "Attach" +6. Choose your user +7. click "Attach policy" -## Instructions for setting up a Production Deployment -### Notes ### -* The application only has access to CodePipeline in the region that the EB is deployed to -* If this data is sensitive, you might want to restrict access in the Security Group that gets created by Elastic Beanstalk (the wizard seems to always create a Security Group) -* Choose *Generic*->*Docker* for the Platform -* You will also need GitHub connectivity as well as CodeBuild, CodePipeline, and ElasticBeanstalk roles for this (the last 3 can be generated by AWS) +## Instructions for setting up a CI/CD Pipeline and Deployment on Elastic Beanstalk ## +### Notes +* The application can only report on CodePipelines in the region that the Elastic Beanstalk environment is deployed to +* If this data is sensitive, you might want to restrict access in the Security Group that gets created by Elastic Beanstalk (EB will always create a Security Group, so just modify the one that it creates after creation) +* Choose *Generic*->*Docker* for the Elastic Beanstalk Platform +* You will need to either create an EC2 role that hass the _AWSCodePipelineReadOnlyAccess_ managed policy attached to it, or attach that policy to the EC2 Role generated by Elatsic Beanstalk +* You will also need GitHub connectivity as well as CodeBuild, CodePipeline, and ElasticBeanstalk Service roles for this (the last 3 can be generated by AWS) +* The details of the EB environment are up to you to decide, but a basic single t1.micro seems to work fine for occasional needs -### Setup ### -1. Set up EC2 Role with AWSCodePipelineReadOnlyAccess -2. (Optional) Set up the Security Groups -3. Create the Elastic Beanstalk Environment with the EC2 role as the Instance Profile for the VM and Security Group, if created -4. Create a CodeBuild with buildspec.yml to build the Java artifacts (use the Amazon managed Ubuntu Java Runtime, you shouldn't need a VPC or artifacts) -5. Create a CodeBuild with eb_docker_build.yml to containerize the Java artifacts (use the Amazon managed Ubuntu Docker Runtime, ensure you specify the eb_docker_buildspec.yml, you shouldn't need a VPC or artifacts) -6. Create a CodePipeline with: +### Setup +1. (Optional) Set up EC2 Role with the managed Policy _AWSCodePipelineReadOnlyAccess_ attached to it +2. Create the Elastic Beanstalk Environment with the EC2 role as the _IAM Instance Profile_ in the *Security* settings if you have created it (if you autogenerate, attach the Managed Policy to the generated role) +3. Create a CodeBuild with *_buildspec.yml_* to build the Java artifacts (use the Amazon managed Ubuntu Java Runtime, you shouldn't need a VPC or artifacts, use an existing CodeBuild Service Role or generate a new one) +4. Create a CodeBuild with *_eb_docker_build.yml_* to containerize the Java artifacts (use the Amazon managed Ubuntu Docker Runtime, ensure you specify the eb_docker_buildspec.yml, you shouldn't need a VPC or artifacts, use an existing CodeBuild Service Role or generate a new one) +5. Create a CodePipeline with: 1. GitHub repo as the Source stage (you can use any version of the repo and any branch you see fit, as long as the necesssary files exist for CodeBuild to function) 2. The Java CodeBuild as the first part of the Build stage, with output artifacts tagged something like "_JavaArtifacts_" 3. The Containerize CodeBuild as the second part of the Build stage, with the input artifacts as the output artifats of the Java build (in this example, _JavaArtifacts_) and the output artifacts tagged as something like "_EBApp_" 4. (Optional) Set up a Human approval step before deployment if uptime is critical 5. Set up a Deploy stage to your Elastic Beanstlak environment from Step 3 with the Input artifacts as the output from the Contaizer step (in this example, _EBApp_) -7. Release the Change to trigger a new build and deployment \ No newline at end of file +6. Release the Change to trigger a new build and deployment \ No newline at end of file From 30dc49cee08056951d274dbab7bbd62ba61f2475 Mon Sep 17 00:00:00 2001 From: gyachuk Date: Thu, 20 Sep 2018 10:31:01 -0700 Subject: [PATCH 49/86] Fixed th:src syntax for ajaxSequencer.js script --- src/main/resources/templates/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 2c320e9..df0aedf 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -61,7 +61,7 @@ - + From e133be078f9d7c0e7ee89157bf5c8950a2a33858 Mon Sep 17 00:00:00 2001 From: gyachuk Date: Thu, 20 Sep 2018 10:43:09 -0700 Subject: [PATCH 50/86] Ignore .vscode folder --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 7acc016..8b09bf6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,8 @@ target .idea *.iml +### Visual Studio ### +.vscode + ### Local data files ### /src/main/resources/data/ \ No newline at end of file From 7272d9785792ba8f86f0b1de5aab7c6126c93a10 Mon Sep 17 00:00:00 2001 From: gyachuk Date: Wed, 26 Sep 2018 08:33:23 -0700 Subject: [PATCH 51/86] Add loading indicator, and sort pipelines most recent changes first --- src/main/resources/static/js/pipelineBox.js | 53 +++++-- src/main/resources/templates/index.html | 144 +++++++++++++++++++- 2 files changed, 187 insertions(+), 10 deletions(-) diff --git a/src/main/resources/static/js/pipelineBox.js b/src/main/resources/static/js/pipelineBox.js index 8f71fe7..d601a9d 100644 --- a/src/main/resources/static/js/pipelineBox.js +++ b/src/main/resources/static/js/pipelineBox.js @@ -2,7 +2,6 @@ let ajaxSequencer = AjaxSequencer($); let pipelineService = PipelineService($, ajaxSequencer); - /** * @component Page Header - pretty much static. */ @@ -43,10 +42,46 @@ const pipelinegrid = Vue.component("pipelinegrid", { mounted() { pipelineService.getPipelines().done((names) => { // Empty out app.pipelines in case we're navigating back from a detail page. - app.pipelines.splice([]); + // app.pipelines.splice([]); + console.log('pipeline start'); + const startTime = new Date(); + app.loading = true; + + let promises = []; + let pipelines = []; for (let i = 0; i < names.length; i++) { - app.pipelines.push(names[i]); + // app.pipelines.push(names[i]); + promises.push(function(name, stages, i) { + let promise = pipelineService.getPipelineDetails(name); + promise.done((pipeline) => pipelines[i] = { name: name, pipeline: pipeline }); + return promise; + }(names[i], pipelines, i)); } + + $.when.apply($, promises).done(() => { + console.log('$.when', pipelines.length, pipelines); + + pipelines = pipelines.sort(function(a, b) { + a = latestStageChangeTime(a.pipeline); + b = latestStageChangeTime(b.pipeline); + return b - a; + }); + + console.log('sorted', pipelines.length, pipelines); + + // Hack for now. Use the sorted names. Data will have to be re-fetched. Better to use existing stages data. + app.pipelines.splice(0, app.pipelines.length, ...pipelines.map((pipeline) => pipeline.name)); + app.loading = false; + + const endTime = new Date(); + console.log('pipeline fetch took', moment.duration(endTime.getTime() - startTime.getTime()).humanize()); + + function latestStageChangeTime(stages) { + let statusChanges = stages.map((stage) => stage.lastStatusChange); + let maxStatusChange = Math.max.apply(Math, statusChanges); + return maxStatusChange; + } + }); }); } }); @@ -65,7 +100,7 @@ const pipeline = Vue.component("pipeline", {
    • - +
    @@ -109,8 +144,6 @@ const pipeline = Vue.component("pipeline", { /** * @component Pipeline Header - contains information about the entire Pipeline. - * - * Clicking of the body navigates to a card detail route. */ const pipelineheader = Vue.component("pipelineheader", { props: ["pipelineName", "stages"], // attribute of tag @@ -325,11 +358,13 @@ let app = new Vue({ el: "#app", router: router, data: { - pipelines: pipelines + pipelines: pipelines, + loading: true }, methods: {} }); // Refresh every 60 seconds. -window.setInterval(() => router.go(0), 60000); - +if (!window.location.search) { + window.setInterval(() => router.go(0), 60000); +} diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index df0aedf..1c9d1a7 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -37,6 +37,133 @@ + + @@ -47,7 +174,22 @@ -
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    From d607dea9601ad9a0803d23ba17ac3c3434c05f51 Mon Sep 17 00:00:00 2001 From: gyachuk Date: Wed, 26 Sep 2018 13:19:35 -0700 Subject: [PATCH 52/86] Cleaned out unused code; increased size of loading indicator. --- src/main/resources/static/js/pipelineBox.js | 70 ++++++--------------- src/main/resources/templates/index.html | 4 +- 2 files changed, 21 insertions(+), 53 deletions(-) diff --git a/src/main/resources/static/js/pipelineBox.js b/src/main/resources/static/js/pipelineBox.js index d601a9d..2313055 100644 --- a/src/main/resources/static/js/pipelineBox.js +++ b/src/main/resources/static/js/pipelineBox.js @@ -3,7 +3,7 @@ let ajaxSequencer = AjaxSequencer($); let pipelineService = PipelineService($, ajaxSequencer); /** - * @component Page Header - pretty much static. + * @component Page Header - pretty much static. Take the text and insert it into the header. */ const pageheader = Vue.component("pageheader", { template: ` @@ -36,21 +36,19 @@ const pipelinegrid = Vue.component("pipelinegrid", { props: ["pipelines"], template: ` <div class="card-deck"> - <pipeline v-for="item in pipelines" v-bind:pipeline="item" /> + <pipeline v-for="item in pipelines" v-bind:pipelineName="item.name" v-bind:pipeline="item.pipeline" /> </div> `, mounted() { pipelineService.getPipelines().done((names) => { - // Empty out app.pipelines in case we're navigating back from a detail page. - // app.pipelines.splice([]); - console.log('pipeline start'); - const startTime = new Date(); + // Show the loading indicator. app.loading = true; let promises = []; let pipelines = []; + for (let i = 0; i < names.length; i++) { - // app.pipelines.push(names[i]); + // Fetch the details for each pipeline. Do this in a closure so we can track each promise. promises.push(function(name, stages, i) { let promise = pipelineService.getPipelineDetails(name); promise.done((pipeline) => pipelines[i] = { name: name, pipeline: pipeline }); @@ -58,30 +56,25 @@ const pipelinegrid = Vue.component("pipelinegrid", { }(names[i], pipelines, i)); } + // When all promises have completed, sort them with most recently changes first. $.when.apply($, promises).done(() => { - console.log('$.when', pipelines.length, pipelines); + // Sort the array of piplines. pipelines = pipelines.sort(function(a, b) { a = latestStageChangeTime(a.pipeline); b = latestStageChangeTime(b.pipeline); return b - a; }); - console.log('sorted', pipelines.length, pipelines); - - // Hack for now. Use the sorted names. Data will have to be re-fetched. Better to use existing stages data. - app.pipelines.splice(0, app.pipelines.length, ...pipelines.map((pipeline) => pipeline.name)); - app.loading = false; - - const endTime = new Date(); - console.log('pipeline fetch took', moment.duration(endTime.getTime() - startTime.getTime()).humanize()); + // Replace the contents of app.pipelines with these new (sorted) pipelines. + app.pipelines.splice(0, app.pipelines.length, ...pipelines); function latestStageChangeTime(stages) { let statusChanges = stages.map((stage) => stage.lastStatusChange); let maxStatusChange = Math.max.apply(Math, statusChanges); return maxStatusChange; } - }); + }).always(() => app.loading = false); }); } }); @@ -92,52 +85,22 @@ const pipelinegrid = Vue.component("pipelinegrid", { * Clicking of the body navigates to a card detail route. */ const pipeline = Vue.component("pipeline", { - props: ["pipeline"], // attribute of tag + props: ["pipeline", "pipelineName"], // attribute of tag template: ` <div v-bind:class="['card', 'bg-light', 'mb-4']" style="min-width: 350px" v-on:click="clickHandler"> <div class="card-body"> - <pipelineheader v-bind:pipelineName="pipeline" v-bind:stages="stages"/> + <pipelineheader v-bind:pipelineName="pipelineName" v-bind:stages="pipeline"/> </div> <ul class="list-group list-group-flush"> - <li v-for="item in stages"> + <li v-for="item in pipeline"> <stage v-bind:stage="item"/> </li> </ul> </div> `, - data: function() { - return { - stages: [] - }; - }, methods: { clickHandler: function() { router.push('/card/' + this.pipelineName); - }, - getPipelineDetails: function(pipelineName) { - pipelineService.getPipelineDetails(pipelineName).done((stages) => this.stages = stages); - } - }, - mounted() { - this.getPipelineDetails(this.pipeline); - }, - computed: { - pipelineName: function() { - return this.pipeline; - }, - borderClass: function() { - const isFailed = - this.stages.findIndex(item => item.latestStatus === "failed") !== -1; - const isBuilding = - this.stages.findIndex(item => item.latestStatus === "inprogress") !== - -1; - if (isFailed) { - return "border-danger"; - } else if (isBuilding) { - return "border-info"; - } else { - return "border-success"; - } } } }); @@ -171,6 +134,9 @@ const pipelineheader = Vue.component("pipelineheader", { this.getPipelineDetails(this.pipelineName, this.stages || []); } }, + mounted() { + this.getPipelineDetails(this.pipelineName, this.stages || []); + }, methods: { getPipelineDetails: function(pipelineName, stages) { let componentScope = this; @@ -322,7 +288,9 @@ const pipelinecard = Vue.component("pipelinecard", { router.back(); }, getPipelineDetails: function(pipelineName) { - pipelineService.getPipelineDetails(pipelineName).done((stages) => this.stages = stages); + pipelineService.getPipelineDetails(pipelineName) + .done((stages) => this.stages = stages) + .always(() => app.loading = false); } }, mounted() { diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 1c9d1a7..34f6f08 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -153,8 +153,8 @@ .loading { position: fixed; z-index: 999; - height: 2em; - width: 2em; + height: 10em; + width: 10em; overflow: show; margin: auto; top: 0; From 52defc91f9ecd3c097442435020752b7b09c7aed Mon Sep 17 00:00:00 2001 From: gyachuk <gyachuk@yahoo.com> Date: Fri, 28 Sep 2018 13:25:09 -0700 Subject: [PATCH 53/86] Added "?static" query param to avoid 60-second refresh --- src/main/resources/static/js/pipelineBox.js | 7 ++++--- src/main/resources/templates/index.html | 12 ++++++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/resources/static/js/pipelineBox.js b/src/main/resources/static/js/pipelineBox.js index 2313055..a516cb5 100644 --- a/src/main/resources/static/js/pipelineBox.js +++ b/src/main/resources/static/js/pipelineBox.js @@ -332,7 +332,8 @@ let app = new Vue({ methods: {} }); -// Refresh every 60 seconds. -if (!window.location.search) { - window.setInterval(() => router.go(0), 60000); +// Refresh every 60 seconds, unless "?static" is part of the URL. +const refresh = ! window.location.search.substr(1).split("&").map((elem) => elem === "static").reduce((a,b) => a || b); +if (refresh) { + window.setInterval(() => router.go(0), 10000); } diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 34f6f08..ba85b12 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -1,10 +1,14 @@ <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> +<!-- --------------------------------------------------------------- --> +<!-- Append "?static" query param to avoid automatic page refreshes. --> +<!-- --------------------------------------------------------------- --> + <head> - <!-- --> + <!-- --------------------- --> <!-- Configuration Section --> - <!-- --> + <!-- --------------------- --> <!-- Change the title of the Dashboard here --> <title>CodePipeline Dashboard @@ -30,9 +34,9 @@ ]; - + - + From 35977f54b23cda5f9352318ad6b76d77d580c800 Mon Sep 17 00:00:00 2001 From: gyachuk Date: Fri, 28 Sep 2018 13:35:13 -0700 Subject: [PATCH 54/86] List all actions states for each stage. --- src/main/resources/static/js/pipelineService.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/resources/static/js/pipelineService.js b/src/main/resources/static/js/pipelineService.js index cc532c7..1f49951 100644 --- a/src/main/resources/static/js/pipelineService.js +++ b/src/main/resources/static/js/pipelineService.js @@ -13,13 +13,13 @@ let PipelineService = function (jquery, as) { }); } - function parsePipelineState(stageState, commitMessage) { - const currentRevision = stageState.actionStates[0].currentRevision || {}; - const latestExecution = stageState.actionStates[0].latestExecution || {}; + function parsePipelineActionState(actionState, commitMessage) { + const currentRevision = actionState.currentRevision || {}; + const latestExecution = actionState.latestExecution || {}; const status = latestExecution.status || ''; const errorDetails = latestExecution.errorDetails || {}; return { - name: stageState.stageName, + name: actionState.actionName, revisionId: currentRevision.revisionId, latestStatus: status.toLowerCase(), lastStatusChange: latestExecution.lastStatusChange, @@ -33,7 +33,11 @@ let PipelineService = function (jquery, as) { let stages = []; return as.get("/pipeline/" + pipelineName).then(function(response) { for (let i = 0; i < response.stageStates.length; i++) { - stages.push(parsePipelineState(response.stageStates[i], response.commitMessage)); + const stageState = response.stageStates[i]; + for (let j=0; j < stageState.actionStates.length; j++) { + let actionState = stageState.actionStates[j]; + stages.push(parsePipelineActionState(actionState, response.commitMessage)); + } } return stages; }); From 17c657ddc96d86e9b2ec329522e0d0a239ac56be Mon Sep 17 00:00:00 2001 From: gyachuk Date: Tue, 2 Oct 2018 13:16:44 -0700 Subject: [PATCH 55/86] Don't duplicate commit message in every stage. --- src/main/resources/static/js/pipelineBox.js | 46 ++++++++----------- .../resources/static/js/pipelineService.js | 15 ++++-- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/src/main/resources/static/js/pipelineBox.js b/src/main/resources/static/js/pipelineBox.js index 2313055..d2faf20 100644 --- a/src/main/resources/static/js/pipelineBox.js +++ b/src/main/resources/static/js/pipelineBox.js @@ -36,7 +36,7 @@ const pipelinegrid = Vue.component("pipelinegrid", { props: ["pipelines"], template: `
    - +
    `, mounted() { @@ -51,7 +51,7 @@ const pipelinegrid = Vue.component("pipelinegrid", { // Fetch the details for each pipeline. Do this in a closure so we can track each promise. promises.push(function(name, stages, i) { let promise = pipelineService.getPipelineDetails(name); - promise.done((pipeline) => pipelines[i] = { name: name, pipeline: pipeline }); + promise.done((pipeline) => pipelines[i] = pipeline); return promise; }(names[i], pipelines, i)); } @@ -61,8 +61,8 @@ const pipelinegrid = Vue.component("pipelinegrid", { // Sort the array of piplines. pipelines = pipelines.sort(function(a, b) { - a = latestStageChangeTime(a.pipeline); - b = latestStageChangeTime(b.pipeline); + a = latestStageChangeTime(a.stages); + b = latestStageChangeTime(b.stages); return b - a; }); @@ -85,22 +85,22 @@ const pipelinegrid = Vue.component("pipelinegrid", { * Clicking of the body navigates to a card detail route. */ const pipeline = Vue.component("pipeline", { - props: ["pipeline", "pipelineName"], // attribute of tag + props: ["pipeline"], // attribute of tag template: `
    - +
      -
    • - +
    • +
    `, methods: { clickHandler: function() { - router.push('/card/' + this.pipelineName); + router.push('/card/' + this.pipeline.name); } } }); @@ -109,16 +109,16 @@ const pipeline = Vue.component("pipeline", { * @component Pipeline Header - contains information about the entire Pipeline. */ const pipelineheader = Vue.component("pipelineheader", { - props: ["pipelineName", "stages"], // attribute of tag + props: ["pipeline", "stages"], // attribute of tag template: ` -
    {{ pipelineName }}
    +
    {{ pipeline.name }}

    Started {{ startDate }} took {{ duration }}
    - {{ commitMessage }} + {{ pipeline.commitMessage }}

    `, @@ -131,14 +131,14 @@ const pipelineheader = Vue.component("pipelineheader", { }, watch: { stages: function(stages) { - this.getPipelineDetails(this.pipelineName, this.stages || []); + this.getPipelineDetails(this.stages || []); } }, mounted() { - this.getPipelineDetails(this.pipelineName, this.stages || []); + this.getPipelineDetails(this.stages || []); }, methods: { - getPipelineDetails: function(pipelineName, stages) { + getPipelineDetails: function(stages) { let componentScope = this; // Start off with the largest min value, unless there are no stages at all. // In that case, use zero so we end up with a zero-length duration: (max - min) @@ -150,13 +150,6 @@ const pipelineheader = Vue.component("pipelineheader", { for (let i = 0; i < stages.length; i++) { let stage = stages[i]; - // Anything that needs to be processed for *all* stages needs to go at the top of the loop. - if (!commitMessage) { - commitMessage = stage.commitMessage; - } - - // After this point, only duration calculations. - // // We really only care about stages with "succeeded" status. // We want to compute the duration as the time from the time of the first stage, up to the time // of the first stage that hasn't "succeeded". Note that this could be the first stage, in which @@ -181,7 +174,6 @@ const pipelineheader = Vue.component("pipelineheader", { } componentScope.duration = moment.duration(max - min).humanize(); componentScope.startDate = moment(min).fromNow(); - componentScope.commitMessage = commitMessage; } } }); @@ -269,10 +261,10 @@ const pipelinecard = Vue.component("pipelinecard", { - +
      -
    • +
    @@ -280,7 +272,7 @@ const pipelinecard = Vue.component("pipelinecard", { `, data: function() { return { - stages: [] + pipeline: {} }; }, methods: { @@ -289,7 +281,7 @@ const pipelinecard = Vue.component("pipelinecard", { }, getPipelineDetails: function(pipelineName) { pipelineService.getPipelineDetails(pipelineName) - .done((stages) => this.stages = stages) + .done((pipeline) => this.pipeline = pipeline) .always(() => app.loading = false); } }, diff --git a/src/main/resources/static/js/pipelineService.js b/src/main/resources/static/js/pipelineService.js index 1f49951..db3cfdd 100644 --- a/src/main/resources/static/js/pipelineService.js +++ b/src/main/resources/static/js/pipelineService.js @@ -13,7 +13,7 @@ let PipelineService = function (jquery, as) { }); } - function parsePipelineActionState(actionState, commitMessage) { + function parsePipelineActionState(actionState) { const currentRevision = actionState.currentRevision || {}; const latestExecution = actionState.latestExecution || {}; const status = latestExecution.status || ''; @@ -24,22 +24,27 @@ let PipelineService = function (jquery, as) { latestStatus: status.toLowerCase(), lastStatusChange: latestExecution.lastStatusChange, externalExecutionUrl: latestExecution.externalExecutionUrl, - errorDetails: errorDetails.message, - commitMessage: commitMessage + errorDetails: errorDetails.message }; } function getPipelineDetails(pipelineName) { let stages = []; return as.get("/pipeline/" + pipelineName).then(function(response) { + let pipelineDetails = { + name: pipelineName, + commitMessage: response.commitMessage, + stages: [] + }; + for (let i = 0; i < response.stageStates.length; i++) { const stageState = response.stageStates[i]; for (let j=0; j < stageState.actionStates.length; j++) { let actionState = stageState.actionStates[j]; - stages.push(parsePipelineActionState(actionState, response.commitMessage)); + pipelineDetails.stages.push(parsePipelineActionState(actionState)); } } - return stages; + return pipelineDetails; }); } From fd8097119858975853c52954ca59748ba39752f9 Mon Sep 17 00:00:00 2001 From: gyachuk Date: Tue, 2 Oct 2018 14:01:41 -0700 Subject: [PATCH 56/86] Fixed comments in index.html, so SAX parser doesn't puke --- src/main/resources/templates/index.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index ba85b12..9d95047 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -1,14 +1,14 @@ - + - + - + - + CodePipeline Dashboard @@ -34,9 +34,9 @@ ]; - + - + From 91f0f526e93b80004a0ea562730855b420c1f673 Mon Sep 17 00:00:00 2001 From: gyachuk Date: Wed, 3 Oct 2018 11:53:18 -0700 Subject: [PATCH 57/86] refresh interval back to 1 minute --- src/main/resources/static/js/pipelineBox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/static/js/pipelineBox.js b/src/main/resources/static/js/pipelineBox.js index 952b435..311eaf2 100644 --- a/src/main/resources/static/js/pipelineBox.js +++ b/src/main/resources/static/js/pipelineBox.js @@ -327,5 +327,5 @@ let app = new Vue({ // Refresh every 60 seconds, unless "?static" is part of the URL. const refresh = ! window.location.search.substr(1).split("&").map((elem) => elem === "static").reduce((a,b) => a || b); if (refresh) { - window.setInterval(() => router.go(0), 10000); + window.setInterval(() => router.go(0), 60000); } From 1a45195440b52df6dbd41c8a41204fdc31969905 Mon Sep 17 00:00:00 2001 From: gyachuk Date: Mon, 8 Oct 2018 09:23:00 -0700 Subject: [PATCH 58/86] case-insensitive, partial matches on stage names --- src/main/resources/static/js/pipelineBox.js | 4 ++-- src/main/resources/templates/index.html | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/resources/static/js/pipelineBox.js b/src/main/resources/static/js/pipelineBox.js index 0fd8434..e39e587 100644 --- a/src/main/resources/static/js/pipelineBox.js +++ b/src/main/resources/static/js/pipelineBox.js @@ -208,10 +208,10 @@ const stage = Vue.component("stage", { return false; }, matchesStage: function(needs, name) { - return !!name.match(needs.stage); + return name.toLowerCase().indexOf(needs.stage.toLowerCase()) >= 0; }, matchesStatus: function(needs, status) { - return !!status.match(needs.status); + return status.indexOf(needs.status) >= 0; } }, computed: { diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 444f35a..ce1ed51 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -25,7 +25,9 @@ - + + + + + From 553f476851a6217d4ea57d10567578aa4af0f16d Mon Sep 17 00:00:00 2001 From: Mike Naik Date: Sun, 25 Jul 2021 21:46:50 -0700 Subject: [PATCH 76/86] Update AWS SDK to support IMDSv2 in the Region Provider Chain --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cdf5aeb..476b276 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ UTF-8 1.8 1.16.22 - 1.11.172 + 1.12.31 3.2.1 2.5.2 2.19.1 From eaa7da1c65c7d1117a102d1b71357820a3ee9fd4 Mon Sep 17 00:00:00 2001 From: Mike Naik Date: Tue, 27 Jul 2021 17:58:51 -0700 Subject: [PATCH 77/86] Automated Deployment via CF Template w/ README --- README.md | 54 +++--- deployment-cft.yml | 468 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 499 insertions(+), 23 deletions(-) create mode 100644 deployment-cft.yml diff --git a/README.md b/README.md index 1a510ce..3e7c650 100644 --- a/README.md +++ b/README.md @@ -5,36 +5,20 @@ This is a Spring Boot app which serves a dashboard to see the status of your AWS It uses the AWS Java client to fetch data from AWS. Please follow the policy instructions below to provide access for development or set up a Role for Elastic Beanstlak deployment. For development, this means that the computer running the spring boot app must have network access to AWS. -## Getting Started +## Getting Started Locally ### With Java/Maven You can run the application on your local computer by running `mvn spring-boot:run` from the command line after grabbing the source (assuming Maven is installed already). There is also a Dockerfile included to run the application in a container (local or remote). After that, you can reach the application in a web browser at ```http://localhost:8080/``` The terminal will stream the log of your application. -#### Specifying AWS Region and AWS CLI Profile -Issue ```AWS_REGION="eu-west-1" AWS_PROFILE="ci" mvn spring-boot:run``` - ### With Docker After you have it running with Java/Maven (which builds it), assuming you have Docker installed and running, follow the guidelines in the docker_buildspec.yml to build a Docker image. To run the app in a Docker container: ``` docker run -p8080:8080 -v`echo $HOME/.aws`:/home/app/.aws:ro --name dashboard codecentric/aws-codepipelines-dashboard ``` After start, you can reach the application from the same URL as above. This configuration assumes that you've already an AWS account with a running AWS CLI on your development host. If you're having trouble with that, see "_Instructions for Setting up AWS permission for Development_" below. - -## Usage -### Display All Pipelines -Navigate to ```http://localhost:8080/``` - -### Display Some Pipelines -Navigate to ```http://localhost:8080/#/filtered/regexp``` to display all pipelines whose name matches the regexp. - -#### Examples -Navigate to ```http://localhost:8080/#/filtered/project-[ab]``` to display all pipelines whose name contains `project-a` or `project-b` -Navigate to ```http://localhost:8080/#/filtered/(project-alpha)|(project-beta)``` to display all pipelines whose name contains `project-alpha` or `project-beta` - - -## Instructions for setting up AWS Permission for Development +### Instructions for setting up AWS Permission for Development You have to give/ensure the user mentioned in $HOME/.aws/credentials has the right policy. Check policies with this CLI command: ``` aws iam list-attached-user-policies --user-name @@ -55,17 +39,41 @@ _AWSCodePipelineFullAccess_ will also work. If you do not have either of these, 6. Choose your user 7. click "Attach policy" +### Usage +#### Display All Pipelines +Navigate to ```http://localhost:8080/``` + +#### Display Some Pipelines +Navigate to ```http://localhost:8080/#/filtered/regexp``` to display all pipelines whose name matches the regexp. + +##### Examples +Navigate to ```http://localhost:8080/#/filtered/project-[ab]``` to display all pipelines whose name contains `project-a` or `project-b` +Navigate to ```http://localhost:8080/#/filtered/(project-alpha)|(project-beta)``` to display all pipelines whose name contains `project-alpha` or `project-beta` + + +## Instructions for manually setting up a CI/CD Pipeline and Deployment on Elastic Beanstalk + +### CloudFormation Automated Deployment Instructions +Simply deploy the `deployment-cft.yml` CloudFormation template via the AWS CloudFormation Console or CLI. +* The only thing you must provide is either a GitHub Access Token or the Secret String portion of a SecretsManager Secret that has a GitHub access token +* Many other existing resources can be resued as well, if provided + * The CloudFormation Template will automatically deploy any resources that you have not provided and set up ElasticBeanstalk in the Default VPC with a CodePipeline deploying to it +* You can also configure whether approval is required to deploy updates +* You can also specify which CodeBuild Image, ElasticBeanstalk SolutionStack, and Instance Type to use +* If you specify a Custom CName Prefix, the full dashboard URL will also be listed as a Stack output. -## Instructions for setting up a CI/CD Pipeline and Deployment on Elastic Beanstalk ## ### Notes -* The application can only report on CodePipelines in the region that the Elastic Beanstalk environment is deployed to -* If this data is sensitive, you might want to restrict access in the Security Group that gets created by Elastic Beanstalk (EB will always create a Security Group, so just modify the one that it creates after creation) +* The Dashboard will be available over HTTP on port 80 (no HTTPS, not 8080 like local development) +* The Dashboard can only report on CodePipelines in the region that the Elastic Beanstalk environment is deployed to +* If this data is sensitive, you might want to restrict access in the Security Group that gets created by Elastic Beanstalk (EB will always create a Security Group, you must modify the one that it creates after creation if doing this manually) + +#### Old Instructions for manual setup - DEPRECATED - USE THE ABOVE METHOD - RETAINED FOR CICD BACKWARDS COMPATIBILITY +##### Old Deployment Notes * Choose *Generic*->*Docker* for the Elastic Beanstalk Platform * You will need to either create an EC2 role that hass the _AWSCodePipelineReadOnlyAccess_ managed policy attached to it, or attach that policy to the EC2 Role generated by Elatsic Beanstalk * You will also need GitHub connectivity as well as CodeBuild, CodePipeline, and ElasticBeanstalk Service roles for this (the last 3 can be generated by AWS) * The details of the EB environment are up to you to decide, but a basic single t1.micro seems to work fine for occasional needs - -### Setup +#### Old, Manual, Tedious Setup 1. (Optional) Set up EC2 Role with the managed Policy _AWSCodePipelineReadOnlyAccess_ attached to it 2. Create the Elastic Beanstalk Environment with the EC2 role as the _IAM Instance Profile_ in the *Security* settings if you have created it (if you autogenerate, attach the Managed Policy to the generated role) 3. Create a CodeBuild with *_buildspec.yml_* to build the Java artifacts (use the Amazon managed Ubuntu Java Runtime, you shouldn't need a VPC or artifacts, use an existing CodeBuild Service Role or generate a new one) diff --git a/deployment-cft.yml b/deployment-cft.yml new file mode 100644 index 0000000..c5397d8 --- /dev/null +++ b/deployment-cft.yml @@ -0,0 +1,468 @@ +# This CloudFormation Template should do an end-to-end one-step deployment of the CodePipeline dashboard from source. +AWSTemplateFormatVersion: "2010-09-09" +Description: > + CodePipeline Dashboard Elastic Beanstalk Deployment and CICD Pipeline. It will deploy anything that you do not + supply. If a custom CNAME prefix is specified, the dashboard URL will be provided as an output of the template. + Elastic Beanstalk will always create the default Security Group, which allows open access on Port 80, that must be + manually locked down after stack creation if you want to limit access to the dashboard. + +Parameters: + GitHubToken: + Type: String + NoEcho: true + Description: > + GitHub OAuth/Personal Access Token for CodePipeline to use to pull source (even though repo is public). Only + needs repo read scope. Alternatively, the you can use an existing secret below. + SecretResolveString: + Type: String + Description: > + CloudFormation SecretsManage Resolve String formatted location of GitHub Access Token in the format + ':SecretString:::' for any values that are applicable. + "SecretString" is a required string (for example "MyGHSecretId:SecretString:MyJsonKey"): + https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html#dynamic-references-secretsmanager + ApprovalRequired: + Type: String + AllowedValues: + - 'Yes' + - 'No' + Default: 'No' + Description: Require Human Approval for Deployments from this pipeline? + Branch: + Type: String + Default: master + Description: Override the branch of the CodePipelnie repo to pull from GitHub for the CICD pipeline + S3Bucket: + Type: String + Description: The name of a pre-existing S3 bucket to use for temporary storage between stages of the CodePipeline + CodeBuildRoleArn: + Type: String + Description: The ARN of pre-existing the role for CodeBuild to use, should be able to write to S3 build bucket + CodePipelineRoleArn: + Type: String + Description: The Arn of pre-existing role for CodePipeline to use, should be able to read from S3 build bucket + EBServiceRoleArn: + Type: String + Description: The Arn of the pre-existing Elastic Beanstalk Service Role for Elastic Beanstalk to perform actions + InstanceProfileArn: + Type: String + Description: The Arn of the pre-existing Instance Profile for the Elastic Beanstalk EC2 instances to use + CodeBuildImage: + Type: String + Default: 'aws/codebuild/amazonlinux2-x86_64-standard:3.0' + Description: The CodeBuild Image to use to build the application + EBSolutionStack: + Type: String + Default: '64bit Amazon Linux 2 v3.2.3 running Corretto 8' #EOL June 30, 2022 + Description: The ElasticBeanstalk Solution Stack to use, should be using the Corretto8 Platform + InstanceType: + Type: String + Default: t3.micro + Description: The instance type for the single deployed instance, must be supported in this region + CNamePrefix: + Type: String + MaxLength: 63 + Description: Desired unique CNAME prefix (4-63 chars) for the EB Environment, otherwise automatically generated + ConstraintDescription: Must be less than 63 charcaters + +Metadata: + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: "GitHub Access - Provide only one of the following (Required)" + Parameters: + - GitHubToken + - SecretResolveString + - Label: + default: "Pre-Existing Components (Optional)" + Parameters: + - S3Bucket + - CodeBuildRoleArn + - CodePipelineRoleArn + - EBServiceRoleArn + - InstanceProfileArn + - Label: + default: "Overrides (Optional)" + Parameters: + - ApprovalRequired + - Branch + - CodeBuildImage + - EBSolutionStack + - InstanceType + - CNamePrefix + ParameterLabels: + GitHubToken: + default: GitHub Access Token + SecretResolveString: + default: GitHub Secret CloudFormation Resolve String + S3Bucket: + default: S3 Bucket + CodeBuildRoleArn: + default: CodeBuild Role + CodePipelineRoleArn: + default: CodePipeline Role + EBServiceRoleArn: + default: Elastic Beanstalk Service Role + InstanceProfileArn: + default: EC2 Instance Profile + ApprovalRequired: + default: Manual Approval Required + Branch: + default: Repo Branch + CodeBuildImage: + default: CodeBuild Image + EBSolutionStack: + default: Elastic Beanstalk Solution Stack + InstanceType: + default: Instance Type + CNamePrefix: + default: CNAME Prefix + +Conditions: + AutoDeploy: + !Equals [!Ref ApprovalRequired, "No"] + CreateGHSecret: + !Not [ !Equals [!Ref GitHubToken, ""]] + CreateS3: + !Equals [!Ref S3Bucket, ""] + CreateCodeBuildRole: + !Equals [!Ref CodeBuildRoleArn, ""] + CreateCodePipelineRole: + !Equals [!Ref CodePipelineRoleArn, ""] + CreateEBServiceRole: + !Equals [!Ref EBServiceRoleArn, ""] + CreateInstanceProfileAndRole: + !Equals [!Ref InstanceProfileArn, ""] + CNamePrefixSpecified: + !Not [!Equals [!Ref CNamePrefix, ""]] + +Rules: #Unfortunately, Assertions may not be enforced until a change set is executed. Still better than nothing. + GitHubTokenOrSecretExclusivelyProvided: + Assertions: #Ensure Exclusive Or between GH Token and GH Secret + - Assert: !Or [ !Not [!Equals [!Ref GitHubToken, ""]], !Not [!Equals [SecretResolveString, ""]]] + AssertDescription: Either GitHub Access Token or GitHub Secret CloudFormation Resolve String must be provided + - Assert: !Not [!And [ !Not [!Equals [!Ref GitHubToken, ""]], !Not [!Equals [!Ref SecretResolveString, ""]]]] + AssertDescription: Cannot provide both GitHub Access Token and GitHub Secret CloudFormation Resolve String + +Resources: + GHAuthTokenSecret: + Condition: CreateGHSecret + Type: AWS::SecretsManager::Secret + Properties: + Description: GH Access Token for AWS CodePipeline Dashboard Source access + SecretString: !Ref GitHubToken + + PipelineBucket: + Condition: CreateS3 + Type: AWS::S3::Bucket + Properties: + AccessControl: BucketOwnerFullControl + + CodeBuildServiceRole: + Condition: CreateCodeBuildRole + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Principal: + Service: + - "codebuild.amazonaws.com" + Action: + - "sts:AssumeRole" + Description: Role that CodeBuild assumes to build CodePipeline Dashboard application + Path: /service-role/ + Policies: + - PolicyName: !Sub 'CloduwatchLogGenerationPolicy' + PolicyDocument: + Version: 2012-10-17 + Statement: + - Sid: CreateBuildLogsInCloudwatch + Effect: Allow + Resource: + - !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*' + - !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*:*' + Action: + - 'logs:CreateLogGroup' + - 'logs:CreateLogStream' + - 'logs:PutLogEvents' + - PolicyName: !Sub 'S3BuildBucketAccessPolicy' + PolicyDocument: + Version: 2012-10-17 + Statement: + - Sid: GetAndPutBuildArtifactsFromAndToS3 + Action: + - 's3:PutObject' + - 's3:GetObject' + - 's3:PutObjectVersion' + - "s3:GetObjectVersion" + Effect: Allow + Resource: !If + - CreateS3 + - !Join + - '' + - - 'arn:aws:s3:::' + - !Ref PipelineBucket + - '/*' + - !Sub 'arn:aws:s3:::${S3Bucket}/*' + + ContinuousBuild: + Type: AWS::CodeBuild::Project + Properties: + ServiceRole: !If + - CreateCodeBuildRole + - !GetAtt CodeBuildServiceRole.Arn + - !Ref CodeBuildRoleArn + Artifacts: + Type: CODEPIPELINE + Environment: + Type: LINUX_CONTAINER + ComputeType: BUILD_GENERAL1_SMALL + Image: !Ref CodeBuildImage + EnvironmentVariables: + - Name: TARGET_BUCKET + Value: !If + - CreateS3 + - !Ref PipelineBucket + - !Ref S3Bucket + Source: + Type: CODEPIPELINE + BuildSpec: | + version: 0.2 + phases: + install: + runtime-versions: + java: corretto8 + build: + commands: + - mvn package + post_build: + commands: + - APP_JAR=$(mvn -q -Dexec.executable="echo" -Dexec.args='${project.build.finalName}' --non-recursive exec:exec) + artifacts: + files: + - 'target/$APP_JAR.jar' + discard-paths: yes + TimeoutInMinutes: 20 + + EBServiceRole: + Condition: CreateEBServiceRole + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - elasticbeanstalk.amazonaws.com + Action: + - sts:AssumeRole + Description: Role that ElasticBeanstalk Service assumes to manage resources + Path: /service-role/ + ManagedPolicyArns: [arn:aws:iam::aws:policy/AWSElasticBeanstalkManagedUpdatesCustomerRolePolicy] + + EBRole: + Condition: CreateInstanceProfileAndRole + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - ec2.amazonaws.com + Action: + - sts:AssumeRole + Description: ElasticBeanstalk EC2 role that allows ReadOnly Access to CodePipeline + ManagedPolicyArns: [arn:aws:iam::aws:policy/AWSCodePipeline_ReadOnlyAccess] + + EBInstanceProfile: + Condition: CreateInstanceProfileAndRole + DependsOn: EBRole + Type: AWS::IAM::InstanceProfile + Properties: + Roles: [!Ref EBRole] + + CPDashApp: + Type: AWS::ElasticBeanstalk::Application + Properties: + Description: CodePipeline Dashboard Elastic Beanstalk App + + CPDashEnv: + Type: AWS::ElasticBeanstalk::Environment + Properties: + ApplicationName: !Ref CPDashApp + Description: CodePipeline Dashboard Elastic Beanstalk Environment + SolutionStackName: !Ref EBSolutionStack + CNAMEPrefix: !If + - CNamePrefixSpecified + - !Ref CNamePrefix + - !Ref AWS::NoValue + OptionSettings: #Defaults to Webserver Tier + - Namespace: aws:ec2:instances + OptionName: InstanceTypes + Value: !Ref InstanceType + - Namespace: aws:elasticbeanstalk:environment + OptionName: EnvironmentType + Value: SingleInstance + - Namespace: aws:elasticbeanstalk:environment + OptionName: ServiceRole + Value: !If + - CreateEBServiceRole + - !GetAtt EBServiceRole.Arn + - !Ref EBServiceRoleArn + - Namespace: aws:elasticbeanstalk:managedactions:platformupdate + OptionName: UpdateLevel + Value: minor #Managed updates on by default, take all platform updates + - Namespace: aws:autoscaling:launchconfiguration + OptionName: DisableIMDSv1 + Value: true + - Namespace: aws:autoscaling:launchconfiguration + OptionName: IamInstanceProfile + Value: !If + - CreateInstanceProfileAndRole + - !Ref EBInstanceProfile + - !Ref InstanceProfileArn + - Namespace: aws:elasticbeanstalk:application:environment + OptionName: SERVER_PORT #Have SpringBoot run on nGinx default for SampleApp compatibility using 'SERVER_PORT' + Value: 5000 #instead of having nGinx listen on SpringBoot default of 8080 using 'PORT' + + CodePipelineServiceRole: + Condition: CreateCodePipelineRole + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Principal: + Service: + - "codepipeline.amazonaws.com" + Action: + - "sts:AssumeRole" + Description: Role that CodePipeline assumes to manage building and deploying the CodePipeline Dashboard + Path: /service-role/ + Policies: + - PolicyName: !Sub 'CodeBuildAndElasticBeanStalkPermissions' + PolicyDocument: + Version: 2012-10-17 + Statement: + - Sid: PutItemsCodePipelineOrElasticBeanstalkBuckets + Action: + - 's3:PutObject' + Resource: + - 'arn:aws:s3:::codepipeline*' + - 'arn:aws:s3:::elasticbeanstalk*' + Effect: Allow + - Sid: ElasticBeanstalkCreationAndDeployment + Action: + - 'elasticbeanstalk:*' + - 'ec2:*' + - 'elasticloadbalancing:*' + - 'autoscaling:*' + - 'cloudwatch:*' + - 's3:*' + - 'sns:*' + - 'cloudformation:*' + - 'rds:*' + - 'sqs:*' + - 'ecs:*' + - 'iam:PassRole' + Resource: '*' + Effect: Allow + - Sid: CodeBuildTriggeringAndStatus + Action: + - 'codebuild:BatchGetBuilds' + - 'codebuild:StartBuild' + Resource: '*' + Effect: Allow + + CiCdPipeline: + Type: AWS::CodePipeline::Pipeline + Properties: + ArtifactStore: + Type: S3 + Location: !If + - CreateS3 + - !Ref PipelineBucket + - !Ref S3Bucket + RestartExecutionOnUpdate: true + RoleArn: !If + - CreateCodePipelineRole + - !GetAtt CodePipelineServiceRole.Arn + - !Ref CodePipelineRoleArn + Stages: + - Name: Source + Actions: + - InputArtifacts: [] + Name: PullSourceFromGitHub + ActionTypeId: + Category: Source + Owner: ThirdParty + Version: '1' + Provider: GitHub #Even though CodeStar is preferred, this requires less interaction to set up + OutputArtifacts: + - Name: SourceCode + Configuration: + Owner: uplift-inc #Update here to use a different fork, didn't parameterize because not needed for our use + Repo: 'aws-codepipelines-dashboard' + Branch: !Ref Branch + OAuthToken: !If + - CreateGHSecret + - !Sub '{{resolve:secretsmanager:${GHAuthTokenSecret}}}' + - !Sub '{{resolve:secretsmanager:${SecretResolveString}}}' + PollForSourceChanges: true #Can't set up Webhook without Admin permissions, must poll + RunOrder: 1 + - Name: Build + Actions: + - InputArtifacts: + - Name: SourceCode + Name: MavenBuild + ActionTypeId: + Category: Build + Owner: AWS + Version: '1' + Provider: CodeBuild + OutputArtifacts: + - Name: AppJar + Configuration: + ProjectName: !Ref ContinuousBuild + RunOrder: 1 + - !If + - AutoDeploy + - !Ref AWS::NoValue + - Name: Human + Actions: + - InputArtifacts: [] + OutputArtifacts: [] + Name: HumanApproval + ActionTypeId: + Category: Approval + Owner: AWS + Version: 1 + Provider: Manual + - Name: Deploy + Actions: + - InputArtifacts: + - Name: AppJar + Name: DeployToElasticBeanstalk + ActionTypeId: + Category: Deploy + Owner: AWS + Version: '1' + Provider: ElasticBeanstalk + OutputArtifacts: [] + Configuration: + ApplicationName: !Ref CPDashApp + EnvironmentName: !Ref CPDashEnv + RunOrder: 1 + +Outputs: + CPDashURL: + Condition: CNamePrefixSpecified + Description: The Custom URL of the CodePipeline Dashboard for this region + Value: !Sub 'http://${CNamePrefix}.${AWS::Region}.elasticbeanstalk.com' + Export: + Name: !Sub '${AWS::Region}-CodePipelineDashboardURL' \ No newline at end of file From ee31c171a7766e66f20cd56a7e19e00264f23348 Mon Sep 17 00:00:00 2001 From: Mike Naik Date: Fri, 30 Sep 2022 16:02:25 -0700 Subject: [PATCH 78/86] New Solution Stack and CNAME note --- deployment-cft.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/deployment-cft.yml b/deployment-cft.yml index c5397d8..4958c98 100644 --- a/deployment-cft.yml +++ b/deployment-cft.yml @@ -52,7 +52,7 @@ Parameters: Description: The CodeBuild Image to use to build the application EBSolutionStack: Type: String - Default: '64bit Amazon Linux 2 v3.2.3 running Corretto 8' #EOL June 30, 2022 + Default: '64bit Amazon Linux 2 v3.3.2 running Corretto 8' Description: The ElasticBeanstalk Solution Stack to use, should be using the Corretto8 Platform InstanceType: Type: String @@ -61,7 +61,9 @@ Parameters: CNamePrefix: Type: String MaxLength: 63 - Description: Desired unique CNAME prefix (4-63 chars) for the EB Environment, otherwise automatically generated + Description: > + Desired unique CNAME prefix (4-63 chars) for the EB Environment, otherwise automatically generated. Note that + specifying this will prevent CloudFormation upgrades that require replacement (Solution Stack or Instance Type) ConstraintDescription: Must be less than 63 charcaters Metadata: From e7388162e841985180fc58a74dffd4e812dd21a2 Mon Sep 17 00:00:00 2001 From: Mike Naik Date: Fri, 30 Sep 2022 16:09:36 -0700 Subject: [PATCH 79/86] Fix note --- deployment-cft.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment-cft.yml b/deployment-cft.yml index 4958c98..baa6522 100644 --- a/deployment-cft.yml +++ b/deployment-cft.yml @@ -63,7 +63,7 @@ Parameters: MaxLength: 63 Description: > Desired unique CNAME prefix (4-63 chars) for the EB Environment, otherwise automatically generated. Note that - specifying this will prevent CloudFormation upgrades that require replacement (Solution Stack or Instance Type) + specifying this will prevent CloudFormation upgrades that require replacement ConstraintDescription: Must be less than 63 charcaters Metadata: From 7494a60bea628a75df63603b510900cb7f2e6680 Mon Sep 17 00:00:00 2001 From: ddumaisupgrade Date: Mon, 16 Oct 2023 11:15:30 -0400 Subject: [PATCH 80/86] TOP-20098 bump version to fix CVEs --- pom.xml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 476b276..c143789 100644 --- a/pom.xml +++ b/pom.xml @@ -13,14 +13,14 @@ org.springframework.boot spring-boot-starter-parent - 1.5.7.RELEASE + 3.1.2 UTF-8 UTF-8 - 1.8 - 1.16.22 + 17 + 1.18.30 1.12.31 3.2.1 2.5.2 @@ -78,7 +78,18 @@ spring-boot-starter-test test - + + junit + junit + 4.11 + test + + + org.mockito + mockito-core + 2.2.3 + test + From 8fd062b731292581ce91001b8b94887ef896d9a3 Mon Sep 17 00:00:00 2001 From: ddumaisupgrade Date: Mon, 16 Oct 2023 11:33:27 -0400 Subject: [PATCH 81/86] fix --- pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index c143789..0f79126 100644 --- a/pom.xml +++ b/pom.xml @@ -6,14 +6,14 @@ de.codecentric aws-codepipelines-dashboard - 1.3 + 1.4 ${project.artifactId} Shows the status of your AWS pipelines in a dashboard org.springframework.boot spring-boot-starter-parent - 3.1.2 + 3.1.4 @@ -21,10 +21,10 @@ UTF-8 17 1.18.30 - 1.12.31 - 3.2.1 - 2.5.2 - 2.19.1 + 1.12.566 + 3.7.1 + 2.7.8 + 2.29.4 From c9fb26655f693e311f857e7cc005a7e34a9a456f Mon Sep 17 00:00:00 2001 From: ddumaisupgrade Date: Mon, 16 Oct 2023 14:01:23 -0400 Subject: [PATCH 82/86] fix --- deployment-cft.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deployment-cft.yml b/deployment-cft.yml index baa6522..ac25cae 100644 --- a/deployment-cft.yml +++ b/deployment-cft.yml @@ -234,7 +234,7 @@ Resources: phases: install: runtime-versions: - java: corretto8 + java: corretto17 build: commands: - mvn package @@ -467,4 +467,4 @@ Outputs: Description: The Custom URL of the CodePipeline Dashboard for this region Value: !Sub 'http://${CNamePrefix}.${AWS::Region}.elasticbeanstalk.com' Export: - Name: !Sub '${AWS::Region}-CodePipelineDashboardURL' \ No newline at end of file + Name: !Sub '${AWS::Region}-CodePipelineDashboardURL' From 040c2c9f852091e1c36fe58cafe990423841be02 Mon Sep 17 00:00:00 2001 From: ddumaisupgrade Date: Mon, 16 Oct 2023 14:07:41 -0400 Subject: [PATCH 83/86] fix --- deployment-cft.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deployment-cft.yml b/deployment-cft.yml index ac25cae..6b27171 100644 --- a/deployment-cft.yml +++ b/deployment-cft.yml @@ -48,11 +48,11 @@ Parameters: Description: The Arn of the pre-existing Instance Profile for the Elastic Beanstalk EC2 instances to use CodeBuildImage: Type: String - Default: 'aws/codebuild/amazonlinux2-x86_64-standard:3.0' + Default: 'aws/codebuild/amazonlinux2-x86_64-standard:5.0' Description: The CodeBuild Image to use to build the application EBSolutionStack: Type: String - Default: '64bit Amazon Linux 2 v3.3.2 running Corretto 8' + Default: '64bit Amazon Linux 2023 v4.0.1 running Corretto 17' Description: The ElasticBeanstalk Solution Stack to use, should be using the Corretto8 Platform InstanceType: Type: String From f016ca0630d69980f29655f846f75ed331d66d62 Mon Sep 17 00:00:00 2001 From: ddumaisupgrade Date: Wed, 18 Oct 2023 15:21:46 -0400 Subject: [PATCH 84/86] TOP-20098-fix --- pom.xml | 2 +- src/main/resources/templates/index.html | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 0f79126..fa968b6 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ 1.18.30 1.12.566 3.7.1 - 2.7.8 + 2.6.14 2.29.4 diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index bc9974e..eb0bd07 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -205,11 +205,11 @@ - + - + - + @@ -223,4 +223,4 @@ - \ No newline at end of file + From 4c294ec7115f331da4e85407a4dca8ff1ea64393 Mon Sep 17 00:00:00 2001 From: ddumaisupgrade Date: Thu, 19 Oct 2023 09:53:45 -0400 Subject: [PATCH 85/86] fix start date and duration --- src/main/resources/static/js/pipelineService.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/resources/static/js/pipelineService.js b/src/main/resources/static/js/pipelineService.js index c15b187..cce58c9 100644 --- a/src/main/resources/static/js/pipelineService.js +++ b/src/main/resources/static/js/pipelineService.js @@ -41,7 +41,6 @@ let PipelineService = function (jquery, as) { lastStatusChange: 0, states: [] }; - for (let i = 0; i < response.stageStates.length; i++) { const stageState = response.stageStates[i]; let stages = []; @@ -50,8 +49,7 @@ let PipelineService = function (jquery, as) { stages.push(parsePipelineActionState(actionState)); } const statusChanges = stages.map((stage) => stage.lastStatusChange || 0); - const lastStatusChange = Math.max.apply(Math, statusChanges); - + const lastStatusChange = Math.max.apply(Math, [Date.parse(statusChanges)]); pipelineDetails.states.push({ name: stageState.stageName, lastStatusChange: lastStatusChange, From 07a83fc6a07d391a6d936f7f3b084c51f047e570 Mon Sep 17 00:00:00 2001 From: ddumaisupgrade Date: Thu, 19 Oct 2023 09:55:15 -0400 Subject: [PATCH 86/86] Fix --- src/main/resources/static/js/pipelineService.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/resources/static/js/pipelineService.js b/src/main/resources/static/js/pipelineService.js index cce58c9..14c97ee 100644 --- a/src/main/resources/static/js/pipelineService.js +++ b/src/main/resources/static/js/pipelineService.js @@ -41,6 +41,7 @@ let PipelineService = function (jquery, as) { lastStatusChange: 0, states: [] }; + for (let i = 0; i < response.stageStates.length; i++) { const stageState = response.stageStates[i]; let stages = []; @@ -50,6 +51,7 @@ let PipelineService = function (jquery, as) { } const statusChanges = stages.map((stage) => stage.lastStatusChange || 0); const lastStatusChange = Math.max.apply(Math, [Date.parse(statusChanges)]); + pipelineDetails.states.push({ name: stageState.stageName, lastStatusChange: lastStatusChange,