From 9c53ed8393d838dc5a5b76146c06070ab7ec9ab4 Mon Sep 17 00:00:00 2001 From: Ralph Ursprung Date: Fri, 22 Nov 2024 16:49:12 +0100 Subject: [PATCH 1/3] add spring boot integration this adds an `AutoConfiguration` for spring which automatically triggers the liquibase migration if this library is added to a project which already has spring boot on the classpath. the dependency to spring boot is marked as optional, thus it is not added as a transitive dependency to consumers which do not use spring boot. this needs to be added here until a more generic solution exists directly within spring. see spring-projects/spring-boot#37936 for more details. the test coverage for this is a bit small since we currently don't have access to the instanciated `OpenSearchClient` and would instead have to create one on our own just for the test. in the best case we'd be able to share some code with [`spring-data-opensearch-testcontainers`] (or pull it in as a dependency) to support [`@ServiceConnection`] usage. however, it seems that this library hasn't yet been published to maven, thus this isn't possible and copying over all the code might not make that much sense. for more information see these links: - [general information on spring auto-configuration](https://docs.spring.io/spring-boot/reference/using/auto-configuration.html) - [creating your own auto-configuration](https://docs.spring.io/spring-boot/reference/features/developing-auto-configuration.html) [`spring-data-opensearch-testcontainers`]: https://github.com/opensearch-project/spring-data-opensearch/tree/main/spring-data-opensearch-testcontainers [`@ServiceConnection`]: https://docs.spring.io/spring-boot/reference/testing/testcontainers.html#testing.testcontainers.service-connections --- pom.xml | 20 +++++-- .../database/OpenSearchConnection.java | 1 - .../LiquibaseOpenSearchAutoConfiguration.java | 16 ++++++ .../spring/SpringLiquibaseOpenSearch.java | 54 +++++++++++++++++++ .../SpringLiquibaseOpenSearchProperties.java | 40 ++++++++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../AbstractOpenSearchLiquibaseIT.java | 2 +- .../spring/SpringLiquibaseOpenSearchIT.java | 35 ++++++++++++ .../spring/SpringTestApplication.java | 13 +++++ src/test/resources/application.properties | 3 ++ 10 files changed, 180 insertions(+), 5 deletions(-) create mode 100644 src/main/java/liquibase/ext/opensearch/integration/spring/LiquibaseOpenSearchAutoConfiguration.java create mode 100644 src/main/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearch.java create mode 100644 src/main/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearchProperties.java create mode 100644 src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 src/test/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearchIT.java create mode 100644 src/test/java/liquibase/ext/opensearch/integration/spring/SpringTestApplication.java create mode 100644 src/test/resources/application.properties diff --git a/pom.xml b/pom.xml index 9e12dd6..f1a08bd 100644 --- a/pom.xml +++ b/pom.xml @@ -135,10 +135,24 @@ 2.0.16 test + + - org.slf4j - slf4j-simple - 2.0.16 + org.springframework.boot + spring-boot-starter + 3.3.5 + true + + + org.springframework.boot + spring-boot-configuration-processor + 3.3.5 + true + + + org.springframework.boot + spring-boot-starter-test + 3.3.5 test diff --git a/src/main/java/liquibase/ext/opensearch/database/OpenSearchConnection.java b/src/main/java/liquibase/ext/opensearch/database/OpenSearchConnection.java index d005195..eae6e1b 100644 --- a/src/main/java/liquibase/ext/opensearch/database/OpenSearchConnection.java +++ b/src/main/java/liquibase/ext/opensearch/database/OpenSearchConnection.java @@ -5,7 +5,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import lombok.SneakyThrows; import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; diff --git a/src/main/java/liquibase/ext/opensearch/integration/spring/LiquibaseOpenSearchAutoConfiguration.java b/src/main/java/liquibase/ext/opensearch/integration/spring/LiquibaseOpenSearchAutoConfiguration.java new file mode 100644 index 0000000..88ec8d6 --- /dev/null +++ b/src/main/java/liquibase/ext/opensearch/integration/spring/LiquibaseOpenSearchAutoConfiguration.java @@ -0,0 +1,16 @@ +package liquibase.ext.opensearch.integration.spring; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration +@EnableConfigurationProperties(SpringLiquibaseOpenSearchProperties.class) +@ConditionalOnProperty({"opensearch.uris", "opensearch.liquibase.enabled"}) +public class LiquibaseOpenSearchAutoConfiguration { + @Bean + public SpringLiquibaseOpenSearch getLiquibaseOpenSearch() { + return new SpringLiquibaseOpenSearch(); + } +} diff --git a/src/main/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearch.java b/src/main/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearch.java new file mode 100644 index 0000000..2212b91 --- /dev/null +++ b/src/main/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearch.java @@ -0,0 +1,54 @@ +package liquibase.ext.opensearch.integration.spring; + +import liquibase.Scope; +import liquibase.UpdateSummaryOutputEnum; +import liquibase.command.CommandScope; +import liquibase.command.core.UpdateCommandStep; +import liquibase.command.core.helpers.DbUrlConnectionArgumentsCommandStep; +import liquibase.command.core.helpers.ShowSummaryArgument; +import liquibase.database.DatabaseFactory; +import liquibase.ext.opensearch.database.OpenSearchConnection; +import liquibase.ext.opensearch.database.OpenSearchLiquibaseDatabase; +import liquibase.integration.spring.SpringResourceAccessor; +import liquibase.ui.UIServiceEnum; +import lombok.Getter; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ResourceLoader; + +import static liquibase.ext.opensearch.database.OpenSearchLiquibaseDatabase.OPENSEARCH_PREFIX; +import static liquibase.ext.opensearch.database.OpenSearchLiquibaseDatabase.OPENSEARCH_URI_SEPARATOR; + +public class SpringLiquibaseOpenSearch implements InitializingBean { + + @Autowired + private ResourceLoader resourceLoader; + + @Autowired + private SpringLiquibaseOpenSearchProperties properties; + + @Getter + protected UIServiceEnum uiService = UIServiceEnum.LOGGER; + + @Override + public void afterPropertiesSet() throws Exception { + // liquibase requires the prefix to identify this as an OpenSearch database. + final var url = OPENSEARCH_PREFIX + String.join(OPENSEARCH_URI_SEPARATOR, properties.uris()); + + Scope.child(Scope.Attr.ui.name(), this.uiService.getUiServiceClass().getDeclaredConstructor().newInstance(), + () -> { + final var database = (OpenSearchLiquibaseDatabase) DatabaseFactory.getInstance().openDatabase(url, properties.username(), properties.password(), null, new SpringResourceAccessor(this.resourceLoader)); + final var connection = (OpenSearchConnection) database.getConnection(); + new CommandScope(UpdateCommandStep.COMMAND_NAME) + .addArgumentValue(ShowSummaryArgument.SHOW_SUMMARY_OUTPUT, UpdateSummaryOutputEnum.LOG) + .addArgumentValue(DbUrlConnectionArgumentsCommandStep.DATABASE_ARG, database) + .addArgumentValue(UpdateCommandStep.CHANGELOG_FILE_ARG, properties.liquibase().changelogFile()) + .addArgumentValue(UpdateCommandStep.CONTEXTS_ARG, properties.liquibase().contexts()) + .addArgumentValue(UpdateCommandStep.LABEL_FILTER_ARG, properties.liquibase().labelFilterArgs()) + .execute(); + connection.close(); + database.close(); + }); + } + +} diff --git a/src/main/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearchProperties.java b/src/main/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearchProperties.java new file mode 100644 index 0000000..57945f3 --- /dev/null +++ b/src/main/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearchProperties.java @@ -0,0 +1,40 @@ +package liquibase.ext.opensearch.integration.spring; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.DefaultValue; + +import java.util.List; + +/** + * @param uris URL of the OpenSearch instances to use. If multiple URLs are listed they must all belong to the same cluster. + * @param username Username for authentication with OpenSearch. + * @param password Password for authentication with OpenSearch. + * @param liquibase Liquibase-specific properties. + */ +@ConfigurationProperties("opensearch") +public record SpringLiquibaseOpenSearchProperties( + @DefaultValue("http://localhost:9200") + List uris, + String username, + String password, + + @DefaultValue() + SpringLiquibaseProperties liquibase + ) { + /** + * @param changelogFile Path to the liquibase changelog file (must be on the classpath). + * @param contexts Context filter to be used. + * @param labelFilterArgs Label filter to be used. + */ + public record SpringLiquibaseProperties( + @DefaultValue("true") + Boolean enabled, + + @DefaultValue("db.changelog/db.changelog-master.yaml") + String changelogFile, + + String contexts, + + String labelFilterArgs + ) {} +} diff --git a/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..a0789e5 --- /dev/null +++ b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +liquibase.ext.opensearch.integration.spring.LiquibaseOpenSearchAutoConfiguration diff --git a/src/test/java/liquibase/ext/opensearch/AbstractOpenSearchLiquibaseIT.java b/src/test/java/liquibase/ext/opensearch/AbstractOpenSearchLiquibaseIT.java index b1cf852..4c16be0 100644 --- a/src/test/java/liquibase/ext/opensearch/AbstractOpenSearchLiquibaseIT.java +++ b/src/test/java/liquibase/ext/opensearch/AbstractOpenSearchLiquibaseIT.java @@ -27,7 +27,7 @@ public abstract class AbstractOpenSearchLiquibaseIT { protected OpenSearchLiquibaseDatabase database; protected OpenSearchConnection connection; - protected static final String OPENSEARCH_DOCKER_IMAGE_NAME = "opensearchproject/opensearch:2.18.0"; + public static final String OPENSEARCH_DOCKER_IMAGE_NAME = "opensearchproject/opensearch:2.18.0"; @Container protected OpensearchContainer container = new OpensearchContainer<>(DockerImageName.parse(OPENSEARCH_DOCKER_IMAGE_NAME)); diff --git a/src/test/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearchIT.java b/src/test/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearchIT.java new file mode 100644 index 0000000..77d21e6 --- /dev/null +++ b/src/test/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearchIT.java @@ -0,0 +1,35 @@ +package liquibase.ext.opensearch.integration.spring; + +import org.junit.jupiter.api.Test; +import org.opensearch.testcontainers.OpensearchContainer; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import static liquibase.ext.opensearch.AbstractOpenSearchLiquibaseIT.OPENSEARCH_DOCKER_IMAGE_NAME; + +@Testcontainers +@SpringBootTest +class SpringLiquibaseOpenSearchIT { + + @Container + protected static OpensearchContainer container = new OpensearchContainer<>(DockerImageName.parse(OPENSEARCH_DOCKER_IMAGE_NAME)); + + @DynamicPropertySource + static void setProperties(DynamicPropertyRegistry registry) { + registry.add("opensearch.uris", container::getHttpHostAddress); + registry.add("opensearch.username", container::getUsername); + registry.add("opensearch.password", container::getPassword); + } + + /** + * On context load liquibase is automatically being triggered (hard to test as we'd have to construct our own OpenSearch client). + */ + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/liquibase/ext/opensearch/integration/spring/SpringTestApplication.java b/src/test/java/liquibase/ext/opensearch/integration/spring/SpringTestApplication.java new file mode 100644 index 0000000..f0b8715 --- /dev/null +++ b/src/test/java/liquibase/ext/opensearch/integration/spring/SpringTestApplication.java @@ -0,0 +1,13 @@ +package liquibase.ext.opensearch.integration.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringTestApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringTestApplication.class, args); + } + +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..fbf2714 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,3 @@ +spring.application.name=liquibase-opensearch-test + +opensearch.liquibase.changelog-file=liquibase/ext/changelog.httprequest.yaml From 20a56db20392818f40558dcb113026d4b6fa414d Mon Sep 17 00:00:00 2001 From: Ralph Ursprung Date: Fri, 22 Nov 2024 18:59:09 +0100 Subject: [PATCH 2/3] `OpenSearchConnection`: use built-in `TrustAllStrategy` no need to hand-roll this if there's a built-in alternative. --- .../ext/opensearch/database/OpenSearchConnection.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/liquibase/ext/opensearch/database/OpenSearchConnection.java b/src/main/java/liquibase/ext/opensearch/database/OpenSearchConnection.java index eae6e1b..74ad9cd 100644 --- a/src/main/java/liquibase/ext/opensearch/database/OpenSearchConnection.java +++ b/src/main/java/liquibase/ext/opensearch/database/OpenSearchConnection.java @@ -12,6 +12,7 @@ import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.client5.http.ssl.TrustAllStrategy; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.nio.ssl.TlsStrategy; import org.apache.hc.core5.reactor.ssl.TlsDetails; @@ -143,7 +144,7 @@ private void connect() { try { sslcontext = SSLContextBuilder .create() - .loadTrustMaterial(null, (chains, authType) -> true) + .loadTrustMaterial(null, new TrustAllStrategy()) .build(); } catch (final NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) { throw new RuntimeException(e); From 75ab59de03cedeac3dab5878269e85c02c22f2e9 Mon Sep 17 00:00:00 2001 From: Andriy Redko Date: Thu, 2 Jan 2025 19:28:29 -0500 Subject: [PATCH 3/3] Add more tests to make sure Liquibase and Liquibase OpenSearch combinations work Signed-off-by: Andriy Redko --- pom.xml | 11 ++++++ .../LiquibaseOpenSearchAutoConfiguration.java | 22 ++++++++++- ...SpringLiquibaseOpenSearchDataSourceIT.java | 39 +++++++++++++++++++ .../SpringLiquibaseOpenSearchDisabledIT.java | 25 ++++++++++++ .../spring/SpringLiquibaseOpenSearchIT.java | 4 +- .../liquibase/db/liquibase-changeLog.xml | 19 +++++++++ 6 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 src/test/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearchDataSourceIT.java create mode 100644 src/test/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearchDisabledIT.java create mode 100644 src/test/resources/liquibase/db/liquibase-changeLog.xml diff --git a/pom.xml b/pom.xml index f1a08bd..29662f3 100644 --- a/pom.xml +++ b/pom.xml @@ -155,6 +155,17 @@ 3.3.5 test + + org.springframework.boot + spring-boot-starter-jdbc + 3.3.5 + test + + + com.h2database + h2 + test + diff --git a/src/main/java/liquibase/ext/opensearch/integration/spring/LiquibaseOpenSearchAutoConfiguration.java b/src/main/java/liquibase/ext/opensearch/integration/spring/LiquibaseOpenSearchAutoConfiguration.java index 88ec8d6..7e78901 100644 --- a/src/main/java/liquibase/ext/opensearch/integration/spring/LiquibaseOpenSearchAutoConfiguration.java +++ b/src/main/java/liquibase/ext/opensearch/integration/spring/LiquibaseOpenSearchAutoConfiguration.java @@ -1,14 +1,34 @@ package liquibase.ext.opensearch.integration.spring; +import org.opensearch.client.opensearch.OpenSearchClient; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.AllNestedConditions; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; @AutoConfiguration @EnableConfigurationProperties(SpringLiquibaseOpenSearchProperties.class) -@ConditionalOnProperty({"opensearch.uris", "opensearch.liquibase.enabled"}) +@Conditional(LiquibaseOpenSearchAutoConfiguration.LiquibaseOpenSearchCondition.class) +@ConditionalOnClass(OpenSearchClient.class) public class LiquibaseOpenSearchAutoConfiguration { + static final class LiquibaseOpenSearchCondition extends AllNestedConditions { + LiquibaseOpenSearchCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty(prefix = "opensearch", name = "liquibase.enabled", matchIfMissing = true) + private static final class LiquibaseOpenSearchEnabledCondition { + } + + @ConditionalOnProperty(prefix = "opensearch", name = "uris") + private static final class LiquibaseOpenSearchUrlCondition { + + } + } + @Bean public SpringLiquibaseOpenSearch getLiquibaseOpenSearch() { return new SpringLiquibaseOpenSearch(); diff --git a/src/test/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearchDataSourceIT.java b/src/test/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearchDataSourceIT.java new file mode 100644 index 0000000..1a9c116 --- /dev/null +++ b/src/test/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearchDataSourceIT.java @@ -0,0 +1,39 @@ +package liquibase.ext.opensearch.integration.spring; + +import org.junit.jupiter.api.Test; +import org.opensearch.testcontainers.OpensearchContainer; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import static liquibase.ext.opensearch.AbstractOpenSearchLiquibaseIT.OPENSEARCH_DOCKER_IMAGE_NAME; + +@Testcontainers +@SpringBootTest(properties = { + "spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE", + "spring.datasource.platform=h2", + "spring.liquibase.change-log=classpath:/liquibase/db/liquibase-changeLog.xml" +}) +class SpringLiquibaseOpenSearchDataSourceIT { + + @Container + protected static OpensearchContainer container = new OpensearchContainer<>(DockerImageName.parse(OPENSEARCH_DOCKER_IMAGE_NAME)); + + @DynamicPropertySource + static void setProperties(DynamicPropertyRegistry registry) { + registry.add("opensearch.uris", container::getHttpHostAddress); + registry.add("opensearch.username", container::getUsername); + registry.add("opensearch.password", container::getPassword); + } + + /** + * On context load liquibase is automatically being triggered (hard to test as we'd have to construct our own OpenSearch client). + */ + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearchDisabledIT.java b/src/test/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearchDisabledIT.java new file mode 100644 index 0000000..ac30eea --- /dev/null +++ b/src/test/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearchDisabledIT.java @@ -0,0 +1,25 @@ +package liquibase.ext.opensearch.integration.spring; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.testcontainers.junit.jupiter.Testcontainers; + +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.hamcrest.MatcherAssert.assertThat; + +@Testcontainers +@SpringBootTest(properties = { + "spring.liquibase.enabled=false", + "opensearch.liquibase.enabled=false" +}) +class SpringLiquibaseOpenSearchDisabledIT { + @Autowired private ApplicationContext context; + + @Test + void contextLoads() { + assertThat(context.getBeansOfType(SpringLiquibaseOpenSearch.class).keySet(), empty()); + } + +} diff --git a/src/test/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearchIT.java b/src/test/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearchIT.java index 77d21e6..9f2fe06 100644 --- a/src/test/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearchIT.java +++ b/src/test/java/liquibase/ext/opensearch/integration/spring/SpringLiquibaseOpenSearchIT.java @@ -12,7 +12,9 @@ import static liquibase.ext.opensearch.AbstractOpenSearchLiquibaseIT.OPENSEARCH_DOCKER_IMAGE_NAME; @Testcontainers -@SpringBootTest +@SpringBootTest(properties = { + "spring.liquibase.enabled=false" +}) class SpringLiquibaseOpenSearchIT { @Container diff --git a/src/test/resources/liquibase/db/liquibase-changeLog.xml b/src/test/resources/liquibase/db/liquibase-changeLog.xml new file mode 100644 index 0000000..f7f505e --- /dev/null +++ b/src/test/resources/liquibase/db/liquibase-changeLog.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file