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