diff --git a/front50-integration/front50-integration.gradle b/front50-integration/front50-integration.gradle index dc2d60b6e..4727329df 100644 --- a/front50-integration/front50-integration.gradle +++ b/front50-integration/front50-integration.gradle @@ -6,8 +6,10 @@ dependencies { testImplementation "org.testcontainers:testcontainers" testImplementation "org.testcontainers:junit-jupiter" testImplementation "org.testcontainers:mysql" + testImplementation "org.testcontainers:postgresql" testRuntimeOnly "ch.qos.logback:logback-classic" testRuntimeOnly "com.mysql:mysql-connector-j" + testRuntimeOnly "org.postgresql:postgresql" } test.configure { diff --git a/front50-integration/src/test/java/com/netflix/spinnaker/front50/CompositeStorageContainerSqlToProgressTest.java b/front50-integration/src/test/java/com/netflix/spinnaker/front50/CompositeStorageContainerSqlToProgressTest.java new file mode 100644 index 000000000..b17a27600 --- /dev/null +++ b/front50-integration/src/test/java/com/netflix/spinnaker/front50/CompositeStorageContainerSqlToProgressTest.java @@ -0,0 +1,318 @@ +/* + * Copyright 2025 Harness, 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.front50; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.testcontainers.shaded.org.awaitility.Awaitility.await; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.sql.*; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.PostgreSQLContainer; +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 CompositeStorageContainerSqlToProgressTest { + + private static final String MYSQL_NETWORK_ALIAS = "mysqlPrimary"; + private static final String POSTGRES_NETWORK_ALIAS = "postgresHost"; + + private static final int POSTGRES_PORT = 5432; + + private static final int MYSQL_PORT = 3306; + + private static final Logger logger = + LoggerFactory.getLogger(CompositeStorageContainerSqlToProgressTest.class); + + private static final Network network = Network.newNetwork(); + + // withNetwork/withNetworkAliases return a GenericContainer, so call them elsewhere. + private static final MySQLContainer mysql = + new MySQLContainer(DockerImageName.parse("mysql:8.0.37")).withDatabaseName("front50"); + + private static final PostgreSQLContainer postgres = + new PostgreSQLContainer(DockerImageName.parse("postgres:15")) + .withDatabaseName("front50migrated"); + + private static GenericContainer front50Container; + + @BeforeAll + static void setupOnce() throws Exception { + 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); + + mysql.withNetworkAliases(MYSQL_NETWORK_ALIAS).withNetwork(network); + mysql.start(); + + postgres.withNetworkAliases(POSTGRES_NETWORK_ALIAS).withNetwork(network); + postgres.start(); + + DockerImageName dockerImageName = DockerImageName.parse(fullDockerImageName); + + front50Container = + new GenericContainer(dockerImageName) + .withNetwork(network) + .withExposedPorts(8080) + .dependsOn(mysql) + .dependsOn(postgres) + .waitingFor(Wait.forHealthcheck().withStartupTimeout(Duration.ofSeconds(90))) + .withEnv("SPRING_APPLICATION_JSON", getSpringApplicationJson()); + + Slf4jLogConsumer logConsumer = new Slf4jLogConsumer(logger); + front50Container.start(); + front50Container.followOutput(logConsumer); + } + + private static String getSpringApplicationJson() throws JsonProcessingException { + String jdbcUrl = + "jdbc:mysql://" + + MYSQL_NETWORK_ALIAS + + ":" + + MYSQL_PORT + + "/" + + mysql.getDatabaseName() + + "?useSSL=false"; + logger.info("jdbcUrl: '{}'", jdbcUrl); + String jdbcUrlSecondary = + "jdbc:postgresql://" + + POSTGRES_NETWORK_ALIAS + + ":" + + POSTGRES_PORT + + "/" + + postgres.getDatabaseName() + + "?useSSL=false"; + logger.info("jdbcUrlSecondary: '{}'", jdbcUrlSecondary); + Map properties = + Map.ofEntries( + Map.entry("spinnaker.s3.enabled", "false"), + Map.entry("spinnaker.migration.enabled", "true"), + Map.entry("spinnaker.migration.primaryName", "secondarySqlStorageService"), + Map.entry("spinnaker.migration.previousName", "sqlStorageService"), + Map.entry("spinnaker.migration.writeOnly", "false"), + Map.entry("spinnaker.migration.deleteOrphans", "false"), + Map.entry("spinnaker.migration.compositeStorageService.enabled", "true"), + Map.entry("spinnaker.migration.compositeStorageService.reads.primary", "true"), + Map.entry("spinnaker.migration.compositeStorageService.reads.previous", "false"), + Map.entry("sql.enabled", "true"), + Map.entry("sql.secondary.enabled", "true"), + Map.entry("sql.secondary.poolName", "secondary"), + Map.entry("sql.connectionPools.default.default", "true"), + Map.entry("sql.connectionPools.default.jdbcUrl", jdbcUrl), + Map.entry("sql.connectionPools.default.user", mysql.getUsername()), + Map.entry("sql.connectionPools.default.password", mysql.getPassword()), + Map.entry("sql.connectionPools.secondary.enabled", "true"), + Map.entry("sql.connectionPools.secondary.jdbcUrl", jdbcUrlSecondary), + Map.entry("sql.connectionPools.secondary.user", postgres.getUsername()), + Map.entry("sql.connectionPools.secondary.password", postgres.getPassword()), + Map.entry("sql.connectionPools.secondary.dialect", "POSTGRES"), + Map.entry("sql.migration.jdbcUrl", jdbcUrl), + Map.entry("sql.migration.user", mysql.getUsername()), + Map.entry("sql.migration.password", mysql.getPassword()), + Map.entry("sql.secondaryMigration.jdbcUrl", jdbcUrlSecondary), + Map.entry("sql.secondaryMigration.user", postgres.getUsername()), + Map.entry("sql.secondaryMigration.password", postgres.getPassword()), + Map.entry("services.fiat.baseUrl", "http://nowhere")); + ObjectMapper mapper = new ObjectMapper(); + return mapper.writeValueAsString(properties); + } + + @AfterAll + static void cleanupOnce() { + if (front50Container != null) { + front50Container.stop(); + } + + if (mysql != null) { + mysql.stop(); + } + + if (postgres != null) { + postgres.stop(); + } + } + + @BeforeEach + void init(TestInfo testInfo) { + System.out.println("--------------- Test " + testInfo.getDisplayName()); + } + + @Test + void verifySecondaryPoolLiquibaseMigration() throws Exception { + String query = "SELECT ID FROM DATABASECHANGELOG;"; + Connection connectionSecondary = + DriverManager.getConnection( + postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()); + ResultSet outputSecondary = connectionSecondary.prepareStatement(query).executeQuery(); + Connection connectionPrimary = + DriverManager.getConnection(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + ResultSet outputPrimary = connectionPrimary.prepareStatement(query).executeQuery(); + + List> expected = convertResultSetToList(outputPrimary); + List> actual = convertResultSetToList(outputSecondary); + assertThat(actual.contains(expected)); + assertThat(actual.size()).isGreaterThan(1); + assertThat(expected.size()).isGreaterThan(1); + assertThat(actual.size()).isGreaterThan(expected.size()); + + connectionPrimary.close(); + connectionSecondary.close(); + } + + @Test + void verifyFront50CompositeMigrationSqlToPostgres() throws Exception { + // Verify empty Applications prior to migration from previousClass + HttpRequest request = + HttpRequest.newBuilder() + .uri( + new URI( + "http://" + + front50Container.getHost() + + ":" + + front50Container.getFirstMappedPort() + + "/v2/applications")) + .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); + assertThat(response.body()).isEqualTo("[]"); + + // Add data to previous class. Normally this exists but for the sake of the test the data is + // added here + Connection connectionPreviousClass = + DriverManager.getConnection(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + executeSqlScript( + connectionPreviousClass, + new File(getClass().getClassLoader().getResource("front50-test-applications.sql").toURI())); + executeSqlScript( + connectionPreviousClass, + new File( + getClass() + .getClassLoader() + .getResource("front50-test-applications-history.sql") + .toURI())); + + // Verify that the data is migrated to the primaryClass db and returned by the Front50 API + String query = "SELECT ID FROM applications ORDER BY ID;"; + ResultSet outputPrimary = connectionPreviousClass.prepareStatement(query).executeQuery(); + List> expected = convertResultSetToList(outputPrimary); + + Connection connectionPrimaryClass = + DriverManager.getConnection( + postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()); + + await() + .atMost(120, SECONDS) + .pollInterval(5, SECONDS) + .untilAsserted( + () -> { + ResultSet outputSecondary = + connectionPrimaryClass.prepareStatement(query).executeQuery(); + List> actual = convertResultSetToList(outputSecondary); + assertEquals(expected, actual); + assertThat(actual.size()).isEqualTo(2); + assertThat(expected.size()).isEqualTo(2); + }); + + request = + HttpRequest.newBuilder() + .uri( + new URI( + "http://" + + front50Container.getHost() + + ":" + + front50Container.getFirstMappedPort() + + "/v2/applications")) + .GET() + .build(); + + response = client.send(request, HttpResponse.BodyHandlers.ofString()); + assertThat(response).isNotNull(); + logger.info("response: {}, {}", response.statusCode(), response.body()); + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.body()).isNotNull(); + assertThat(response.body()).containsIgnoringCase("app1"); + assertThat(response.body()).containsIgnoringCase("dummy-application2"); + + connectionPrimaryClass.close(); + connectionPreviousClass.close(); + } + + private static List> convertResultSetToList(ResultSet rs) + throws SQLException { + List> results = new ArrayList<>(); + ResultSetMetaData rsmd = rs.getMetaData(); + int columnCount = rsmd.getColumnCount(); + + while (rs.next()) { + Map row = new HashMap<>(); + for (int i = 1; i <= columnCount; i++) { + row.put(rsmd.getColumnName(i), rs.getObject(i)); + } + results.add(row); + } + + return results; + } + + private void executeSqlScript(Connection connection, File sqlFile) throws IOException { + try (FileReader reader = new FileReader(sqlFile); + Statement statement = connection.createStatement()) { + + // Execute the SQL from the file + StringBuilder sqlScript = new StringBuilder(); + int ch; + while ((ch = reader.read()) != -1) { + sqlScript.append((char) ch); + } + + // Execute the script in the database + statement.execute(sqlScript.toString()); + } catch (Exception e) { + throw new IOException("Error executing SQL script", e); + } + } +} diff --git a/front50-integration/src/test/java/com/netflix/spinnaker/front50/CompositeStorageContainerTest.java b/front50-integration/src/test/java/com/netflix/spinnaker/front50/CompositeStorageContainerTest.java new file mode 100644 index 000000000..972185adf --- /dev/null +++ b/front50-integration/src/test/java/com/netflix/spinnaker/front50/CompositeStorageContainerTest.java @@ -0,0 +1,316 @@ +/* + * Copyright 2025 Harness, 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.front50; + +import static java.lang.Thread.sleep; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.testcontainers.shaded.org.awaitility.Awaitility.await; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.sql.*; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.MySQLContainer; +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 CompositeStorageContainerTest { + + private static final String MYSQL_NETWORK_ALIAS = "mysqlPrimary"; + private static final String MYSQL_NETWORK_ALIAS_SECONDARY = "mysqlSecondary"; + + private static final int MYSQL_PORT = 3306; + + private static final Logger logger = LoggerFactory.getLogger(CompositeStorageContainerTest.class); + + private static final Network network = Network.newNetwork(); + + // withNetwork/withNetworkAliases return a GenericContainer, so call them elsewhere. + private static final MySQLContainer mysql = + new MySQLContainer(DockerImageName.parse("mysql:8.0.37")).withDatabaseName("front50"); + + private static final MySQLContainer mysqlSecondary = + new MySQLContainer(DockerImageName.parse("mysql:8.0.37")).withDatabaseName("front50migrated"); + + private static GenericContainer front50Container; + + @BeforeAll + static void setupOnce() throws Exception { + 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); + + mysql.withNetworkAliases(MYSQL_NETWORK_ALIAS).withNetwork(network); + mysql.start(); + + mysqlSecondary.withNetworkAliases(MYSQL_NETWORK_ALIAS_SECONDARY).withNetwork(network); + mysqlSecondary.start(); + + DockerImageName dockerImageName = DockerImageName.parse(fullDockerImageName); + + front50Container = + new GenericContainer(dockerImageName) + .withNetwork(network) + .withExposedPorts(8080) + .dependsOn(mysql) + .dependsOn(mysqlSecondary) + .waitingFor(Wait.forHealthcheck().withStartupTimeout(Duration.ofSeconds(90))) + .withEnv("SPRING_APPLICATION_JSON", getSpringApplicationJson()); + + Slf4jLogConsumer logConsumer = new Slf4jLogConsumer(logger); + front50Container.start(); + front50Container.followOutput(logConsumer); + } + + private static String getSpringApplicationJson() throws JsonProcessingException { + String jdbcUrl = + "jdbc:mysql://" + + MYSQL_NETWORK_ALIAS + + ":" + + MYSQL_PORT + + "/" + + mysql.getDatabaseName() + + "?useSSL=false"; + logger.info("jdbcUrl: '{}'", jdbcUrl); + String jdbcUrlSecondary = + "jdbc:mysql://" + + MYSQL_NETWORK_ALIAS_SECONDARY + + ":" + + MYSQL_PORT + + "/" + + mysqlSecondary.getDatabaseName() + + "?useSSL=false"; + logger.info("jdbcUrlSecondary: '{}'", jdbcUrlSecondary); + Map properties = + Map.ofEntries( + Map.entry("spinnaker.s3.enabled", "false"), + Map.entry("spinnaker.migration.enabled", "true"), + Map.entry("spinnaker.migration.primaryName", "secondarySqlStorageService"), + Map.entry("spinnaker.migration.previousName", "sqlStorageService"), + Map.entry("spinnaker.migration.writeOnly", "false"), + Map.entry("spinnaker.migration.deleteOrphans", "false"), + Map.entry("spinnaker.migration.compositeStorageService.enabled", "true"), + Map.entry("spinnaker.migration.compositeStorageService.reads.primary", "true"), + Map.entry("spinnaker.migration.compositeStorageService.reads.previous", "false"), + Map.entry("sql.enabled", "true"), + Map.entry("sql.secondary.enabled", "true"), + Map.entry("sql.secondary.poolName", "secondary"), + Map.entry("sql.connectionPools.default.default", "true"), + Map.entry("sql.connectionPools.default.jdbcUrl", jdbcUrl), + Map.entry("sql.connectionPools.default.user", mysql.getUsername()), + Map.entry("sql.connectionPools.default.password", mysql.getPassword()), + Map.entry("sql.connectionPools.secondary.enabled", "true"), + Map.entry("sql.connectionPools.secondary.jdbcUrl", jdbcUrlSecondary), + Map.entry("sql.connectionPools.secondary.user", mysqlSecondary.getUsername()), + Map.entry("sql.connectionPools.secondary.password", mysqlSecondary.getPassword()), + Map.entry("sql.migration.jdbcUrl", jdbcUrl), + Map.entry("sql.migration.user", mysql.getUsername()), + Map.entry("sql.migration.password", mysql.getPassword()), + Map.entry("sql.secondaryMigration.jdbcUrl", jdbcUrlSecondary), + Map.entry("sql.secondaryMigration.user", mysqlSecondary.getUsername()), + Map.entry("sql.secondaryMigration.password", mysqlSecondary.getPassword()), + Map.entry("services.fiat.baseUrl", "http://nowhere")); + ObjectMapper mapper = new ObjectMapper(); + return mapper.writeValueAsString(properties); + } + + @AfterAll + static void cleanupOnce() { + if (front50Container != null) { + front50Container.stop(); + } + + if (mysql != null) { + mysql.stop(); + } + + if (mysqlSecondary != null) { + mysqlSecondary.stop(); + } + } + + @BeforeEach + void init(TestInfo testInfo) { + System.out.println("--------------- Test " + testInfo.getDisplayName()); + } + + @Test + void verifySecondaryPoolLiquibaseMigration() throws Exception { + String query = "SELECT ID FROM DATABASECHANGELOG;"; + Connection connectionSecondary = + DriverManager.getConnection( + mysqlSecondary.getJdbcUrl(), + mysqlSecondary.getUsername(), + mysqlSecondary.getPassword()); + ResultSet outputSecondary = connectionSecondary.prepareStatement(query).executeQuery(); + Connection connectionPrimary = + DriverManager.getConnection(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + ResultSet outputPrimary = connectionPrimary.prepareStatement(query).executeQuery(); + + List> expected = convertResultSetToList(outputPrimary); + List> actual = convertResultSetToList(outputSecondary); + assertEquals(expected, actual); + + connectionPrimary.close(); + connectionSecondary.close(); + } + + @Test + void verifyFront50CompositeMigrationSqlToSql() throws Exception { + // Verify empty Applications prior to migration from previousClass + HttpRequest request = + HttpRequest.newBuilder() + .uri( + new URI( + "http://" + + front50Container.getHost() + + ":" + + front50Container.getFirstMappedPort() + + "/v2/applications")) + .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); + assertThat(response.body()).isEqualTo("[]"); + + // Add data to previous class. Normally this exists but for the sake of the test the data is + // added here + Connection connectionPreviousClass = + DriverManager.getConnection(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + executeSqlScript( + connectionPreviousClass, + new File(getClass().getClassLoader().getResource("front50-test-applications.sql").toURI())); + executeSqlScript( + connectionPreviousClass, + new File( + getClass() + .getClassLoader() + .getResource("front50-test-applications-history.sql") + .toURI())); + + sleep(45000); + + // Verify that the data is migrated to the primaryClass db and returned by the Front50 API + String query = "SELECT ID FROM applications ORDER BY ID;"; + ResultSet outputPrimary = connectionPreviousClass.prepareStatement(query).executeQuery(); + List> expected = convertResultSetToList(outputPrimary); + + Connection connectionPrimaryClass = + DriverManager.getConnection( + mysqlSecondary.getJdbcUrl(), + mysqlSecondary.getUsername(), + mysqlSecondary.getPassword()); + + await() + .atMost(120, SECONDS) + .pollInterval(5, SECONDS) + .untilAsserted( + () -> { + ResultSet outputSecondary = + connectionPrimaryClass.prepareStatement(query).executeQuery(); + List> actual = convertResultSetToList(outputSecondary); + assertEquals(expected, actual); + assertThat(actual.size()).isEqualTo(2); + assertThat(expected.size()).isEqualTo(2); + }); + + request = + HttpRequest.newBuilder() + .uri( + new URI( + "http://" + + front50Container.getHost() + + ":" + + front50Container.getFirstMappedPort() + + "/v2/applications")) + .GET() + .build(); + + response = client.send(request, HttpResponse.BodyHandlers.ofString()); + assertThat(response).isNotNull(); + logger.info("response: {}, {}", response.statusCode(), response.body()); + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.body()).isNotNull(); + assertThat(response.body()).containsIgnoringCase("app1"); + assertThat(response.body()).containsIgnoringCase("dummy-application2"); + + connectionPrimaryClass.close(); + connectionPreviousClass.close(); + } + + private static List> convertResultSetToList(ResultSet rs) + throws SQLException { + List> results = new ArrayList<>(); + ResultSetMetaData rsmd = rs.getMetaData(); + int columnCount = rsmd.getColumnCount(); + + while (rs.next()) { + Map row = new HashMap<>(); + for (int i = 1; i <= columnCount; i++) { + row.put(rsmd.getColumnName(i), rs.getObject(i)); + } + results.add(row); + } + + return results; + } + + private void executeSqlScript(Connection connection, File sqlFile) throws IOException { + try (FileReader reader = new FileReader(sqlFile); + Statement statement = connection.createStatement()) { + + // Execute the SQL from the file + StringBuilder sqlScript = new StringBuilder(); + int ch; + while ((ch = reader.read()) != -1) { + sqlScript.append((char) ch); + } + + // Execute the script in the database + statement.execute(sqlScript.toString()); + } catch (Exception e) { + throw new IOException("Error executing SQL script", e); + } + } +} diff --git a/front50-integration/src/test/java/com/netflix/spinnaker/front50/StandaloneContainerTest.java b/front50-integration/src/test/java/com/netflix/spinnaker/front50/StandaloneContainerTest.java index 9aba8e431..6f906ce3c 100644 --- a/front50-integration/src/test/java/com/netflix/spinnaker/front50/StandaloneContainerTest.java +++ b/front50-integration/src/test/java/com/netflix/spinnaker/front50/StandaloneContainerTest.java @@ -54,7 +54,7 @@ class StandaloneContainerTest { // withNetwork/withNetworkAliases return a GenericContainer, so call them elsewhere. private static final MySQLContainer mysql = - new MySQLContainer(DockerImageName.parse("mysql:5.7.22")); + new MySQLContainer(DockerImageName.parse("mysql:8.0.37")); private static GenericContainer front50Container; diff --git a/front50-integration/src/test/resources/front50-test-applications-history.sql b/front50-integration/src/test/resources/front50-test-applications-history.sql new file mode 100644 index 000000000..75f49b5df --- /dev/null +++ b/front50-integration/src/test/resources/front50-test-applications-history.sql @@ -0,0 +1,4 @@ +insert into applications_history (id, body, body_sig, last_modified_at, recorded_at) +values ('app1', '{"name":"app1","description":null,"email":"test@front50.email","updateTs":"1738579339278","createTs":null,"lastModifiedBy":"anonymous","cloudProviders":"kubernetes","trafficGuards":[],"instancePort":80,"user":"[anonymous]"}', '6972009b4230a5af4edb44df87e2703d', 1738579339278, 1738579339311), + ('app1', '{"name":"app1","description":null,"email":"test@front50.email","updateTs":"1738579806480","createTs":"1738579339278","lastModifiedBy":"anonymous","cloudProviders":"kubernetes","trafficGuards":[],"instancePort":80,"platformHealthOnlyShowOverride":false,"platformHealthOnly":true,"user":"[anonymous]","dataSources":{"disabled":[],"enabled":[]},"trafficGuards":[]}', 'ff5168aa9d988755ced0948b29f38be0', 1738579806480, 1738579806483), + ('dummy-application2', '{"name":"dummy-application2","description":null,"email":"test@front50.me","updateTs":"1738579396051","createTs":null,"lastModifiedBy":"anonymous","cloudProviders":"kubernetes","trafficGuards":[],"instancePort":80,"user":"[anonymous]"}', 'ad750a02d4ce18821e09ba93618f7e84', 1738579396051, 1738579396053); diff --git a/front50-integration/src/test/resources/front50-test-applications.sql b/front50-integration/src/test/resources/front50-test-applications.sql new file mode 100644 index 000000000..e39510016 --- /dev/null +++ b/front50-integration/src/test/resources/front50-test-applications.sql @@ -0,0 +1,3 @@ +insert into applications (id, body, created_at, last_modified_at, last_modified_by, is_deleted) +values ('app1', '{"name":"app1","description":null,"email":"test@front50.email","updateTs":"1738579806480","createTs":"1738579339278","lastModifiedBy":"anonymous","cloudProviders":"kubernetes","trafficGuards":[],"instancePort":80,"platformHealthOnlyShowOverride":false,"platformHealthOnly":true,"user":"[anonymous]","dataSources":{"disabled":[],"enabled":[]},"trafficGuards":[]}', 1738579339278, 1738579806480, 'anonymous', 0), + ('dummy-application2', '{"name":"dummy-application2","description":null,"email":"test@front50.me","updateTs":"1738579396051","createTs":null,"lastModifiedBy":"anonymous","cloudProviders":"kubernetes","trafficGuards":[],"instancePort":80,"user":"[anonymous]"}', 1738579396051, 1738579396051, 'anonymous', 0); diff --git a/front50-sql/src/main/kotlin/com/netflix/spinnaker/config/CompositeStorageServiceConfiguration.kt b/front50-sql/src/main/kotlin/com/netflix/spinnaker/config/CompositeStorageServiceConfiguration.kt index af9a0881a..723f5ba81 100644 --- a/front50-sql/src/main/kotlin/com/netflix/spinnaker/config/CompositeStorageServiceConfiguration.kt +++ b/front50-sql/src/main/kotlin/com/netflix/spinnaker/config/CompositeStorageServiceConfiguration.kt @@ -71,9 +71,11 @@ class CompositeStorageServiceConfiguration( beanName: String? ): StorageService { return if (className != null && className.isNotBlank()) { - storageServices.first { it.javaClass.canonicalName == className } + val storageServiceClass = Class.forName(className) + storageServices.find { storageServiceClass.isInstance(it) } + ?: throw IllegalStateException("No StorageService bean of class $className found") } else { - applicationContext.getBean(beanName) as StorageService + beanName?.let { applicationContext.getBean(it) } as StorageService } } } diff --git a/front50-sql/src/main/kotlin/com/netflix/spinnaker/config/SqlConfiguration.kt b/front50-sql/src/main/kotlin/com/netflix/spinnaker/config/SqlConfiguration.kt index 8c74afdd4..e8edbfc69 100644 --- a/front50-sql/src/main/kotlin/com/netflix/spinnaker/config/SqlConfiguration.kt +++ b/front50-sql/src/main/kotlin/com/netflix/spinnaker/config/SqlConfiguration.kt @@ -23,8 +23,8 @@ import com.netflix.spinnaker.kork.sql.config.DefaultSqlConfiguration import com.netflix.spinnaker.kork.sql.config.SqlProperties import java.time.Clock import org.jooq.DSLContext +import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean @@ -58,22 +58,25 @@ class SqlConfiguration { ) @Bean - @ConditionalOnBean(name = ["secondaryJooq"]) + @ConditionalOnProperty("sql.enabled", "sql.secondary.enabled") fun secondarySqlStorageService( objectMapper: ObjectMapper, registry: Registry, - @Qualifier("secondaryJooq") jooq: DSLContext, + @Autowired(required = false) @Qualifier("secondaryJooq") secondaryJooq: DSLContext?, + jooq: DSLContext, sqlProperties: SqlProperties, front50SqlProperties: Front50SqlProperties - ): SqlStorageService = - SqlStorageService( + ): SqlStorageService { + val effectiveJooq = secondaryJooq ?: jooq + return SqlStorageService( objectMapper, registry, - jooq, + effectiveJooq, Clock.systemDefaultZone(), sqlProperties.retries, 1000, sqlProperties.connectionPools.filter { !it.value.default }.keys.first(), front50SqlProperties ) + } }