From 634f46b424c29fe321e7f0a5290f303354c045f3 Mon Sep 17 00:00:00 2001 From: Alexandros Pappas <6052803+apappascs@users.noreply.github.com> Date: Wed, 2 Oct 2024 20:36:29 +0200 Subject: [PATCH] feat(config-server): add MongoDB environment repository support (#2390) * feat(config-server): add MongoDB environment repository support This commit introduces MongoDB as a new backend option for the Spring Cloud Config Server, enabling users to store and manage their configuration properties in a MongoDB database. * resolve pr comments * add documentation for mongodb backend * resolve comments --- docs/modules/ROOT/nav.adoc | 1 + .../environment-repository/mongo-backend.adoc | 58 ++++++ spring-cloud-config-server/pom.xml | 15 ++ .../EnvironmentRepositoryConfiguration.java | 36 +++- .../MongoDbEnvironmentProperties.java | 97 ++++++++++ .../MongoDbEnvironmentRepository.java | 127 ++++++++++++ .../MongoDbEnvironmentRepositoryFactory.java | 40 ++++ ...vironmentRepositoryConfigurationTests.java | 78 ++++++++ .../MongoDbEnvironmentRepositoryTests.java | 182 ++++++++++++++++++ .../src/test/resources/data-mongo.json | 66 +++++++ 10 files changed, 699 insertions(+), 1 deletion(-) create mode 100644 docs/modules/ROOT/pages/server/environment-repository/mongo-backend.adoc create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/MongoDbEnvironmentProperties.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/MongoDbEnvironmentRepository.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/MongoDbEnvironmentRepositoryFactory.java create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/MongoDbEnvironmentRepositoryConfigurationTests.java create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/MongoDbEnvironmentRepositoryTests.java create mode 100644 spring-cloud-config-server/src/test/resources/data-mongo.json diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 70d2e8c8..789342ef 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -15,6 +15,7 @@ *** xref:server/environment-repository/aws-parameter-store-backend.adoc[] *** xref:server/environment-repository/aws-secrets-manager-backend.adoc[] *** xref:server/environment-repository/credhub-backend.adoc[] +*** xref:server/environment-repository/mongo-backend.adoc[] *** xref:server/environment-repository/composite-repositories.adoc[] *** xref:server/environment-repository/custom-enviroment-repository.adoc[] *** xref:server/environment-repository/property-overrides.adoc[] diff --git a/docs/modules/ROOT/pages/server/environment-repository/mongo-backend.adoc b/docs/modules/ROOT/pages/server/environment-repository/mongo-backend.adoc new file mode 100644 index 00000000..653a3f69 --- /dev/null +++ b/docs/modules/ROOT/pages/server/environment-repository/mongo-backend.adoc @@ -0,0 +1,58 @@ +[[mongo-backend]] += MongoDB Backend +:page-section-summary-toc: 1 + +Spring Cloud Config Server supports MongoDB as a backend for configuration properties. +You can enable this feature by adding `spring-boot-starter-data-mongodb` to the classpath and using the `mongodb` profile. + +[source,xml,indent=0] +.pom.xml +---- + + + org.springframework.boot + spring-boot-starter-data-mongodb + + +---- + +Configure your application's `application.properties` or `application.yml` to point to your MongoDB instance: + +[source,yaml] +---- +spring: + profiles: + active: mongodb + data: + mongodb: + database: your-database-name + port: '27017' + host: localhost +---- + +The configuration properties should be stored in documents within the `properties` collection. Each document represents a set of properties for a given application, profile, and label. + +Example MongoDB document: + +[source,json] +---- +{ + "application": "myapp", + "profile": "development", + "label": "master", + "properties": { + "property1": "value1", + "property2": "value2" + } +} +---- + +You can disable autoconfiguration for `MongoDbEnvironmentRepository` by setting the `spring.cloud.config.server.mongodb.enabled` property to `false`. + +The default values for MongoDB backend configuration are as follows: + +- **Collection Name:** `"properties"` (Name of the MongoDB collection to query for configuration properties.) + +- **Default Label:** `"master"` (Default label to use if none is specified.) + +NOTE: You can change these defaults by setting `spring.cloud.config.server.mongodb.collection` and `spring.cloud.config.server.mongodb.defaultLabel` in your application's configuration. diff --git a/spring-cloud-config-server/pom.xml b/spring-cloud-config-server/pom.xml index 2c734181..4695997f 100644 --- a/spring-cloud-config-server/pom.xml +++ b/spring-cloud-config-server/pom.xml @@ -147,6 +147,11 @@ google-auth-library-oauth2-http true + + org.springframework.boot + spring-boot-starter-data-mongodb + true + com.h2database h2 @@ -232,6 +237,16 @@ spring-core-test test + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + mongodb + test + diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java index bdfd8237..9a2ca957 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java @@ -69,6 +69,9 @@ import org.springframework.cloud.config.server.environment.HttpRequestConfigToke import org.springframework.cloud.config.server.environment.JdbcEnvironmentProperties; import org.springframework.cloud.config.server.environment.JdbcEnvironmentRepository; import org.springframework.cloud.config.server.environment.JdbcEnvironmentRepositoryFactory; +import org.springframework.cloud.config.server.environment.MongoDbEnvironmentProperties; +import org.springframework.cloud.config.server.environment.MongoDbEnvironmentRepository; +import org.springframework.cloud.config.server.environment.MongoDbEnvironmentRepositoryFactory; import org.springframework.cloud.config.server.environment.MultipleJGitEnvironmentProperties; import org.springframework.cloud.config.server.environment.MultipleJGitEnvironmentRepository; import org.springframework.cloud.config.server.environment.MultipleJGitEnvironmentRepositoryFactory; @@ -99,6 +102,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.credhub.core.CredHubOperations; +import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.vault.core.VaultTemplate; @@ -112,19 +116,21 @@ import org.springframework.vault.core.VaultTemplate; * @author Scott Frederick * @author Tejas Pandilwar * @author Iulian Antohe + * @author Alexandros Pappas */ @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties({ SvnKitEnvironmentProperties.class, CredhubEnvironmentProperties.class, JdbcEnvironmentProperties.class, NativeEnvironmentProperties.class, VaultEnvironmentProperties.class, RedisEnvironmentProperties.class, AwsS3EnvironmentProperties.class, AwsSecretsManagerEnvironmentProperties.class, AwsParameterStoreEnvironmentProperties.class, - GoogleSecretManagerEnvironmentProperties.class }) + GoogleSecretManagerEnvironmentProperties.class, MongoDbEnvironmentProperties.class }) @Import({ CompositeRepositoryConfiguration.class, JdbcRepositoryConfiguration.class, VaultConfiguration.class, VaultRepositoryConfiguration.class, SpringVaultRepositoryConfiguration.class, CredhubConfiguration.class, CredhubRepositoryConfiguration.class, SvnRepositoryConfiguration.class, NativeRepositoryConfiguration.class, GitRepositoryConfiguration.class, RedisRepositoryConfiguration.class, GoogleCloudSourceConfiguration.class, AwsS3RepositoryConfiguration.class, AwsSecretsManagerRepositoryConfiguration.class, AwsParameterStoreRepositoryConfiguration.class, GoogleSecretManagerRepositoryConfiguration.class, + MongoRepositoryConfiguration.class, // DefaultRepositoryConfiguration must be last DefaultRepositoryConfiguration.class }) public class EnvironmentRepositoryConfiguration { @@ -378,6 +384,19 @@ public class EnvironmentRepositoryConfiguration { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(MongoTemplate.class) + @ConditionalOnProperty(value = "spring.cloud.config.server.mongodb.enabled", matchIfMissing = true) + static class MongoDbFactoryConfig { + + @Bean + @ConditionalOnBean(MongoTemplate.class) + public MongoDbEnvironmentRepositoryFactory mongoDbEnvironmentRepositoryFactory(MongoTemplate mongoTemplate) { + return new MongoDbEnvironmentRepositoryFactory(mongoTemplate); + } + + } + } @Configuration(proxyBeanMethods = false) @@ -579,3 +598,18 @@ class GoogleSecretManagerRepositoryConfiguration { } } + +@Configuration(proxyBeanMethods = false) +@Profile("mongodb") +@ConditionalOnClass(MongoTemplate.class) +@ConditionalOnProperty(value = "spring.cloud.config.server.mongodb.enabled", matchIfMissing = true) +class MongoRepositoryConfiguration { + + @Bean + @ConditionalOnBean(MongoTemplate.class) + public MongoDbEnvironmentRepository mongoDbEnvironmentRepository(MongoDbEnvironmentRepositoryFactory factory, + MongoDbEnvironmentProperties environmentProperties) { + return factory.build(environmentProperties); + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/MongoDbEnvironmentProperties.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/MongoDbEnvironmentProperties.java new file mode 100644 index 00000000..979641d0 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/MongoDbEnvironmentProperties.java @@ -0,0 +1,97 @@ +/* + * Copyright 2018-2019 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.config.server.environment; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.config.server.support.EnvironmentRepositoryProperties; +import org.springframework.core.Ordered; + +/** + * Properties related to MongoDB environment repository. + * + * @author Alexandros Pappas + */ +@ConfigurationProperties("spring.cloud.config.server.mongodb") +public class MongoDbEnvironmentProperties implements EnvironmentRepositoryProperties { + + /** + * Flag to indicate that MongoDB environment repository configuration is enabled. + */ + private boolean enabled = true; + + /** + * Order of the MongoDB environment repository. + */ + private int order = Ordered.LOWEST_PRECEDENCE - 10; + + /** + * Name of the MongoDB collection to query for configuration properties. + */ + private String collection = "properties"; + + /** + * Flag to determine how to handle query exceptions. + */ + private boolean failOnError = true; + + /** + * Default label to use if none is specified. + */ + private String defaultLabel = "master"; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public int getOrder() { + return order; + } + + @Override + public void setOrder(int order) { + this.order = order; + } + + public String getCollection() { + return collection; + } + + public void setCollection(String collection) { + this.collection = collection; + } + + public boolean isFailOnError() { + return failOnError; + } + + public void setFailOnError(boolean failOnError) { + this.failOnError = failOnError; + } + + public String getDefaultLabel() { + return defaultLabel; + } + + public void setDefaultLabel(String defaultLabel) { + this.defaultLabel = defaultLabel; + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/MongoDbEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/MongoDbEnvironmentRepository.java new file mode 100644 index 00000000..b3dde1f9 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/MongoDbEnvironmentRepository.java @@ -0,0 +1,127 @@ +/* + * Copyright 2018-2019 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.config.server.environment; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.mongodb.MongoException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.environment.PropertySource; +import org.springframework.core.Ordered; +import org.springframework.dao.DataAccessException; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.util.StringUtils; + +/** + * @author Alexandros Pappas + */ +public class MongoDbEnvironmentRepository implements EnvironmentRepository, Ordered { + + private static final Log logger = LogFactory.getLog(JdbcEnvironmentRepository.class); + + private final MongoTemplate mongoTemplate; + + private final MongoDbEnvironmentProperties properties; + + public MongoDbEnvironmentRepository(MongoTemplate mongoTemplate, MongoDbEnvironmentProperties properties) { + this.mongoTemplate = mongoTemplate; + this.properties = properties; + } + + @Override + public Environment findOne(String application, String profile, String label) { + label = StringUtils.hasText(label) ? label : this.properties.getDefaultLabel(); + profile = StringUtils.hasText(profile) ? profile : "default"; + + // Prepare the environment with applications and profiles + String[] profilesArray = StringUtils.commaDelimitedListToStringArray(profile); + Environment environment = new Environment(application, profilesArray, label, null, null); + + // Prepend "application," to config if not already present + String config = application.startsWith("application") ? application : "application," + application; + + List applications = Arrays.stream(StringUtils.commaDelimitedListToStringArray(config)).distinct() + .collect(Collectors.toList()); + List profiles = Arrays.stream(profilesArray).distinct().collect(Collectors.toList()); + + // Reverse for the intended processing order + Collections.reverse(applications); + Collections.reverse(profiles); + + // Add property sources for each combination of application and profile + for (String env : profiles) { + for (String app : applications) { + addPropertySource(environment, app, env, label); + } + } + // add properties without profile, equivalent to foo.yml, application.yml + for (String app : applications) { + addPropertySource(environment, app, null, label); + } + return environment; + } + + private void addPropertySource(Environment environment, String application, String profile, String label) { + try { + Criteria criteria = Criteria.where("application").is(application).and("label").is(label); + if (profile != null) { + criteria = criteria.and("profile").is(profile); + } + else { + // Handling properties without profile by explicitly looking for them + criteria = criteria.andOperator(Criteria.where("profile").is(null)); + } + + Query query = new Query(criteria); + List propertyMaps = this.mongoTemplate.find(query, Map.class, this.properties.getCollection()); + + for (Map propertyMap : propertyMaps) { + String propertySourceName = (profile != null) ? application + "-" + profile : application; + @SuppressWarnings("unchecked") + Map source = (Map) propertyMap.get("properties"); + if (source != null && !source.isEmpty()) { + environment.add(new PropertySource(propertySourceName, source)); + } + } + } + catch (DataAccessException | MongoException e) { + if (!this.properties.isFailOnError()) { + if (logger.isDebugEnabled()) { + logger.debug("Failed to retrieve configuration from MongoDB", e); + } + } + else { + throw e; + } + } + } + + @Override + public int getOrder() { + return this.properties.getOrder(); + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/MongoDbEnvironmentRepositoryFactory.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/MongoDbEnvironmentRepositoryFactory.java new file mode 100644 index 00000000..de136168 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/MongoDbEnvironmentRepositoryFactory.java @@ -0,0 +1,40 @@ +/* + * Copyright 2018-2019 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.config.server.environment; + +import org.springframework.data.mongodb.core.MongoTemplate; + +/** + * Factory for creating instances of MongoDbEnvironmentRepository. + * + * @author Alexandros Pappas + */ +public class MongoDbEnvironmentRepositoryFactory + implements EnvironmentRepositoryFactory { + + private final MongoTemplate mongoTemplate; + + public MongoDbEnvironmentRepositoryFactory(MongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + } + + @Override + public MongoDbEnvironmentRepository build(MongoDbEnvironmentProperties environmentProperties) { + return new MongoDbEnvironmentRepository(this.mongoTemplate, environmentProperties); + } + +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/MongoDbEnvironmentRepositoryConfigurationTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/MongoDbEnvironmentRepositoryConfigurationTests.java new file mode 100644 index 00000000..c3a84990 --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/MongoDbEnvironmentRepositoryConfigurationTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2018-2019 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.config.server.environment; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; +import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.cloud.config.server.test.ConfigServerTestUtils; +import org.springframework.cloud.config.server.test.TestConfigServerApplication; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests to verify MongoDbEnvironmentRepository configuration. + * + * @author Alexandros Pappas + */ +public class MongoDbEnvironmentRepositoryConfigurationTests { + + @Test + public void mongoDbEnvironmentRepositoryBeansConfiguredWhenDefault() { + new WebApplicationContextRunner().withUserConfiguration(TestConfigServerApplication.class) + .withPropertyValues("spring.profiles.active=test,mongodb", "spring.main.web-application-type=none") + .run(context -> { + assertThat(context).hasSingleBean(MongoDbEnvironmentRepositoryFactory.class); + assertThat(context).hasSingleBean(MongoDbEnvironmentRepository.class); + }); + } + + @Test + public void mongoDbEnvironmentRepositoryBeansConfiguredWhenEnabled() throws IOException { + getApplicationContextWithMongoDbEnabled(true, context -> { + assertThat(context).hasSingleBean(MongoDbEnvironmentRepositoryFactory.class); + assertThat(context).hasSingleBean(MongoDbEnvironmentRepository.class); + }); + } + + @Test + public void mongoDbEnvironmentRepositoryFactoryNotConfiguredWhenDisabled() throws IOException { + getApplicationContextWithMongoDbEnabled(false, + context -> assertThat(context).doesNotHaveBean(MongoDbEnvironmentRepositoryFactory.class)); + } + + @Test + public void mongoDbEnvironmentRepositoryNotConfiguredWhenDisabled() throws IOException { + getApplicationContextWithMongoDbEnabled(false, + context -> assertThat(context).doesNotHaveBean(MongoDbEnvironmentRepository.class)); + } + + private void getApplicationContextWithMongoDbEnabled(boolean mongoDbEnabled, + ContextConsumer consumer) throws IOException { + String uri = ConfigServerTestUtils.prepareLocalRepo(); + new WebApplicationContextRunner().withUserConfiguration(TestConfigServerApplication.class) + .withPropertyValues("spring.profiles.active=test,mongodb", "spring.main.web-application-type=none", + "spring.cloud.config.server.git.uri:" + uri, + "spring.cloud.config.server.mongodb.enabled:" + mongoDbEnabled) + .run(consumer); + } + +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/MongoDbEnvironmentRepositoryTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/MongoDbEnvironmentRepositoryTests.java new file mode 100644 index 00000000..7abb39cf --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/MongoDbEnvironmentRepositoryTests.java @@ -0,0 +1,182 @@ +/* + * Copyright 2018-2019 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.config.server.environment; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +import com.mongodb.MongoException; +import com.mongodb.MongoTimeoutException; +import org.bson.Document; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import wiremock.com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.cloud.config.environment.Environment; +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; + +/** + * @author Alexandros Pappas + */ +@SpringBootTest +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@ActiveProfiles("mongodb") +@Testcontainers +@Tag("DockerRequired") +public class MongoDbEnvironmentRepositoryTests { + + @Container + @ServiceConnection + static MongoDBContainer mongoContainer = new MongoDBContainer("mongo:5.0"); + + @Autowired + private MongoTemplate mongoTemplate; + + @BeforeEach + void setup() throws IOException { + mongoContainer.start(); + mongoTemplate.dropCollection("properties"); + InputStream inputStream = new ClassPathResource("/data-mongo.json").getInputStream(); + String json = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + List documents = Arrays.asList(new ObjectMapper().readValue(json, Document[].class)); + mongoTemplate.getCollection("properties").insertMany(documents); + } + + @Test + public void basicProperties() { + MongoDbEnvironmentProperties properties = new MongoDbEnvironmentProperties(); + Environment env = new MongoDbEnvironmentRepository(mongoTemplate, properties).findOne("foo", "bar", ""); + assertThat(env.getName()).isEqualTo("foo"); + assertThat(env.getProfiles()).isEqualTo(new String[] { "bar" }); + assertThat(env.getLabel()).isEqualTo("master"); + assertThat(env.getPropertySources()).isNotEmpty(); + assertThat(env.getPropertySources().get(0).getName()).isEqualTo("foo-bar"); + assertThat(env.getPropertySources().get(0).getSource().get("a_b_c")).isEqualTo("foo-bar"); + assertThat(env.getPropertySources().get(1).getName()).isEqualTo("application-bar"); + assertThat(env.getPropertySources().get(1).getSource().get("a_b_c")).isEqualTo("application-bar"); + assertThat(env.getPropertySources().get(2).getName()).isEqualTo("foo"); + assertThat(env.getPropertySources().get(2).getSource().get("a_b_c")).isEqualTo("foo-null"); + assertThat(env.getPropertySources().get(3).getName()).isEqualTo("application"); + assertThat(env.getPropertySources().get(3).getSource().get("a_b_c")).isEqualTo("application-null"); + } + + @Test + public void testDefaultProfile() { + MongoDbEnvironmentProperties properties = new MongoDbEnvironmentProperties(); + Environment env = new MongoDbEnvironmentRepository(mongoTemplate, properties).findOne("foo", "", ""); + assertThat(env.getName()).isEqualTo("foo"); + assertThat(env.getProfiles()).isEqualTo(new String[] { "default" }); + assertThat(env.getLabel()).isEqualTo("master"); + assertThat(env.getPropertySources()).isNotEmpty(); + assertThat(env.getPropertySources().get(0).getName()).isEqualTo("foo-default"); + assertThat(env.getPropertySources().get(0).getSource().get("a_b_c")).isEqualTo("foo-default"); + assertThat(env.getPropertySources().get(1).getName()).isEqualTo("application-default"); + assertThat(env.getPropertySources().get(1).getSource().get("a_b_c")).isEqualTo("application-default"); + assertThat(env.getPropertySources().get(2).getName()).isEqualTo("foo"); + assertThat(env.getPropertySources().get(2).getSource().get("a_b_c")).isEqualTo("foo-null"); + assertThat(env.getPropertySources().get(3).getName()).isEqualTo("application"); + assertThat(env.getPropertySources().get(3).getSource().get("a_b_c")).isEqualTo("application-null"); + } + + @Test + public void testProfileNotExist() { + MongoDbEnvironmentProperties properties = new MongoDbEnvironmentProperties(); + Environment env = new MongoDbEnvironmentRepository(mongoTemplate, properties).findOne("foo", "not_exist", ""); + assertThat(env.getName()).isEqualTo("foo"); + assertThat(env.getProfiles()).isEqualTo(new String[] { "not_exist" }); + assertThat(env.getLabel()).isEqualTo("master"); + assertThat(env.getPropertySources()).isNotEmpty(); + assertThat(env.getPropertySources().get(0).getName()).isEqualTo("foo"); + assertThat(env.getPropertySources().get(0).getSource().get("a_b_c")).isEqualTo("foo-null"); + assertThat(env.getPropertySources().get(1).getName()).isEqualTo("application"); + assertThat(env.getPropertySources().get(1).getSource().get("a_b_c")).isEqualTo("application-null"); + } + + @Test + public void testApplicationNotExist() { + MongoDbEnvironmentProperties properties = new MongoDbEnvironmentProperties(); + Environment env = new MongoDbEnvironmentRepository(mongoTemplate, properties).findOne("not_exist", "bar", ""); + assertThat(env.getName()).isEqualTo("not_exist"); + assertThat(env.getProfiles()).isEqualTo(new String[] { "bar" }); + assertThat(env.getLabel()).isEqualTo("master"); + assertThat(env.getPropertySources()).isNotEmpty(); + assertThat(env.getPropertySources().get(0).getName()).isEqualTo("application-bar"); + assertThat(env.getPropertySources().get(0).getSource().get("a_b_c")).isEqualTo("application-bar"); + assertThat(env.getPropertySources().get(1).getName()).isEqualTo("application"); + assertThat(env.getPropertySources().get(1).getSource().get("a_b_c")).isEqualTo("application-null"); + } + + @Test + public void testApplicationProfileBothNotExist() { + MongoDbEnvironmentProperties properties = new MongoDbEnvironmentProperties(); + Environment env = new MongoDbEnvironmentRepository(mongoTemplate, properties).findOne("not_exist", "not_exist", + ""); + assertThat(env.getName()).isEqualTo("not_exist"); + assertThat(env.getProfiles()).isEqualTo(new String[] { "not_exist" }); + assertThat(env.getLabel()).isEqualTo("master"); + assertThat(env.getPropertySources()).isNotEmpty(); + assertThat(env.getPropertySources().get(0).getName()).isEqualTo("application"); + assertThat(env.getPropertySources().get(0).getSource().get("a_b_c")).isEqualTo("application-null"); + } + + @Test + public void testCustomLabel() { + MongoDbEnvironmentProperties properties = new MongoDbEnvironmentProperties(); + properties.setDefaultLabel("main"); + Environment env = new MongoDbEnvironmentRepository(mongoTemplate, properties).findOne("foo", "bar", ""); + assertThat(env.getName()).isEqualTo("foo"); + assertThat(env.getProfiles()).isEqualTo(new String[] { "bar" }); + assertThat(env.getLabel()).isEqualTo("main"); + assertThat(env.getPropertySources()).isNotEmpty(); + assertThat(env.getPropertySources().get(0).getName()).isEqualTo("foo-bar"); + assertThat(env.getPropertySources().get(0).getSource().get("a_b_c")).isEqualTo("foo-bar"); + assertThat(env.getPropertySources().get(1).getName()).isEqualTo("application-bar"); + assertThat(env.getPropertySources().get(1).getSource().get("a_b_c")).isEqualTo("application-bar"); + } + + @Test + public void testFailOnError() { + MongoTemplate failingMongoTemplate = Mockito.spy(mongoTemplate); + Mockito.doThrow(new MongoTimeoutException("Timed out after 30000 ms while waiting for a server.")) + .when(failingMongoTemplate).find(any(Query.class), any(), anyString()); + MongoDbEnvironmentRepository repository = new MongoDbEnvironmentRepository(failingMongoTemplate, + new MongoDbEnvironmentProperties()); + assertThatThrownBy(() -> repository.findOne("foo", "bar", "")).isInstanceOf(MongoException.class) + .hasMessageContaining("Timed out after 30000 ms while waiting for a server."); + } + +} diff --git a/spring-cloud-config-server/src/test/resources/data-mongo.json b/spring-cloud-config-server/src/test/resources/data-mongo.json new file mode 100644 index 00000000..96c3a770 --- /dev/null +++ b/spring-cloud-config-server/src/test/resources/data-mongo.json @@ -0,0 +1,66 @@ +[ + { + "application": "foo", + "profile": "bar", + "label": "master", + "properties": { + "a_b_c": "foo-bar" + } + }, + { + "application": "foo", + "profile": "default", + "label": "master", + "properties": { + "a_b_c": "foo-default" + } + }, + { + "application": "foo", + "profile": null, + "label": "master", + "properties": { + "a_b_c": "foo-null" + } + }, + { + "application": "application", + "profile": "bar", + "label": "master", + "properties": { + "a_b_c": "application-bar" + } + }, + { + "application": "application", + "profile": "default", + "label": "master", + "properties": { + "a_b_c": "application-default" + } + }, + { + "application": "application", + "profile": null, + "label": "master", + "properties": { + "a_b_c": "application-null" + } + }, + { + "application": "foo", + "profile": "bar", + "label": "main", + "properties": { + "a_b_c": "foo-bar" + } + }, + { + "application": "application", + "profile": "bar", + "label": "main", + "properties": { + "a_b_c": "application-bar" + } + } +]