diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 521010b58a..5af569f7c7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: uses: docker/setup-buildx-action@v3 - uses: actions/setup-java@v4 with: - java-version: 11 + java-version: 17 distribution: 'zulu' cache: 'gradle' - name: Prepare build variables diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 036ca190a2..6cce09965f 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -19,7 +19,7 @@ jobs: uses: docker/setup-buildx-action@v3 - uses: actions/setup-java@v4 with: - java-version: 11 + java-version: 17 distribution: 'zulu' cache: 'gradle' - name: Prepare build variables diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5fa8fcc1aa..544178928f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: uses: docker/setup-buildx-action@v3 - uses: actions/setup-java@v4 with: - java-version: 11 + java-version: 17 distribution: 'zulu' cache: 'gradle' - name: Assemble release info diff --git a/Dockerfile.compile b/Dockerfile.compile index 5609ffa51a..f47e1a778d 100644 --- a/Dockerfile.compile +++ b/Dockerfile.compile @@ -1,9 +1,9 @@ -FROM ubuntu:bionic +FROM ubuntu:jammy LABEL maintainer="sig-platform@spinnaker.io" RUN echo "mysql-server mysql-server/root_password password sa" | debconf-set-selections RUN echo "mysql-server mysql-server/root_password_again password sa" | debconf-set-selections RUN apt-get update && apt-get install -y \ - openjdk-11-jdk \ + openjdk-17-jdk \ mysql-server \ && rm -rf /var/lib/apt/lists/* ENV GRADLE_USER_HOME /workspace/.gradle diff --git a/Dockerfile.slim b/Dockerfile.slim index 0e2429c98d..3f1e011c21 100644 --- a/Dockerfile.slim +++ b/Dockerfile.slim @@ -1,6 +1,6 @@ -FROM alpine:3.11 +FROM alpine:3.20 LABEL maintainer="sig-platform@spinnaker.io" -RUN apk --no-cache add --update bash openjdk11-jre +RUN apk --no-cache add --update bash openjdk17-jre RUN addgroup -S -g 10111 spinnaker RUN adduser -S -G spinnaker -u 10111 spinnaker COPY keel-web/build/install/keel /opt/keel diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index 4a492faf9d..6d5f3cc612 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -1,6 +1,6 @@ -FROM ubuntu:bionic +FROM ubuntu:jammy 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 openjdk-17-jre-headless wget RUN adduser --system --uid 10111 --group spinnaker COPY keel-web/build/install/keel /opt/keel RUN mkdir -p /opt/keel/plugins && chown -R spinnaker:nogroup /opt/keel/plugins diff --git a/build.gradle b/build.gradle index 9b015eb7ff..5f74024e9f 100644 --- a/build.gradle +++ b/build.gradle @@ -30,10 +30,6 @@ allprojects { apply(plugin: "com.github.ben-manes.versions") apply(plugin: "com.diffplug.spotless") - java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } dependencies { annotationProcessor(platform("io.spinnaker.kork:kork-bom:${korkVersion}")) diff --git a/gradle.properties b/gradle.properties index 275ab41834..7f16895cca 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,4 @@ +targetJava17=true kotlinVersion=1.6.21 fiatVersion=1.50.0 korkVersion=7.243.0 @@ -9,6 +10,8 @@ okHttpVersion=4.5.0 resilience4jVersion=1.5.0 spinnakerGradleVersion=8.32.1 +org.gradle.jvmargs=-Xmx4g -Xms4g + # Used to control whether to spin up docker to run liquibase before jooq buildingInDocker=false diff --git a/gradle/kotlin.gradle b/gradle/kotlin.gradle index 2d790a97bb..aef735ac5e 100644 --- a/gradle/kotlin.gradle +++ b/gradle/kotlin.gradle @@ -5,7 +5,7 @@ apply plugin: "kotlin-allopen" compileKotlin { kotlinOptions { languageVersion = "1.6" - jvmTarget = "11" + jvmTarget = "17" // see https://kotlinlang.org/docs/java-to-kotlin-interop.html#compatibility-mode-for-default-methods freeCompilerArgs += "-Xjvm-default=enable" } @@ -14,7 +14,7 @@ compileKotlin { compileTestKotlin { kotlinOptions { languageVersion = "1.6" - jvmTarget = "11" + jvmTarget = "17" freeCompilerArgs += "-Xjvm-default=enable" } } diff --git a/keel-core-test/src/main/kotlin/com/netflix/spinnaker/keel/persistence/ArtifactRepositoryTests.kt b/keel-core-test/src/main/kotlin/com/netflix/spinnaker/keel/persistence/ArtifactRepositoryTests.kt index fce0c44a3c..ef920c0821 100644 --- a/keel-core-test/src/main/kotlin/com/netflix/spinnaker/keel/persistence/ArtifactRepositoryTests.kt +++ b/keel-core-test/src/main/kotlin/com/netflix/spinnaker/keel/persistence/ArtifactRepositoryTests.kt @@ -54,6 +54,7 @@ import strikt.assertions.isNull import strikt.assertions.isTrue import java.time.Clock import java.time.Instant +import java.time.temporal.ChronoUnit abstract class ArtifactRepositoryTests : JUnit5Minutests { val publisher: ApplicationEventPublisher = mockk(relaxed = true) @@ -649,7 +650,10 @@ abstract class ArtifactRepositoryTests : JUnit5Minutests } context("artifact creation timestamp exists") { - val createdAt = Instant.now() + // We truncate this since we're using a serialization to java that reduces the level of precision + // and later comparisons break otherwise. This is needed to work with generated columns in + // certain databases. See the PrecisionSqlSerializer class for more info + val createdAt = Instant.now().truncatedTo(ChronoUnit.MICROS) before { subject.register(versionedSnapshotDebian) diff --git a/keel-core/src/main/java/com/netflix/spinnaker/keel/serialization/PrecisionSqlSerializer.java b/keel-core/src/main/java/com/netflix/spinnaker/keel/serialization/PrecisionSqlSerializer.java new file mode 100644 index 0000000000..cdfaeaaae4 --- /dev/null +++ b/keel-core/src/main/java/com/netflix/spinnaker/keel/serialization/PrecisionSqlSerializer.java @@ -0,0 +1,46 @@ +package com.netflix.spinnaker.keel.serialization; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.datatype.jsr310.ser.InstantSerializerBase; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; + +/** + * This class overloads the default Serialization instance in the JavaTimeModule to a precision + * level that the database can process via str_to_date string conversion It's needed because we do + * JSON serialization to a column and then the column gets used to generate another field via string + * conversion. Specifically * triggered_at * dismissed_at are "generated" columns. NOTE this should + * ONLY impact running on Linux as the system clock on Linux systems returns a higher precision in + * Java 17+. It's also impactful as ISO_INSTANT has NO precision limitation giving inconsistent + * string output as a result + * + *

See + * + *

+ * + *

NOTE we're going to 6 digits to match Mysql str_to_date function + * limits on microseconds parsing. This is used in the SQL definition: + * add column triggered_at datetime(3) generated always as (str_to_date(json->>'$.triggeredAt', '%Y-%m-%dT%T.%fZ')) + * column in the 20210616-create-dismissible-notifications.yml liquibase change set. + */ +public class PrecisionSqlSerializer extends InstantSerializerBase { + public PrecisionSqlSerializer() { + super( + Instant.class, + Instant::toEpochMilli, + Instant::getEpochSecond, + Instant::getNano, + new DateTimeFormatterBuilder().appendInstant(6).toFormatter()); + } + + @Override + protected InstantSerializerBase withFormat( + Boolean aBoolean, DateTimeFormatter dateTimeFormatter, JsonFormat.Shape shape) { + return this; + } +} diff --git a/keel-core/src/main/kotlin/com/netflix/spinnaker/keel/serialization/ObjectMappers.kt b/keel-core/src/main/kotlin/com/netflix/spinnaker/keel/serialization/ObjectMappers.kt index 9f786634d5..da9b069c1b 100644 --- a/keel-core/src/main/kotlin/com/netflix/spinnaker/keel/serialization/ObjectMappers.kt +++ b/keel-core/src/main/kotlin/com/netflix/spinnaker/keel/serialization/ObjectMappers.kt @@ -16,8 +16,8 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.registerKotlinModule import com.netflix.spinnaker.keel.jackson.registerKeelApiModule import de.huxhorn.sulky.ulid.ULID -import org.springframework.boot.jackson.JsonComponentModule import java.text.SimpleDateFormat +import java.time.Instant import java.util.TimeZone /** @@ -29,18 +29,20 @@ fun configuredObjectMapper(): ObjectMapper = ObjectMapper().configureForKeel() * Factory method for [YAMLMapper]s configured how we like 'em. */ fun configuredYamlMapper(): YAMLMapper = YAMLMapper().configureForKeel().disable(USE_NATIVE_TYPE_ID) - -fun T.configureForKeel(): T = - apply { +fun T.configureForKeel(): T { + val javaTimeModule = JavaTimeModule() + javaTimeModule.addSerializer(Instant::class.java,PrecisionSqlSerializer()) + return apply { registerKeelApiModule() .registerKotlinModule() .registerULIDModule() - .registerModule(JavaTimeModule()) + .registerModule(javaTimeModule) .configureSaneDateTimeRepresentation() .disable(FAIL_ON_UNKNOWN_PROPERTIES) .enable(ACCEPT_CASE_INSENSITIVE_ENUMS) .setSerializationInclusion(NON_NULL) } +} private fun ObjectMapper.registerULIDModule(): ObjectMapper = registerModule( diff --git a/keel-core/src/test/kotlin/com/netflix/spinnaker/keel/serialization/ObjectMapperSerializationTests.kt b/keel-core/src/test/kotlin/com/netflix/spinnaker/keel/serialization/ObjectMapperSerializationTests.kt new file mode 100644 index 0000000000..4d75665a4e --- /dev/null +++ b/keel-core/src/test/kotlin/com/netflix/spinnaker/keel/serialization/ObjectMapperSerializationTests.kt @@ -0,0 +1,34 @@ +package com.netflix.spinnaker.keel.serialization + +import dev.minutest.junit.JUnit5Minutests +import dev.minutest.rootContext +import org.assertj.core.api.Assertions.assertThat +import java.time.Instant +import java.time.LocalDateTime +import java.time.OffsetDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeFormatterBuilder + +class ObjectMapperSerializationTests : JUnit5Minutests { + + class ObjectMapperSerializationTestsObject { + var date = Instant.from(DateTimeFormatter.ISO_INSTANT.parse("2024-03-10T12:28:19.228816801Z")) + } + + fun tests() = rootContext { + test("Verify instant precision is no more than 6 characters") { + val mapper = configuredObjectMapper() + val sampleObject = ObjectMapperSerializationTestsObject() + val readBack = mapper.readValue( + mapper.writeValueAsString(sampleObject), + ObjectMapperSerializationTestsObject::class.java + ) + // 6 precision is the max allowed via str_to_date in SQL. See + // https://dev.mysql.com/doc/refman/8.4/en/date-and-time-type-syntax.html#:~:text=MySQL%20permits%20fractional%20seconds%20for,microseconds%20(6%20digits)%20precision. + // SO we want to make sure waht we store matches this + val foramt = DateTimeFormatterBuilder().parseCaseInsensitive().appendInstant(6).parseStrict().toFormatter() + assertThat(readBack.date).isEqualTo(foramt.format(sampleObject.date)) + } + } +} diff --git a/keel-sql/src/test/resources/testcontainers.properties b/keel-sql/src/test/resources/testcontainers.properties index 5ecaa452ec..06137e2be6 100644 --- a/keel-sql/src/test/resources/testcontainers.properties +++ b/keel-sql/src/test/resources/testcontainers.properties @@ -1 +1 @@ -ryuk.container.image = public.ecr.aws/s4w6t4b6/testcontainers/ryuk:0.3.4 +ryuk.container.image = testcontainers/ryuk:0.11.0