From 644c77a82273ee43af79ab17b522b90d36cd183c Mon Sep 17 00:00:00 2001 From: David Byron Date: Fri, 29 Mar 2024 08:09:56 -0700 Subject: [PATCH 1/8] chore(build): give local gradle builds more memory to match what github actions uses --- gradle.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gradle.properties b/gradle.properties index 3e5057491..7c211b2c2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,3 +19,5 @@ targetJava11=true # #fiatComposite=true #korkComposite=true + +org.gradle.jvmargs=-Xmx2g -Xms2g From 10661871cf36733f59935cf9621a5f8bca437d13 Mon Sep 17 00:00:00 2001 From: David Byron Date: Thu, 28 Mar 2024 20:11:01 -0700 Subject: [PATCH 2/8] feat(docker): add HEALTHCHECK to facilitate testing container startup --- Dockerfile.java11.slim | 1 + Dockerfile.java11.ubuntu | 3 ++- Dockerfile.slim | 3 ++- Dockerfile.ubuntu | 3 ++- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Dockerfile.java11.slim b/Dockerfile.java11.slim index d5c7e4530..58a92034a 100644 --- a/Dockerfile.java11.slim +++ b/Dockerfile.java11.slim @@ -5,4 +5,5 @@ RUN adduser --system --uid 10111 --group spinnaker COPY echo-web/build/install/echo /opt/echo RUN mkdir -p /opt/echo/plugins && chown -R spinnaker:nogroup /opt/echo/plugins USER spinnaker +HEALTHCHECK CMD curl http://localhost:8089/health | grep UP || exit 1 CMD ["/opt/echo/bin/echo"] diff --git a/Dockerfile.java11.ubuntu b/Dockerfile.java11.ubuntu index a75c0b55a..1235cee0b 100644 --- a/Dockerfile.java11.ubuntu +++ b/Dockerfile.java11.ubuntu @@ -1,8 +1,9 @@ FROM ubuntu:bionic LABEL maintainer="sig-platform@spinnaker.io" -RUN apt-get update && apt-get -y install openjdk-11-jre-headless wget +RUN apt-get update && apt-get -y install curl openjdk-11-jre-headless wget RUN adduser --system --uid 10111 --group spinnaker COPY echo-web/build/install/echo /opt/echo RUN mkdir -p /opt/echo/plugins && chown -R spinnaker:nogroup /opt/echo/plugins USER spinnaker +HEALTHCHECK CMD curl http://localhost:8089/health | grep UP || exit 1 CMD ["/opt/echo/bin/echo"] diff --git a/Dockerfile.slim b/Dockerfile.slim index ba40f2472..247b752a6 100644 --- a/Dockerfile.slim +++ b/Dockerfile.slim @@ -1,9 +1,10 @@ FROM alpine:3.16 LABEL maintainer="sig-platform@spinnaker.io" -RUN apk --no-cache add --update bash openjdk17-jre +RUN apk --no-cache add --update bash curl openjdk17-jre RUN addgroup -S -g 10111 spinnaker RUN adduser -S -G spinnaker -u 10111 spinnaker COPY echo-web/build/install/echo /opt/echo RUN mkdir -p /opt/echo/plugins && chown -R spinnaker:nogroup /opt/echo/plugins USER spinnaker +HEALTHCHECK CMD curl http://localhost:8089/health | grep UP || exit 1 CMD ["/opt/echo/bin/echo"] diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index 76920158a..ee562a9ed 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -1,8 +1,9 @@ FROM ubuntu:bionic LABEL maintainer="sig-platform@spinnaker.io" -RUN apt-get update && apt-get -y install openjdk-17-jre-headless wget +RUN apt-get update && apt-get -y install curl openjdk-17-jre-headless wget RUN adduser --system --uid 10111 --group spinnaker COPY echo-web/build/install/echo /opt/echo RUN mkdir -p /opt/echo/plugins && chown -R spinnaker:nogroup /opt/echo/plugins USER spinnaker +HEALTHCHECK CMD curl http://localhost:8089/health | grep UP || exit 1 CMD ["/opt/echo/bin/echo"] From 75de76f4f6258a63a17b48c2e56fc054a479eb3d Mon Sep 17 00:00:00 2001 From: David Byron Date: Thu, 28 Mar 2024 18:43:52 -0700 Subject: [PATCH 3/8] feat(build): add echo-integration module to exercise the just-built docker image @W-15161670 --- echo-integration/echo-integration.gradle | 25 +++ .../echo/StandaloneContainerTest.java | 168 ++++++++++++++++++ .../src/test/resources/logback.xml | 36 ++++ settings.gradle | 1 + 4 files changed, 230 insertions(+) create mode 100644 echo-integration/echo-integration.gradle create mode 100644 echo-integration/src/test/java/com/netflix/spinnaker/echo/StandaloneContainerTest.java create mode 100644 echo-integration/src/test/resources/logback.xml diff --git a/echo-integration/echo-integration.gradle b/echo-integration/echo-integration.gradle new file mode 100644 index 000000000..919149e21 --- /dev/null +++ b/echo-integration/echo-integration.gradle @@ -0,0 +1,25 @@ +dependencies { + testImplementation "com.fasterxml.jackson.core:jackson-databind" + testImplementation "com.github.tomakehurst:wiremock-jre8-standalone" + testImplementation "org.assertj:assertj-core" + testImplementation "org.junit.jupiter:junit-jupiter-api" + testImplementation "org.slf4j:slf4j-api" + testImplementation "org.testcontainers:testcontainers" + testImplementation "org.testcontainers:junit-jupiter" + testRuntimeOnly "ch.qos.logback:logback-classic" +} + +test.configure { + def fullDockerImageName = System.getenv('FULL_DOCKER_IMAGE_NAME') + onlyIf("there is a docker image to test") { + fullDockerImageName != null && fullDockerImageName.trim() != '' + } +} + +test { + // So stdout and stderr from the just-built container are available in CI + testLogging.showStandardStreams = true + + // Run the tests when the docker image changes + inputs.property 'fullDockerImageName', System.getenv('FULL_DOCKER_IMAGE_NAME') +} diff --git a/echo-integration/src/test/java/com/netflix/spinnaker/echo/StandaloneContainerTest.java b/echo-integration/src/test/java/com/netflix/spinnaker/echo/StandaloneContainerTest.java new file mode 100644 index 000000000..30f7bb499 --- /dev/null +++ b/echo-integration/src/test/java/com/netflix/spinnaker/echo/StandaloneContainerTest.java @@ -0,0 +1,168 @@ +/* + * Copyright 2024 Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spinnaker.echo; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Map; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +@Testcontainers +class StandaloneContainerTest { + + private static final String REDIS_NETWORK_ALIAS = "redisHost"; + + private static final int REDIS_PORT = 6379; + + private static final Logger logger = LoggerFactory.getLogger(StandaloneContainerTest.class); + + private static final Network network = Network.newNetwork(); + + // echo caches pipeline configurations from front50, and echo's health depends + // on it succeeding. + @RegisterExtension + static final WireMockExtension wmFront50 = + WireMockExtension.newInstance().options(wireMockConfig().dynamicPort()).build(); + + static int front50Port; + + // redis isn't required for echo to start, but redis starts quickly enough + // that we may as well wire it up to facilitate future testing. + private static final GenericContainer redis = + new GenericContainer(DockerImageName.parse("library/redis:5-alpine")) + .withNetwork(network) + .withNetworkAliases(REDIS_NETWORK_ALIAS) + .withExposedPorts(REDIS_PORT); + + private static GenericContainer echoContainer; + + @BeforeAll + static void setupOnce() throws Exception { + front50Port = wmFront50.getRuntimeInfo().getHttpPort(); + logger.info("wiremock front50 http port: {} ", front50Port); + + // set up front50 stub for /pipelines...return an empty json list + wmFront50.stubFor( + WireMock.get(urlPathEqualTo("/pipelines")) + .willReturn(aResponse().withStatus(200).withBody("[]"))); + + String fullDockerImageName = System.getenv("FULL_DOCKER_IMAGE_NAME"); + + // Skip the tests if there's no docker image. This allows gradlew build to work. + assumeTrue(fullDockerImageName != null); + + // expose front50 to echo + org.testcontainers.Testcontainers.exposeHostPorts(front50Port); + + redis.start(); + + DockerImageName dockerImageName = DockerImageName.parse(fullDockerImageName); + + // Include the file system bind for /tmp since echo uses spring cloud config + // which writes to it. + echoContainer = + new GenericContainer(dockerImageName) + .withNetwork(network) + .withExposedPorts(8089) + .dependsOn(redis) + .waitingFor(Wait.forHealthcheck().withStartupTimeout(Duration.ofSeconds(90))) + .withEnv("SPRING_APPLICATION_JSON", getSpringApplicationJson()); + + Slf4jLogConsumer logConsumer = new Slf4jLogConsumer(logger); + echoContainer.start(); + echoContainer.followOutput(logConsumer); + } + + private static String getSpringApplicationJson() throws JsonProcessingException { + String redisUrl = "redis://" + REDIS_NETWORK_ALIAS + ":" + REDIS_PORT; + logger.info("redisUrl: '{}'", redisUrl); + Map properties = + Map.of( + "redis.connection", + redisUrl, + "redis.enabled", + "true", + "services.front50.baseUrl", + "http://" + GenericContainer.INTERNAL_HOST_HOSTNAME + ":" + front50Port); + ObjectMapper mapper = new ObjectMapper(); + return mapper.writeValueAsString(properties); + } + + @AfterAll + static void cleanupOnce() { + if (echoContainer != null) { + echoContainer.stop(); + } + + if (redis != null) { + redis.stop(); + } + } + + @BeforeEach + void init(TestInfo testInfo) { + System.out.println("--------------- Test " + testInfo.getDisplayName()); + } + + @Test + void testHealthCheck() throws Exception { + // hit an arbitrary endpoint + HttpRequest request = + HttpRequest.newBuilder() + .uri( + new URI( + "http://" + + echoContainer.getHost() + + ":" + + echoContainer.getFirstMappedPort() + + "/health")) + .GET() + .build(); + + HttpClient client = HttpClient.newHttpClient(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + assertThat(response).isNotNull(); + logger.info("response: {}, {}", response.statusCode(), response.body()); + assertThat(response.statusCode()).isEqualTo(200); + } +} diff --git a/echo-integration/src/test/resources/logback.xml b/echo-integration/src/test/resources/logback.xml new file mode 100644 index 000000000..6145d3878 --- /dev/null +++ b/echo-integration/src/test/resources/logback.xml @@ -0,0 +1,36 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + diff --git a/settings.gradle b/settings.gradle index 7ca294552..ee837897b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -27,6 +27,7 @@ include 'echo-api', 'echo-artifacts', 'echo-bom', 'echo-core', + 'echo-integration', 'echo-model', 'echo-web', 'echo-notifications', From a483a9348e7714587667f8088a47bdeb8fb712a0 Mon Sep 17 00:00:00 2001 From: David Byron Date: Thu, 28 Mar 2024 20:12:44 -0700 Subject: [PATCH 4/8] fix(web): remove circular dependency in configuration of services.fiat.enabled to fix: Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled. 2024-03-29 03:19:26.437 ERROR 1 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPLICATION FAILED TO START *************************** Description: Failed to bind properties under 'services.fiat.enabled' to boolean: Property: services.fiat.enabled Value: "${services.fiat.enabled:false}" Origin: "services.fiat.enabled" from property source "Config resource 'class path resource [echo.yml]' via location 'optional:classpath:/' (document #0)" Reason: java.lang.IllegalArgumentException: Circular placeholder reference 'services.fiat.enabled:false' in property definitions Action: Update your application's configuration --- echo-web/config/echo.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/echo-web/config/echo.yml b/echo-web/config/echo.yml index 2c7efb89c..bc53b1a37 100644 --- a/echo-web/config/echo.yml +++ b/echo-web/config/echo.yml @@ -12,7 +12,7 @@ front50: services: fiat: - enabled: ${services.fiat.enabled:false} + enabled: false baseUrl: ${services.fiat.baseUrl:http://localhost:8089} igor: From 7a00325a67f82b0babe4d33e02aa97793d08a452 Mon Sep 17 00:00:00 2001 From: David Byron Date: Fri, 29 Mar 2024 08:19:07 -0700 Subject: [PATCH 5/8] fix(web): remove circular dependency in configuration of services.fiat.baseUrl to fix: Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled. 2024-03-29 15:14:51.086 ERROR 1 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPLICATION FAILED TO START *************************** Description: Failed to bind properties under 'services.fiat.base-url' to java.lang.String: Property: services.fiat.base-url Value: "${services.fiat.baseUrl:http://localhost:8089}" Origin: "services.fiat.baseUrl" from property source "Config resource 'class path resource [echo.yml]' via location 'optional:classpath:/' (document #0)" Reason: java.lang.IllegalArgumentException: Circular placeholder reference 'services.fiat.baseUrl:http://localhost:8089' in property definitions Action: Update your application's configuration Use port 7003 since that's the default port for fiat from https://spinnaker.io/docs/reference/architecture/microservices-overview/#port-mappings. --- echo-web/config/echo.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/echo-web/config/echo.yml b/echo-web/config/echo.yml index bc53b1a37..5781f8256 100644 --- a/echo-web/config/echo.yml +++ b/echo-web/config/echo.yml @@ -13,7 +13,7 @@ front50: services: fiat: enabled: false - baseUrl: ${services.fiat.baseUrl:http://localhost:8089} + baseUrl: http://localhost:7003 igor: enabled: ${services.igor.enabled:false} From 056ce8517285fd24460da71fa87cb578a3caa7c1 Mon Sep 17 00:00:00 2001 From: David Byron Date: Thu, 28 Mar 2024 20:22:56 -0700 Subject: [PATCH 6/8] fix(web): replace deprecated spring.profiles in configuration with spring.config.activate.on-profile to remove these warnings: 2024-03-29 03:19:11.785 WARN 1 --- [ main] o.s.b.c.config.ConfigDataEnvironment : Property 'spring.profiles' imported from location 'class path resource [echo.yml]' is invalid and should be replaced with 'spring.config.activate.on-profile' [origin: class path resource [echo.yml] - 74:13] 2024-03-29 03:19:11.785 WARN 1 --- [ main] o.s.b.c.config.ConfigDataEnvironment : Property 'spring.profiles' imported from location 'class path resource [echo.yml]' is invalid and should be replaced with 'spring.config.activate.on-profile' [origin: class path resource [echo.yml] - 65:13] See https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-Config-Data-Migration-Guide#profile-specific-documents. --- echo-web/config/echo.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/echo-web/config/echo.yml b/echo-web/config/echo.yml index 5781f8256..bc2900512 100644 --- a/echo-web/config/echo.yml +++ b/echo-web/config/echo.yml @@ -62,7 +62,9 @@ resilience4j.circuitbreaker: # This profile is used in sharded deployments for an Echo that handles only # scheduled tasks. spring: - profiles: scheduler + config: + activate: + on-profile: scheduler scheduler: enabled: true @@ -71,7 +73,9 @@ scheduler: # This profile is used in sharded deployments for an Echo that handles all # operations other than scheduled tasks. spring: - profiles: worker + config: + activate: + on-profile: worker scheduler: enabled: false From 559c25e40444d030b13e8ecfd8c588e8aa1dda72 Mon Sep 17 00:00:00 2001 From: David Byron Date: Fri, 29 Mar 2024 08:58:23 -0700 Subject: [PATCH 7/8] feat(gha): run integration test in pr builds multi-arch with --load doesn't work, so add a separate step using the local platform to make an image available for testing. see docker/buildx#59 --- .github/workflows/pr.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 00efe5013..aa20afd75 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -73,3 +73,16 @@ jobs: tags: | "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:latest-java11-ubuntu" "${{ env.CONTAINER_REGISTRY }}/${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-java11-ubuntu" + - name: Build local slim container image for testing + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile.slim + load: true + platforms: local + tags: | + "${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}" + - name: Test local slim container image + env: + FULL_DOCKER_IMAGE_NAME: "${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}" + run: ./gradlew ${{ steps.build_variables.outputs.REPO }}-integration:test From 71012f6878fe4bfd7c3567100566e441ecd96792 Mon Sep 17 00:00:00 2001 From: David Byron Date: Fri, 29 Mar 2024 08:59:49 -0700 Subject: [PATCH 8/8] feat(gha): run integration test in branch builds --- .github/workflows/build.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 34cf34ad4..684ff400f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,6 +39,19 @@ jobs: env: ORG_GRADLE_PROJECT_version: ${{ steps.build_variables.outputs.VERSION }} run: ./gradlew build --stacktrace ${{ steps.build_variables.outputs.REPO }}-web:installDist + - name: Build local slim container image for testing + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile.slim + load: true + platforms: local + tags: | + "${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-unvalidated" + - name: Test local slim container image + env: + FULL_DOCKER_IMAGE_NAME: "${{ steps.build_variables.outputs.REPO }}:${{ steps.build_variables.outputs.VERSION }}-unvalidated" + run: ./gradlew ${{ steps.build_variables.outputs.REPO }}-integration:test - name: Login to GAR # Only run this on repositories in the 'spinnaker' org, not on forks. if: startsWith(github.repository, 'spinnaker/')