diff --git a/.github/workflows/continuous.yaml b/.github/workflows/continuous.yaml index 22cf25ad..b124ea42 100644 --- a/.github/workflows/continuous.yaml +++ b/.github/workflows/continuous.yaml @@ -65,7 +65,7 @@ jobs: - name: Install Java uses: actions/setup-java@v3 with: - java-version: '17' + java-version: '21' distribution: 'temurin' - name: Validate Gradle wrapper diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 003c7951..203ca6eb 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -19,7 +19,7 @@ jobs: - name: Install Java uses: actions/setup-java@v3 with: - java-version: '17' + java-version: '21' distribution: 'temurin' - name: Validate Gradle wrapper diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index be754ef1..00000000 --- a/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -FROM ghcr.io/graalvm/native-image:ol9-java17-22.3.3 AS builder - -# Install tar and gzip to extract the Maven binaries -RUN microdnf install --nodocs -y \ - findutils \ - && microdnf clean all \ - && rm -rf /var/cache/yum - -WORKDIR /build - -# Copy the source code into the image for building -COPY . /build - -# Build -RUN ./gradlew nativeCompile - -# The deployment Image -FROM docker.io/oraclelinux:9-slim - -EXPOSE 8080 -EXPOSE 8081 - -WORKDIR /workspace - -# Copy the native executable into the container -COPY --from=builder /build/build/native/nativeCompile . -ENTRYPOINT ["/workspace/morp"] \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 421f53f2..63f2689e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ plugins { jacoco `jvm-test-suite` id("io.freefair.lombok") version "8.4" - id("org.springframework.boot") version "3.1.5" + id("org.springframework.boot") version "3.2.0" id("io.spring.dependency-management") version "1.1.4" id("org.graalvm.buildtools.native") version "0.9.28" id("com.github.rising3.semver") version "0.8.2" @@ -29,7 +29,7 @@ dependencies { implementation("net.logstash.logback:logstash-logback-encoder:7.4") implementation("ch.qos.logback:logback-classic") - implementation(platform("org.springframework.cloud:spring-cloud-dependencies:2022.0.4")) + implementation(platform("org.springframework.cloud:spring-cloud-dependencies:2023.0.0")) implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springframework.boot:spring-boot-starter-oauth2-client") @@ -63,7 +63,7 @@ dependencies { } java { - sourceCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 } sonarqube { @@ -74,7 +74,11 @@ sonarqube { } } -extra["testcontainersVersion"] = "1.18.2" +tasks.processTestAot { + enabled = false +} + +extra["testcontainersVersion"] = "1.19.3" // setup separate test suites for unit and integration tests testing { @@ -100,7 +104,7 @@ testing { implementation("org.testcontainers:testcontainers") // testcontainers containers implementation("org.testcontainers:selenium") - implementation("com.github.dasniko:testcontainers-keycloak:3.1.0") + implementation("com.github.dasniko:testcontainers-keycloak:3.2.0") implementation("org.testcontainers:mockserver") // selenium itself implementation("org.seleniumhq.selenium:selenium-api") @@ -115,7 +119,7 @@ testing { targets { all { testTask.configure { - mustRunAfter(tasks.named("dockerBuild")) + mustRunAfter(tasks.bootBuildImage) testLogging.exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL testLogging.showStandardStreams = true } @@ -125,17 +129,6 @@ testing { } } -tasks.create("dockerBuild") { - mustRunAfter(tasks.test) - executable("docker") - args(listOf("build", "-t", "${registry}/${project.name}:${project.version}", "-t", "${registry}/${project.name}:latest", ".")) -} - -tasks.create("dockerBuildPush") { - executable("docker") - args(listOf("buildx", "build", "--platform", "linux/amd64,linux/arm64", "-t", "${registry}/${project.name}:${project.version}", "--push", ".")) -} - tasks.withType { mustRunAfter(tasks.test) imageName.value("${registry}/${project.name}:${project.version}") @@ -157,7 +150,7 @@ tasks.withType { tasks.check { dependsOn(tasks.test) - dependsOn(tasks.named("dockerBuild")) + dependsOn(tasks.bootBuildImage) dependsOn(testing.suites.named("integrationTest")) dependsOn(tasks.jacocoTestReport) } diff --git a/compose/application.demo.yaml b/compose/application.demo.yaml index 88be276c..0e94ee91 100644 --- a/compose/application.demo.yaml +++ b/compose/application.demo.yaml @@ -6,7 +6,8 @@ spring: uri: https://httpbin.org order: 2 predicates: - - Host={tenant}.localtest.me:8080 + # Ports in these kinds of predicates are no longer supported: https://github.com/spring-cloud/spring-cloud-gateway/pull/3037 + - Host={tenant}.localtest.me morp: default-oauth2-client-registration: keycloak diff --git a/compose/application.yaml b/compose/application.yaml index 86af1082..f9970b9a 100644 --- a/compose/application.yaml +++ b/compose/application.yaml @@ -9,12 +9,13 @@ spring: uri: https://httpbin.org order: 2 predicates: - - Host={tenant}.localtest.me:8080 + # Ports in these kinds of predicates are no longer supported: https://github.com/spring-cloud/spring-cloud-gateway/pull/3037 + - Host={tenant}.localtest.me - id: host-mapping-with-stage uri: https://httpbin.org order: 1 predicates: - - Host={tenant}-{stage}.localtest.me:8080 + - Host={tenant}-{stage}.localtest.me - id: path-mapping uri: https://httpbin.org predicates: @@ -26,8 +27,8 @@ spring: - name: TenantFromHost args: patterns: - - static.localtest.me:8080 - - another-static.localtest.me:8080 + - static.localtest.me + - another-static.localtest.me tenant: foo - id: header-mapping uri: https://httpbin.org diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c..d64cd491 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ac72c34e..1af9e093 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 0adc8e1a..1aa94a42 100755 --- a/gradlew +++ b/gradlew @@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -202,11 +202,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/src/integrationTest/java/io/jaconi/morp/ProxyIT.java b/src/integrationTest/java/io/jaconi/morp/ProxyIT.java index b15d9836..325a4c34 100644 --- a/src/integrationTest/java/io/jaconi/morp/ProxyIT.java +++ b/src/integrationTest/java/io/jaconi/morp/ProxyIT.java @@ -44,7 +44,7 @@ void testKeycloak() { var step1 = containerSetup.getWebTestClient().get() .uri("/upstream/tenant1/test") .accept(MediaType.TEXT_HTML) - .header("host", "morp:8081") + .header("host", "morp:8080") .exchange() .expectStatus().is3xxRedirection() .expectHeader().location("/oauth2/authorization/tenant1") @@ -114,7 +114,7 @@ void testKeycloak() { var step5 = containerSetup.getWebTestClient().get() .uri(step4.getResponseHeaders().getLocation()) .cookie(SESSION_COOKIE, session) - .header("host", "morp:8081") + .header("host", "morp:8080") .exchange() .expectStatus().is3xxRedirection() .expectHeader().location("/upstream/tenant1/test") @@ -129,7 +129,7 @@ void testKeycloak() { var step6 = containerSetup.getWebTestClient().get() .uri(step5.getResponseHeaders().getLocation().getPath()) .accept(MediaType.TEXT_HTML) - .header("host", "morp:8081") + .header("host", "morp:8080") .cookie(SESSION_COOKIE, session) .header("x-tenant-id", "tenant1") .exchange() diff --git a/src/integrationTest/java/io/jaconi/morp/SeleniumIT.java b/src/integrationTest/java/io/jaconi/morp/SeleniumIT.java index f74090b2..a724bb40 100644 --- a/src/integrationTest/java/io/jaconi/morp/SeleniumIT.java +++ b/src/integrationTest/java/io/jaconi/morp/SeleniumIT.java @@ -47,9 +47,8 @@ static class Config { // we start Chrome for each test to ensure a clean state (i.e. cookies etc) - // screen recording not working on ARM Mac due to missing ARM image of vnc-recorder @Container - public final BrowserWebDriverContainer chrome = new BrowserWebDriverContainer<>(ArmUtil.select("seleniarm/standalone-chromium:107.0", "selenium/standalone-chrome:107.0")) + public final BrowserWebDriverContainer chrome = new BrowserWebDriverContainer<>(ArmUtil.select("seleniarm/standalone-chromium:latest", "selenium/standalone-chrome:latest")) .withNetwork(containerSetup.getNetwork()) .withNetworkAliases("chrome") .withCapabilities(new ChromeOptions()) @@ -83,8 +82,8 @@ void tearDown() { @ParameterizedTest @CsvSource({ - "tenant1, morp:8081, /upstream/tenant1, /test", - "tenant1, tenant1-morp:8081, /upstream, /test" + "tenant1, morp:8080, /upstream/tenant1, /test", + "tenant1, tenant1-morp:8080, /upstream, /test" }) void testWithKeycloak(String tenant, String host, String prefix, String path) throws MalformedURLException { @@ -105,7 +104,7 @@ void testWithKeycloak(String tenant, String host, String prefix, String path) th // assert that we ended up in the right place URL url = new URL(driver.getCurrentUrl()); assertThat(url.getHost()).isEqualTo(StringUtils.substringBefore(host, ":")); - assertThat(url.getPort()).isEqualTo(8081); + assertThat(url.getPort()).isEqualTo(8080); assertThat(url.getPath()).isEqualTo(prefix + path); assertThat(driver.findElement(By.id("test")).getText()).isEqualTo("Hello from mockserver"); @@ -128,8 +127,8 @@ void testWithKeycloak(String tenant, String host, String prefix, String path) th @ParameterizedTest @CsvSource({ - "tenant2, morp:8081, /upstream/tenant2, /test", - "tenant2, tenant2-morp:8081, /upstream, /test" + "tenant2, morp:8080, /upstream/tenant2, /test", + "tenant2, tenant2-morp:8080, /upstream, /test" }) void testWithOkta(String tenant, String host, String prefix, String path) { diff --git a/src/integrationTest/java/io/jaconi/morp/TestContainerSetup.java b/src/integrationTest/java/io/jaconi/morp/TestContainerSetup.java index b1a1015f..a31dcae0 100644 --- a/src/integrationTest/java/io/jaconi/morp/TestContainerSetup.java +++ b/src/integrationTest/java/io/jaconi/morp/TestContainerSetup.java @@ -12,6 +12,7 @@ import org.testcontainers.containers.Network; import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; +import org.testcontainers.lifecycle.Startables; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.DockerLoggerFactory; import reactor.netty.http.client.HttpClient; @@ -54,29 +55,28 @@ public TestContainerSetup() { // setup mockserver (as protected upstream) var tag = "mockserver-%s".formatted(MockServerClient.class.getPackage().getImplementationVersion()); - this.mockserver = new MockServerContainer(DockerImageName.parse("mockserver/mockserver").withTag(tag)) + DockerImageName mockServerImage = DockerImageName.parse("mockserver/mockserver").withTag(tag); + this.mockserver = new MockServerContainer(mockServerImage) .withNetwork(network) - .withNetworkAliases("upstream"); + .withNetworkAliases("upstream") + .withLogConsumer(new Slf4jLogConsumer(DockerLoggerFactory.getLogger(mockServerImage.asCanonicalNameString()))); // setup morp as auth proxy for upstream - this.morp = new GenericContainer<>(DockerImageName.parse("ghcr.io/jaconi-io/morp:latest")) + DockerImageName morpImage = DockerImageName.parse("ghcr.io/jaconi-io/morp:latest"); + this.morp = new GenericContainer<>(morpImage) .withNetwork(network) .withNetworkAliases("morp", "tenant1-morp", "tenant2-morp") - .withExposedPorts(8081, 8082) + .withExposedPorts(8080, 8081) .withEnv("SPRING_PROFILES_ACTIVE", "test") .withFileSystemBind( "./src/integrationTest/resources/morp/application.yaml", "/workspace/config/application.yaml", BindMode.READ_ONLY) .waitingFor(new HttpWaitStrategy() - .forPort(8082) + .forPort(8081) .forPath("/actuator/health/readiness") - .withStartupTimeout(Duration.ofMinutes(5))); - - // start the containers - keycloak.start(); - mockserver.withLogConsumer(new Slf4jLogConsumer(DockerLoggerFactory.getLogger(mockserver.getDockerImageName()))) - .start(); + .withStartupTimeout(Duration.ofMinutes(5))) + .withLogConsumer(new Slf4jLogConsumer(DockerLoggerFactory.getLogger(morpImage.asCanonicalNameString()))); // for local development convenience, bind mount the git-ignored 'secret.properties' (if it exists) if (Files.exists(Path.of("./secret.properties"))) { @@ -92,8 +92,8 @@ public TestContainerSetup() { .filter(e -> e.getKey().startsWith("MORP_")) .forEach(e -> morp.withEnv(e.getKey(), e.getValue())); - morp.withLogConsumer(new Slf4jLogConsumer(DockerLoggerFactory.getLogger(morp.getDockerImageName()))) - .start(); + // start the containers + Startables.deepStart(keycloak, mockserver, morp).join(); // create client to control mockserver (running as container) mockServerClient = new MockServerClient(mockserver.getHost(), mockserver.getServerPort()); @@ -103,11 +103,11 @@ public TestContainerSetup() { .wiretap(true) // hex dump wiretap .compress(true); webTestClient = WebTestClient.bindToServer(new ReactorClientHttpConnector(httpClient)) - .baseUrl("http://localhost:" + morp.getMappedPort(8081)) + .baseUrl("http://localhost:" + morp.getMappedPort(8080)) .build(); managementTestClient = WebTestClient.bindToServer(new ReactorClientHttpConnector(httpClient)) - .baseUrl("http://localhost:" + morp.getMappedPort(8082)) + .baseUrl("http://localhost:" + morp.getMappedPort(8081)) .build(); } diff --git a/src/integrationTest/resources/morp/application.yaml b/src/integrationTest/resources/morp/application.yaml index 4f98dfb2..3c15a083 100644 --- a/src/integrationTest/resources/morp/application.yaml +++ b/src/integrationTest/resources/morp/application.yaml @@ -1,9 +1,8 @@ -# keycloak is running on 8080 so we need different ports for Morp here server: - port: 8081 + port: 8080 management: server: - port: 8082 + port: 8081 # Routing config spring: @@ -14,7 +13,7 @@ spring: - id: upstream-tenant-path uri: http://upstream:1080 predicates: - - Host=morp:8081 + - Host=morp - Path=/upstream/{tenant}/{path} filters: - SetPath=/{path} @@ -23,7 +22,7 @@ spring: - id: upstream-tenant-host uri: http://upstream:1080 predicates: - - Host={tenant}-morp:8081 + - Host={tenant}-morp - Path=/upstream/{path} filters: - SetPath=/{path} diff --git a/src/test/java/io/jaconi/morp/MorpApplicationTests.java b/src/test/java/io/jaconi/morp/MorpApplicationTests.java index 06ea824b..6c37283e 100644 --- a/src/test/java/io/jaconi/morp/MorpApplicationTests.java +++ b/src/test/java/io/jaconi/morp/MorpApplicationTests.java @@ -13,7 +13,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -@SpringBootTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class MorpApplicationTests { @Nested diff --git a/src/test/java/io/jaconi/morp/oauth/MorpReactiveOAuth2UserServiceTest.java b/src/test/java/io/jaconi/morp/oauth/MorpReactiveOAuth2UserServiceTest.java index 15d403af..005c6452 100644 --- a/src/test/java/io/jaconi/morp/oauth/MorpReactiveOAuth2UserServiceTest.java +++ b/src/test/java/io/jaconi/morp/oauth/MorpReactiveOAuth2UserServiceTest.java @@ -30,7 +30,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -@SpringBootTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class MorpReactiveOAuth2UserServiceTest { @Autowired diff --git a/src/test/java/io/jaconi/morp/oauth/MorpReactiveOidcUserServiceTest.java b/src/test/java/io/jaconi/morp/oauth/MorpReactiveOidcUserServiceTest.java index a63065d8..b64def27 100644 --- a/src/test/java/io/jaconi/morp/oauth/MorpReactiveOidcUserServiceTest.java +++ b/src/test/java/io/jaconi/morp/oauth/MorpReactiveOidcUserServiceTest.java @@ -26,7 +26,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -@SpringBootTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class MorpReactiveOidcUserServiceTest { @Autowired diff --git a/src/test/java/io/jaconi/morp/oauth/RegistrationResolverTest.java b/src/test/java/io/jaconi/morp/oauth/RegistrationResolverTest.java index e7e3920a..24e9939a 100644 --- a/src/test/java/io/jaconi/morp/oauth/RegistrationResolverTest.java +++ b/src/test/java/io/jaconi/morp/oauth/RegistrationResolverTest.java @@ -15,7 +15,7 @@ import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.when; -@SpringBootTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class RegistrationResolverTest { @Autowired diff --git a/src/test/java/io/jaconi/morp/oauth/TenantAwareClientRegistrationRepositoryTest.java b/src/test/java/io/jaconi/morp/oauth/TenantAwareClientRegistrationRepositoryTest.java index dacd5a9d..0c73df37 100644 --- a/src/test/java/io/jaconi/morp/oauth/TenantAwareClientRegistrationRepositoryTest.java +++ b/src/test/java/io/jaconi/morp/oauth/TenantAwareClientRegistrationRepositoryTest.java @@ -16,7 +16,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; -@SpringBootTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class TenantAwareClientRegistrationRepositoryTest { private static final String TENANT = "tenant1";