diff --git a/docs/src/main/asciidoc/spring-cloud-config.adoc b/docs/src/main/asciidoc/spring-cloud-config.adoc
index eef8807b..9c33e3bc 100644
--- a/docs/src/main/asciidoc/spring-cloud-config.adoc
+++ b/docs/src/main/asciidoc/spring-cloud-config.adoc
@@ -337,7 +337,7 @@ To correct the above error the RSA key must be converted to PEM format. An examp
Spring Cloud Config Server also supports https://docs.aws.amazon.com/codecommit/latest/userguide/welcome.html[AWS CodeCommit] authentication.
AWS CodeCommit uses an authentication helper when using Git from the command line.
This helper is not used with the JGit library, so a JGit CredentialProvider for AWS CodeCommit is created if the Git URI matches the AWS CodeCommit pattern.
-AWS CodeCommit URIs follow this pattern:
+AWS CodeCommit URIs follow this pattern:
```bash
https//git-codecommit.${AWS_REGION}.amazonaws.com/v1/repos/${repo}.
@@ -834,6 +834,18 @@ secret value =
----
+===== AWS Parameter Store
+
+When using AWS Parameter Store as a backend, you can share configuration with all applications by placing properties within the `/application` hierarchy.
+
+For example, if you add parameters with the following names, all applications using the config server will have the properties `foo.bar` and `fred.baz` available to them:
+
+[source]
+----
+/config/application/foo.bar
+/config/application-default/fred.baz
+----
+
==== JDBC Backend
Spring Cloud Config Server supports JDBC (relational database) as a backend for configuration properties.
@@ -932,6 +944,99 @@ Configuration files are stored in your bucket as `{application}-{profile}.proper
NOTE: When no profile is specified `default` will be used.
+==== AWS Parameter Store Backend
+
+Spring Cloud Config Server supports AWS Parameter Store as a backend for configuration properties. You can enable this feature by adding a dependency to the link:https://github.com/aws/aws-sdk-java/tree/master/aws-java-sdk-ssm[AWS Java SDK for SSM].
+
+[source,xml,indent=0]
+.pom.xml
+----
+
+ com.amazonaws
+ aws-java-sdk-ssm
+
+----
+
+The following configuration uses the AWS SSM client to access parameters.
+
+[source,yaml]
+----
+spring:
+ profiles:
+ active: awsparamstore
+ cloud:
+ config:
+ server:
+ awsparamstore:
+ region: eu-west-2
+ endpoint: https://ssm.eu-west-2.amazonaws.com
+ origin: aws:parameter:
+ prefix: /config/service
+ profileSeparator: _
+ recursive: true
+ decryptValues: true
+ maxResults: 5
+----
+
+The following table describes the AWS Parameter Store configuration properties.
+
+.AWS Parameter Store Configuration Properties
+|===
+|Property Name |Required |Default Value |Remarks
+
+|*region*
+|no
+|
+|The region to be used by the AWS Parameter Store client. If it's not explicitly set, the SDK tries to determine the region to use by using the link:https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/java-dg-region-selection.html#default-region-provider-chain[Default Region Provider Chain].
+
+|*endpoint*
+|no
+|
+|The URL of the entry point for the AWS SSM client. This can be used to specify an alternate endpoint for the API requests.
+
+|*origin*
+|no
+|`aws:ssm:parameter:`
+|The prefix that is added to the property source's name to show their provenance.
+
+|*prefix*
+|no
+|`/config`
+|Prefix indicating L1 level in the parameter hierarchy for every property loaded from the AWS Parameter Store.
+
+|*profileSeparator*
+|no
+|`-`
+|String that separates an appended profile from the context name.
+
+|*recursive*
+|no
+|`true`
+|Flag to indicate the retrieval of all AWS parameters within a hierarchy.
+
+|*decryptValues*
+|no
+|`true`
+|Flag to indicate the retrieval of all AWS parameters with their value decrypted.
+
+|*maxResults*
+|no
+|`10`
+|The maximum number of items to return for an AWS Parameter Store API call.
+
+|===
+
+AWS Parameter Store API credentials are determined using the link:https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default[Default Credential Provider Chain].
+Versioned parameters are already supported with the default behaviour of returning the latest version.
+
+[NOTE]
+====
+- When no application is specified `application` is the default, and when no profile is specified `default` is used.
+- Valid values for `awsparamstore.prefix` must start with a forward slash followed by one or more valid path segments or be empty.
+- Valid values for `awsparamstore.profileSeparator` can only contain dots, dashes and underscores.
+- Valid values for `awsparamstore.maxResults` must be within the *[1, 10]* range.
+====
+
==== AWS Secrets Manager Backend
Spring Cloud Config Server supports link:https://aws.amazon.com/secrets-manager/[AWS Secrets Manager] as a backend for configuration properties.
diff --git a/pom.xml b/pom.xml
index af56ee71..a5ca4c64 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,7 +28,7 @@
config
3.1.0-SNAPSHOT
- 1.11.903
+ 1.11.911
v1-rev20201112-1.30.10
true
true
@@ -83,6 +83,11 @@
aws-java-sdk-secretsmanager
${aws-java-sdk.version}
+
+ com.amazonaws
+ aws-java-sdk-ssm
+ ${aws-java-sdk.version}
+
com.google.apis
google-api-services-iam
diff --git a/spring-cloud-config-server/pom.xml b/spring-cloud-config-server/pom.xml
index dcd9d737..2117add4 100644
--- a/spring-cloud-config-server/pom.xml
+++ b/spring-cloud-config-server/pom.xml
@@ -104,6 +104,11 @@
aws-java-sdk-secretsmanager
true
+
+ com.amazonaws
+ aws-java-sdk-ssm
+ true
+
org.springframework.boot
spring-boot-autoconfigure-processor
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 2ad72105..a1692f5f 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
@@ -23,6 +23,7 @@ import javax.servlet.http.HttpServletRequest;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.secretsmanager.AWSSecretsManager;
+import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagement;
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
import org.apache.http.client.HttpClient;
import org.eclipse.jgit.api.TransportConfigCallback;
@@ -40,6 +41,9 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.cloud.config.server.composite.CompositeEnvironmentBeanFactoryPostProcessor;
import org.springframework.cloud.config.server.composite.ConditionalOnMissingSearchPathLocator;
import org.springframework.cloud.config.server.composite.ConditionalOnSearchPathLocator;
+import org.springframework.cloud.config.server.environment.AwsParameterStoreEnvironmentProperties;
+import org.springframework.cloud.config.server.environment.AwsParameterStoreEnvironmentRepository;
+import org.springframework.cloud.config.server.environment.AwsParameterStoreEnvironmentRepositoryFactory;
import org.springframework.cloud.config.server.environment.AwsS3EnvironmentProperties;
import org.springframework.cloud.config.server.environment.AwsS3EnvironmentRepository;
import org.springframework.cloud.config.server.environment.AwsS3EnvironmentRepositoryFactory;
@@ -106,17 +110,19 @@ import org.springframework.vault.core.VaultTemplate;
* @author Alberto C. RĂos
* @author Scott Frederick
* @author Tejas Pandilwar
+ * @author Iulian Antohe
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({ SvnKitEnvironmentProperties.class, CredhubEnvironmentProperties.class,
JdbcEnvironmentProperties.class, NativeEnvironmentProperties.class, VaultEnvironmentProperties.class,
RedisEnvironmentProperties.class, AwsS3EnvironmentProperties.class,
- AwsSecretsManagerEnvironmentProperties.class, GoogleSecretManagerEnvironmentProperties.class })
+ AwsSecretsManagerEnvironmentProperties.class, AwsParameterStoreEnvironmentProperties.class, GoogleSecretManagerEnvironmentProperties.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, DefaultRepositoryConfiguration.class })
public class EnvironmentRepositoryConfiguration {
@@ -225,6 +231,18 @@ public class EnvironmentRepositoryConfiguration {
}
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass(AWSSimpleSystemsManagement.class)
+ static class AwsParameterStoreFactoryConfig {
+
+ @Bean
+ public AwsParameterStoreEnvironmentRepositoryFactory awsParameterStoreEnvironmentRepositoryFactory(
+ ConfigServerProperties server) {
+ return new AwsParameterStoreEnvironmentRepositoryFactory(server);
+ }
+
+ }
+
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(SVNException.class)
static class SvnFactoryConfig {
@@ -385,6 +403,20 @@ class AwsS3RepositoryConfiguration {
}
+@Configuration(proxyBeanMethods = false)
+@Profile("awsparamstore")
+class AwsParameterStoreRepositoryConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean(AwsParameterStoreEnvironmentRepository.class)
+ public AwsParameterStoreEnvironmentRepository awsParameterStoreEnvironmentRepository(
+ AwsParameterStoreEnvironmentRepositoryFactory factory,
+ AwsParameterStoreEnvironmentProperties environmentProperties) {
+ return factory.build(environmentProperties);
+ }
+
+}
+
@Configuration(proxyBeanMethods = false)
@Profile("awssecretsmanager")
class AwsSecretsManagerRepositoryConfiguration {
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsParameterStoreEnvironmentProperties.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsParameterStoreEnvironmentProperties.java
new file mode 100644
index 00000000..c2768513
--- /dev/null
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsParameterStoreEnvironmentProperties.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2018-2020 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 javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.config.server.support.EnvironmentRepositoryProperties;
+import org.springframework.core.Ordered;
+import org.springframework.validation.annotation.Validated;
+
+/**
+ * @author Iulian Antohe
+ */
+@Validated
+@ConfigurationProperties("spring.cloud.config.server.awsparamstore")
+public class AwsParameterStoreEnvironmentProperties
+ implements EnvironmentRepositoryProperties {
+
+ static final String DEFAULT_PATH_SEPARATOR = "/";
+
+ private static final String DEFAULT_ORIGIN = "aws:ssm:parameter:";
+
+ private static final String DEFAULT_PREFIX = DEFAULT_PATH_SEPARATOR + "config";
+
+ private static final String DEFAULT_PROFILE_SEPARATOR = "-";
+
+ /**
+ * The order of the environment repository.
+ */
+ private int order = Ordered.LOWEST_PRECEDENCE;
+
+ /**
+ * The region to be used by the AWS Parameter Store client.
+ */
+ private String region;
+
+ /**
+ * The service endpoint to be used by the AWS Parameter Store client.
+ */
+ private String endpoint;
+
+ /**
+ * Prefix indicating the property's origin. Defaults to "aws:ssm:parameter:".
+ */
+ @NotNull
+ private String origin = DEFAULT_ORIGIN;
+
+ /**
+ * Prefix indicating first level for every property loaded from the AWS Parameter
+ * Store. Value must start with a forward slash followed by one or more valid path
+ * segments or be empty. Defaults to "/config".
+ */
+ @NotNull
+ @Pattern(regexp = "(/[a-zA-Z0-9.\\-_]+)*")
+ private String prefix = DEFAULT_PREFIX;
+
+ /**
+ * String that separates an appended profile from the context name. Note that an AWS
+ * parameter name can only contain dots, dashes and underscores next to alphanumeric
+ * characters. Defaults to "-".
+ */
+ @NotBlank
+ @Pattern(regexp = "[a-zA-Z0-9.\\-_/]+")
+ private String profileSeparator = DEFAULT_PROFILE_SEPARATOR;
+
+ /**
+ * Flag to indicate the retrieval of all AWS parameters within a hierarchy. Defaults
+ * to "true".
+ */
+ private boolean recursive = true;
+
+ /**
+ * Flag to indicate the retrieval of all AWS parameters in a hierarchy with their
+ * value decrypted. Defaults to "true".
+ */
+ private boolean decryptValues = true;
+
+ /**
+ * The maximum number of items to return for an AWS Parameter Store API call. Defaults
+ * to "10".
+ */
+ @Min(1)
+ @Max(10)
+ private int maxResults = 10;
+
+ public int getOrder() {
+ return order;
+ }
+
+ @Override
+ public void setOrder(int order) {
+ this.order = order;
+ }
+
+ public String getRegion() {
+ return region;
+ }
+
+ public void setRegion(String region) {
+ this.region = region;
+ }
+
+ public String getEndpoint() {
+ return endpoint;
+ }
+
+ public void setEndpoint(String endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ public String getOrigin() {
+ return origin;
+ }
+
+ public void setOrigin(String origin) {
+ this.origin = origin;
+ }
+
+ public String getPrefix() {
+ return prefix;
+ }
+
+ public void setPrefix(String prefix) {
+ this.prefix = prefix;
+ }
+
+ public String getProfileSeparator() {
+ return profileSeparator;
+ }
+
+ public void setProfileSeparator(String profileSeparator) {
+ this.profileSeparator = profileSeparator;
+ }
+
+ public boolean isRecursive() {
+ return recursive;
+ }
+
+ public void setRecursive(boolean recursive) {
+ this.recursive = recursive;
+ }
+
+ public boolean isDecryptValues() {
+ return decryptValues;
+ }
+
+ public void setDecryptValues(boolean decryptValues) {
+ this.decryptValues = decryptValues;
+ }
+
+ public int getMaxResults() {
+ return maxResults;
+ }
+
+ public void setMaxResults(int maxResults) {
+ this.maxResults = maxResults;
+ }
+
+}
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsParameterStoreEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsParameterStoreEnvironmentRepository.java
new file mode 100644
index 00000000..c454291f
--- /dev/null
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsParameterStoreEnvironmentRepository.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2013-2020 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.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagement;
+import com.amazonaws.services.simplesystemsmanagement.model.GetParametersByPathRequest;
+import com.amazonaws.services.simplesystemsmanagement.model.GetParametersByPathResult;
+import com.amazonaws.services.simplesystemsmanagement.model.Parameter;
+
+import org.springframework.cloud.config.environment.Environment;
+import org.springframework.cloud.config.environment.PropertySource;
+import org.springframework.cloud.config.server.config.ConfigServerProperties;
+import org.springframework.util.StringUtils;
+
+import static org.springframework.cloud.config.server.environment.AwsParameterStoreEnvironmentProperties.DEFAULT_PATH_SEPARATOR;
+
+/**
+ * @author Iulian Antohe
+ */
+public class AwsParameterStoreEnvironmentRepository implements EnvironmentRepository {
+
+ private final AWSSimpleSystemsManagement awsSsmClient;
+
+ private final ConfigServerProperties configServerProperties;
+
+ private final AwsParameterStoreEnvironmentProperties environmentProperties;
+
+ public AwsParameterStoreEnvironmentRepository(AWSSimpleSystemsManagement awsSsmClient,
+ ConfigServerProperties configServerProperties,
+ AwsParameterStoreEnvironmentProperties environmentProperties) {
+ this.awsSsmClient = awsSsmClient;
+ this.configServerProperties = configServerProperties;
+ this.environmentProperties = environmentProperties;
+ }
+
+ @Override
+ public Environment findOne(String application, String profile, String label) {
+ if (!StringUtils.hasLength(application)) {
+ application = configServerProperties.getDefaultApplicationName();
+ }
+
+ if (!StringUtils.hasLength(profile)) {
+ profile = configServerProperties.getDefaultProfile();
+ }
+
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
+
+ Environment result = new Environment(application, profiles, label, null, null);
+
+ Set paths = buildParameterPaths(application, profiles);
+ List propertySources = getPropertySources(paths);
+
+ result.addAll(propertySources);
+
+ return result;
+ }
+
+ private Set buildParameterPaths(String application, String[] profiles) {
+ Set result = new LinkedHashSet<>();
+
+ String prefix = environmentProperties.getPrefix();
+ String defaultApplication = configServerProperties.getDefaultApplicationName();
+ String profileSeparator = environmentProperties.getProfileSeparator();
+ String defaultProfile = configServerProperties.getDefaultProfile();
+
+ List orderedProfiles = Stream
+ .concat(Arrays.stream(profiles).filter(p -> !p.equals(defaultProfile)),
+ Arrays.stream(new String[] { defaultProfile }))
+ .collect(Collectors.toList());
+
+ if (application.equals(defaultApplication)) {
+ for (String profile : orderedProfiles) {
+ result.add(prefix + DEFAULT_PATH_SEPARATOR + defaultApplication
+ + profileSeparator + profile + DEFAULT_PATH_SEPARATOR);
+ }
+ }
+ else {
+ for (String profile : orderedProfiles) {
+ result.add(prefix + DEFAULT_PATH_SEPARATOR + application
+ + profileSeparator + profile + DEFAULT_PATH_SEPARATOR);
+
+ result.add(prefix + DEFAULT_PATH_SEPARATOR + defaultApplication
+ + profileSeparator + profile + DEFAULT_PATH_SEPARATOR);
+ }
+
+ result.add(prefix + DEFAULT_PATH_SEPARATOR + application
+ + DEFAULT_PATH_SEPARATOR);
+ }
+
+ result.add(prefix + DEFAULT_PATH_SEPARATOR + defaultApplication
+ + DEFAULT_PATH_SEPARATOR);
+
+ return result;
+ }
+
+ private List getPropertySources(Set parameterPaths) {
+ List result = new ArrayList<>();
+
+ for (String path : parameterPaths) {
+ String name = environmentProperties.getOrigin() + path;
+ Map source = getPropertiesByParameterPath(path);
+
+ if (!source.isEmpty()) {
+ result.add(new PropertySource(name, source));
+ }
+ }
+
+ Map overrides = configServerProperties.getOverrides();
+
+ if (!overrides.isEmpty()) {
+ result.add(0, new PropertySource("overrides", overrides));
+ }
+
+ return result;
+ }
+
+ private Map getPropertiesByParameterPath(String path) {
+ Map result = new HashMap<>();
+
+ GetParametersByPathRequest request = new GetParametersByPathRequest()
+ .withPath(path).withRecursive(environmentProperties.isRecursive())
+ .withWithDecryption(environmentProperties.isDecryptValues())
+ .withMaxResults(environmentProperties.getMaxResults());
+
+ GetParametersByPathResult response = awsSsmClient.getParametersByPath(request);
+
+ if (response != null) {
+ addParametersToProperties(path, response.getParameters(), result);
+
+ while (StringUtils.hasLength(response.getNextToken())) {
+ response = awsSsmClient.getParametersByPath(request.withNextToken(response.getNextToken()));
+
+ addParametersToProperties(path, response.getParameters(), result);
+ }
+ }
+
+ return result;
+ }
+
+ private void addParametersToProperties(String path, List parameters,
+ Map properties) {
+ for (Parameter parameter : parameters) {
+ String name = StringUtils.delete(parameter.getName(), path)
+ .replace(DEFAULT_PATH_SEPARATOR, ".");
+
+ properties.put(name, parameter.getValue());
+ }
+ }
+
+}
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsParameterStoreEnvironmentRepositoryFactory.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsParameterStoreEnvironmentRepositoryFactory.java
new file mode 100644
index 00000000..9ee52d91
--- /dev/null
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsParameterStoreEnvironmentRepositoryFactory.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2018-2020 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 com.amazonaws.client.builder.AwsClientBuilder;
+import com.amazonaws.regions.Regions;
+import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagement;
+import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagementClientBuilder;
+
+import org.springframework.cloud.config.server.config.ConfigServerProperties;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author Iulian Antohe
+ */
+public class AwsParameterStoreEnvironmentRepositoryFactory implements
+ EnvironmentRepositoryFactory {
+
+ private final ConfigServerProperties configServerProperties;
+
+ public AwsParameterStoreEnvironmentRepositoryFactory(
+ ConfigServerProperties configServerProperties) {
+ this.configServerProperties = configServerProperties;
+ }
+
+ @Override
+ public AwsParameterStoreEnvironmentRepository build(
+ AwsParameterStoreEnvironmentProperties environmentProperties) {
+ AWSSimpleSystemsManagementClientBuilder clientBuilder = AWSSimpleSystemsManagementClientBuilder
+ .standard();
+
+ String region = environmentProperties.getRegion();
+
+ if (StringUtils.hasLength(region)) {
+ Regions awsRegion = Regions.fromName(region);
+
+ clientBuilder.withRegion(awsRegion);
+
+ String endpoint = environmentProperties.getEndpoint();
+
+ if (StringUtils.hasLength(endpoint)) {
+ AwsClientBuilder.EndpointConfiguration endpointConfiguration = new AwsClientBuilder.EndpointConfiguration(
+ endpoint, awsRegion.getName());
+
+ clientBuilder.withEndpointConfiguration(endpointConfiguration);
+ }
+ }
+
+ AWSSimpleSystemsManagement client = clientBuilder.build();
+
+ return new AwsParameterStoreEnvironmentRepository(client, configServerProperties,
+ environmentProperties);
+ }
+
+}
diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/AdhocTestSuite.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/AdhocTestSuite.java
index 0c434be3..025e1aeb 100644
--- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/AdhocTestSuite.java
+++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/AdhocTestSuite.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2019 the original author or authors.
+ * Copyright 2013-2020 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.
@@ -32,6 +32,7 @@ import org.springframework.cloud.config.server.encryption.EncryptionControllerTe
import org.springframework.cloud.config.server.encryption.EncryptionIntegrationTests;
import org.springframework.cloud.config.server.encryption.EnvironmentPrefixHelperTests;
import org.springframework.cloud.config.server.encryption.KeyStoreTextEncryptorLocatorTests;
+import org.springframework.cloud.config.server.environment.AwsParameterStoreEnvironmentRepositoryTests;
import org.springframework.cloud.config.server.environment.AwsS3EnvironmentRepositoryTests;
import org.springframework.cloud.config.server.environment.CompositeEnvironmentRepositoryTests;
import org.springframework.cloud.config.server.environment.EnvironmentControllerIntegrationTests;
@@ -86,7 +87,7 @@ import org.springframework.cloud.config.server.ssh.SshUriPropertyProcessorTest;
SshPropertyValidatorTest.class, CompositeIntegrationTests.class, SubversionConfigServerIntegrationTests.class,
ConfigServerHealthIndicatorTests.class, CustomCompositeEnvironmentRepositoryTests.class,
CustomEnvironmentRepositoryTests.class, BootstrapConfigServerIntegrationTests.class,
- AwsS3EnvironmentRepositoryTests.class })
+ AwsS3EnvironmentRepositoryTests.class, AwsParameterStoreEnvironmentRepositoryTests.class })
@Ignore
public class AdhocTestSuite {
diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/AwsParameterStoreEnvironmentRepositoryTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/AwsParameterStoreEnvironmentRepositoryTests.java
new file mode 100644
index 00000000..7c0efed7
--- /dev/null
+++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/AwsParameterStoreEnvironmentRepositoryTests.java
@@ -0,0 +1,856 @@
+/*
+ * Copyright 2013-2020 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.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+
+import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagement;
+import com.amazonaws.services.simplesystemsmanagement.model.GetParametersByPathRequest;
+import com.amazonaws.services.simplesystemsmanagement.model.GetParametersByPathResult;
+import com.amazonaws.services.simplesystemsmanagement.model.Parameter;
+import com.amazonaws.services.simplesystemsmanagement.model.ParameterType;
+import org.apache.commons.lang3.RandomUtils;
+import org.junit.Test;
+
+import org.springframework.cloud.config.environment.Environment;
+import org.springframework.cloud.config.environment.PropertySource;
+import org.springframework.cloud.config.server.config.ConfigServerProperties;
+import org.springframework.util.StringUtils;
+
+import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.springframework.cloud.config.server.environment.AwsParameterStoreEnvironmentProperties.DEFAULT_PATH_SEPARATOR;
+
+/**
+ * @author Iulian Antohe
+ */
+public class AwsParameterStoreEnvironmentRepositoryTests {
+
+ private static final Map SHARED_PROPERTIES = new HashMap() {
+ {
+ put("logging.level.root", "warn");
+ put("spring.cache.redis.time-to-live", "0");
+ }
+ };
+
+ private static final Map SHARED_DEFAULT_PROPERTIES = new HashMap() {
+ {
+ put("logging.level.root", "error");
+ put("spring.cache.redis.time-to-live", "1000");
+ }
+ };
+
+ private static final Map SHARED_PRODUCTION_PROPERTIES = new HashMap() {
+ {
+ put("logging.level.root", "fatal");
+ put("spring.cache.redis.time-to-live", "5000");
+ }
+ };
+
+ private static final Map APPLICATION_SPECIFIC_PROPERTIES = new HashMap() {
+ {
+ put("logging.level.com.example.service", "trace");
+ put("spring.cache.redis.time-to-live", "30000");
+ }
+ };
+
+ private static final Map APPLICATION_SPECIFIC_DEFAULT_PROPERTIES = new HashMap() {
+ {
+ put("logging.level.com.example.service", "debug");
+ put("spring.cache.redis.time-to-live", "60000");
+ }
+ };
+
+ private static final Map APPLICATION_SPECIFIC_PRODUCTION_PROPERTIES = new HashMap() {
+ {
+ put("logging.level.com.example.service", "info");
+ put("spring.cache.redis.time-to-live", "300000");
+ }
+ };
+
+ private final AWSSimpleSystemsManagement awsSsmClientMock = mock(
+ AWSSimpleSystemsManagement.class, "aws-ssm-client-mock");
+
+ private final ConfigServerProperties configServerProperties = new ConfigServerProperties();
+
+ private final AwsParameterStoreEnvironmentProperties environmentProperties = new AwsParameterStoreEnvironmentProperties();
+
+ private final AwsParameterStoreEnvironmentRepository repository = new AwsParameterStoreEnvironmentRepository(
+ awsSsmClientMock, configServerProperties, environmentProperties);
+
+ @Test
+ @SuppressWarnings("ConstantConditions")
+ public void testFindOneWithNullApplicationAndNullProfile() {
+ // Arrange
+ String application = null;
+ String profile = null;
+ String defaultApp = configServerProperties.getDefaultApplicationName();
+ String defaultProfile = configServerProperties.getDefaultProfile();
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(defaultProfile);
+
+ String sharedDefaultParamsPsName = "aws:ssm:parameter:/config/application-default/";
+ PropertySource sharedDefaultParamsPs = new PropertySource(
+ sharedDefaultParamsPsName, SHARED_DEFAULT_PROPERTIES);
+
+ String sharedParamsPsName = "aws:ssm:parameter:/config/application/";
+ PropertySource sharedParamsPs = new PropertySource(sharedParamsPsName,
+ SHARED_PROPERTIES);
+
+ Environment expected = new Environment(defaultApp, profiles, null, null, null);
+ expected.addAll(Arrays.asList(sharedDefaultParamsPs, sharedParamsPs));
+
+ setupAwsSsmClientMocks(expected);
+
+ // Act
+ Environment result = repository.findOne(application, profile, null);
+
+ // Assert
+ assertThat(result).usingRecursiveComparison().withStrictTypeChecking()
+ .isEqualTo(expected);
+ }
+
+ @Test
+ @SuppressWarnings("ConstantConditions")
+ public void testFindOneWithNullApplicationAndDefaultProfile() {
+ // Arrange
+ String application = null;
+ String profile = configServerProperties.getDefaultProfile();
+ String defaultApp = configServerProperties.getDefaultApplicationName();
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
+
+ String sharedDefaultParamsPsName = "aws:ssm:parameter:/config/application-default/";
+ PropertySource sharedDefaultParamsPs = new PropertySource(
+ sharedDefaultParamsPsName, SHARED_DEFAULT_PROPERTIES);
+
+ String sharedParamsPsName = "aws:ssm:parameter:/config/application/";
+ PropertySource sharedParamsPs = new PropertySource(sharedParamsPsName,
+ SHARED_PROPERTIES);
+
+ Environment expected = new Environment(defaultApp, profiles, null, null, null);
+ expected.addAll(Arrays.asList(sharedDefaultParamsPs, sharedParamsPs));
+
+ setupAwsSsmClientMocks(expected);
+
+ // Act
+ Environment result = repository.findOne(application, profile, null);
+
+ // Assert
+ assertThat(result).usingRecursiveComparison().withStrictTypeChecking()
+ .isEqualTo(expected);
+ }
+
+ @Test
+ @SuppressWarnings("ConstantConditions")
+ public void testFindOneWithNullApplicationAndNonExistentProfile() {
+ // Arrange
+ String application = null;
+ String profile = randomAlphabetic(RandomUtils.nextInt(3, 33));
+ String defaultApp = configServerProperties.getDefaultApplicationName();
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
+
+ String name = "aws:ssm:parameter:/config/application/";
+ PropertySource ps = new PropertySource(name, SHARED_PROPERTIES);
+
+ Environment expected = new Environment(defaultApp, profiles, null, null, null);
+ expected.add(ps);
+
+ setupAwsSsmClientMocks(expected);
+
+ // Act
+ Environment result = repository.findOne(application, profile, null);
+
+ // Assert
+ assertThat(result).usingRecursiveComparison().withStrictTypeChecking()
+ .isEqualTo(expected);
+ }
+
+ @Test
+ @SuppressWarnings("ConstantConditions")
+ public void testFindOneWithNullApplicationAndExistentProfile() {
+ // Arrange
+ String application = null;
+ String profile = "production";
+ String defaultApp = configServerProperties.getDefaultApplicationName();
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
+
+ String sharedProdParamsPsName = "aws:ssm:parameter:/config/application-production/";
+ PropertySource sharedProdParamsPs = new PropertySource(sharedProdParamsPsName,
+ SHARED_PRODUCTION_PROPERTIES);
+
+ String sharedParamsPsName = "aws:ssm:parameter:/config/application/";
+ PropertySource sharedParamsPs = new PropertySource(sharedParamsPsName,
+ SHARED_PROPERTIES);
+
+ Environment expected = new Environment(defaultApp, profiles, null, null, null);
+ expected.addAll(Arrays.asList(sharedProdParamsPs, sharedParamsPs));
+
+ setupAwsSsmClientMocks(expected);
+
+ // Act
+ Environment result = repository.findOne(application, profile, null);
+
+ // Assert
+ assertThat(result).usingRecursiveComparison().withStrictTypeChecking()
+ .isEqualTo(expected);
+ }
+
+ @Test
+ @SuppressWarnings("ConstantConditions")
+ public void testFindOneWithDefaultApplicationAndNullProfile() {
+ // Arrange
+ String application = configServerProperties.getDefaultApplicationName();
+ String profile = null;
+ String defaultProfile = configServerProperties.getDefaultProfile();
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(defaultProfile);
+
+ String sharedDefaultParamsPsName = "aws:ssm:parameter:/config/application-default/";
+ PropertySource sharedDefaultParamsPs = new PropertySource(
+ sharedDefaultParamsPsName, SHARED_DEFAULT_PROPERTIES);
+
+ String sharedParamsPsName = "aws:ssm:parameter:/config/application/";
+ PropertySource sharedParamsPs = new PropertySource(sharedParamsPsName,
+ SHARED_PROPERTIES);
+
+ Environment expected = new Environment(application, profiles, null, null, null);
+ expected.addAll(Arrays.asList(sharedDefaultParamsPs, sharedParamsPs));
+
+ setupAwsSsmClientMocks(expected);
+
+ // Act
+ Environment result = repository.findOne(application, profile, null);
+
+ // Assert
+ assertThat(result).usingRecursiveComparison().withStrictTypeChecking()
+ .isEqualTo(expected);
+ }
+
+ @Test
+ public void testFindOneWithDefaultApplicationAndDefaultProfile() {
+ // Arrange
+ String application = configServerProperties.getDefaultApplicationName();
+ String profile = configServerProperties.getDefaultProfile();
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
+
+ String sharedDefaultParamsPsName = "aws:ssm:parameter:/config/application-default/";
+ PropertySource sharedDefaultParamsPs = new PropertySource(
+ sharedDefaultParamsPsName, SHARED_DEFAULT_PROPERTIES);
+
+ String sharedParamsPsName = "aws:ssm:parameter:/config/application/";
+ PropertySource sharedParamsPs = new PropertySource(sharedParamsPsName,
+ SHARED_PROPERTIES);
+
+ Environment expected = new Environment(application, profiles, null, null, null);
+ expected.addAll(Arrays.asList(sharedDefaultParamsPs, sharedParamsPs));
+
+ setupAwsSsmClientMocks(expected);
+
+ // Act
+ Environment result = repository.findOne(application, profile, null);
+
+ // Assert
+ assertThat(result).usingRecursiveComparison().withStrictTypeChecking()
+ .isEqualTo(expected);
+ }
+
+ @Test
+ public void testFindOneWithDefaultApplicationAndNonExistentProfile() {
+ // Arrange
+ String application = configServerProperties.getDefaultApplicationName();
+ String profile = randomAlphabetic(RandomUtils.nextInt(3, 33));
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
+
+ String name = "aws:ssm:parameter:/config/application/";
+ PropertySource ps = new PropertySource(name, SHARED_PROPERTIES);
+
+ Environment expected = new Environment(application, profiles, null, null, null);
+ expected.add(ps);
+
+ setupAwsSsmClientMocks(expected);
+
+ // Act
+ Environment result = repository.findOne(application, profile, null);
+
+ // Assert
+ assertThat(result).usingRecursiveComparison().withStrictTypeChecking()
+ .isEqualTo(expected);
+ }
+
+ @Test
+ public void testFindOneWithDefaultApplicationAndExistentProfile() {
+ // Arrange
+ String application = configServerProperties.getDefaultApplicationName();
+ String profile = "production";
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
+
+ String sharedProdParamsPsName = "aws:ssm:parameter:/config/application-production/";
+ PropertySource sharedProdParamsPs = new PropertySource(sharedProdParamsPsName,
+ SHARED_PRODUCTION_PROPERTIES);
+
+ String sharedParamsPsName = "aws:ssm:parameter:/config/application/";
+ PropertySource sharedParamsPs = new PropertySource(sharedParamsPsName,
+ SHARED_PROPERTIES);
+
+ Environment expected = new Environment(application, profiles, null, null, null);
+ expected.addAll(Arrays.asList(sharedProdParamsPs, sharedParamsPs));
+
+ setupAwsSsmClientMocks(expected);
+
+ // Act
+ Environment result = repository.findOne(application, profile, null);
+
+ // Assert
+ assertThat(result).usingRecursiveComparison().withStrictTypeChecking()
+ .isEqualTo(expected);
+ }
+
+ @Test
+ @SuppressWarnings("ConstantConditions")
+ public void testFindOneWithNonExistentApplicationAndNullProfile() {
+ // Arrange
+ String application = randomAlphabetic(RandomUtils.nextInt(3, 33));
+ String profile = null;
+ String defaultProfile = configServerProperties.getDefaultProfile();
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(defaultProfile);
+
+ String sharedDefaultParamsPsName = "aws:ssm:parameter:/config/application-default/";
+ PropertySource sharedDefaultParamsPs = new PropertySource(
+ sharedDefaultParamsPsName, SHARED_DEFAULT_PROPERTIES);
+
+ String sharedParamsPsName = "aws:ssm:parameter:/config/application/";
+ PropertySource sharedParamsPs = new PropertySource(sharedParamsPsName,
+ SHARED_PROPERTIES);
+
+ Environment expected = new Environment(application, profiles, null, null, null);
+ expected.addAll(Arrays.asList(sharedDefaultParamsPs, sharedParamsPs));
+
+ setupAwsSsmClientMocks(expected);
+
+ // Act
+ Environment result = repository.findOne(application, profile, null);
+
+ // Assert
+ assertThat(result).usingRecursiveComparison().withStrictTypeChecking()
+ .isEqualTo(expected);
+ }
+
+ @Test
+ public void testFindOneWithNonExistentApplicationAndDefaultProfile() {
+ // Arrange
+ String application = randomAlphabetic(RandomUtils.nextInt(3, 33));
+ String profile = configServerProperties.getDefaultProfile();
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
+
+ String sharedDefaultParamsPsName = "aws:ssm:parameter:/config/application-default/";
+ PropertySource sharedDefaultParamsPs = new PropertySource(
+ sharedDefaultParamsPsName, SHARED_DEFAULT_PROPERTIES);
+
+ String sharedParamsPsName = "aws:ssm:parameter:/config/application/";
+ PropertySource sharedParamsPs = new PropertySource(sharedParamsPsName,
+ SHARED_PROPERTIES);
+
+ Environment expected = new Environment(application, profiles, null, null, null);
+ expected.addAll(Arrays.asList(sharedDefaultParamsPs, sharedParamsPs));
+
+ setupAwsSsmClientMocks(expected);
+
+ // Act
+ Environment result = repository.findOne(application, profile, null);
+
+ // Assert
+ assertThat(result).usingRecursiveComparison().withStrictTypeChecking()
+ .isEqualTo(expected);
+ }
+
+ @Test
+ public void testFindOneWithNonExistentApplicationAndNonExistentProfile() {
+ // Arrange
+ String application = randomAlphabetic(RandomUtils.nextInt(3, 33));
+ String profile = randomAlphabetic(RandomUtils.nextInt(3, 33));
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
+
+ String name = "aws:ssm:parameter:/config/application/";
+ PropertySource ps = new PropertySource(name, SHARED_PROPERTIES);
+
+ Environment expected = new Environment(application, profiles, null, null, null);
+ expected.add(ps);
+
+ setupAwsSsmClientMocks(expected);
+
+ // Act
+ Environment result = repository.findOne(application, profile, null);
+
+ // Assert
+ assertThat(result).usingRecursiveComparison().withStrictTypeChecking()
+ .isEqualTo(expected);
+ }
+
+ @Test
+ public void testFindOneWithNonExistentApplicationAndExistentProfile() {
+ // Arrange
+ String application = randomAlphabetic(RandomUtils.nextInt(3, 33));
+ String profile = "production";
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
+
+ String sharedProdParamsPsName = "aws:ssm:parameter:/config/application-production/";
+ PropertySource sharedProdParamsPs = new PropertySource(sharedProdParamsPsName,
+ SHARED_PRODUCTION_PROPERTIES);
+
+ String sharedParamsPsName = "aws:ssm:parameter:/config/application/";
+ PropertySource sharedParamsPs = new PropertySource(sharedParamsPsName,
+ SHARED_PROPERTIES);
+
+ Environment expected = new Environment(application, profiles, null, null, null);
+ expected.addAll(Arrays.asList(sharedProdParamsPs, sharedParamsPs));
+
+ setupAwsSsmClientMocks(expected);
+
+ // Act
+ Environment result = repository.findOne(application, profile, null);
+
+ // Assert
+ assertThat(result).usingRecursiveComparison().withStrictTypeChecking()
+ .isEqualTo(expected);
+ }
+
+ @Test
+ @SuppressWarnings("ConstantConditions")
+ public void testFindOneWithExistentApplicationAndNullProfile() {
+ // Arrange
+ String application = "service";
+ String profile = null;
+ String defaultProfile = configServerProperties.getDefaultProfile();
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(defaultProfile);
+
+ String appSpecificDefaultParamsPsName = "aws:ssm:parameter:/config/service-default/";
+ PropertySource appSpecificDefaultParamsPs = new PropertySource(
+ appSpecificDefaultParamsPsName, APPLICATION_SPECIFIC_DEFAULT_PROPERTIES);
+
+ String sharedDefaultParamsPsName = "aws:ssm:parameter:/config/application-default/";
+ PropertySource sharedDefaultParamsPs = new PropertySource(
+ sharedDefaultParamsPsName, SHARED_DEFAULT_PROPERTIES);
+
+ String appSpecificParamsPsName = "aws:ssm:parameter:/config/service/";
+ PropertySource appSpecificParamsPs = new PropertySource(appSpecificParamsPsName,
+ APPLICATION_SPECIFIC_PROPERTIES);
+
+ String sharedParamsPsName = "aws:ssm:parameter:/config/application/";
+ PropertySource sharedParamsPs = new PropertySource(sharedParamsPsName,
+ SHARED_PROPERTIES);
+
+ Environment expected = new Environment(application, profiles, null, null, null);
+
+ expected.addAll(Arrays.asList(appSpecificDefaultParamsPs, sharedDefaultParamsPs,
+ appSpecificParamsPs, sharedParamsPs));
+
+ setupAwsSsmClientMocks(expected);
+
+ // Act
+ Environment result = repository.findOne(application, profile, null);
+
+ // Assert
+ assertThat(result).usingRecursiveComparison().withStrictTypeChecking()
+ .isEqualTo(expected);
+ }
+
+ @Test
+ public void testFindOneWithExistentApplicationAndDefaultProfile() {
+ // Arrange
+ String application = "service";
+ String profile = configServerProperties.getDefaultProfile();
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
+
+ String appSpecificDefaultParamsPsName = "aws:ssm:parameter:/config/service-default/";
+ PropertySource appSpecificDefaultParamsPs = new PropertySource(
+ appSpecificDefaultParamsPsName, APPLICATION_SPECIFIC_DEFAULT_PROPERTIES);
+
+ String sharedDefaultParamsPsName = "aws:ssm:parameter:/config/application-default/";
+ PropertySource sharedDefaultParamsPs = new PropertySource(
+ sharedDefaultParamsPsName, SHARED_DEFAULT_PROPERTIES);
+
+ String appSpecificParamsPsName = "aws:ssm:parameter:/config/service/";
+ PropertySource appSpecificParamsPs = new PropertySource(appSpecificParamsPsName,
+ APPLICATION_SPECIFIC_PROPERTIES);
+
+ String sharedParamsPsName = "aws:ssm:parameter:/config/application/";
+ PropertySource sharedParamsPs = new PropertySource(sharedParamsPsName,
+ SHARED_PROPERTIES);
+
+ Environment expected = new Environment(application, profiles, null, null, null);
+
+ expected.addAll(Arrays.asList(appSpecificDefaultParamsPs, sharedDefaultParamsPs,
+ appSpecificParamsPs, sharedParamsPs));
+
+ setupAwsSsmClientMocks(expected);
+
+ // Act
+ Environment result = repository.findOne(application, profile, null);
+
+ // Assert
+ assertThat(result).usingRecursiveComparison().withStrictTypeChecking()
+ .isEqualTo(expected);
+ }
+
+ @Test
+ public void testFindOneWithExistentApplicationAndNonExistentProfile() {
+ // Arrange
+ String application = "service";
+ String profile = randomAlphabetic(RandomUtils.nextInt(3, 33));
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
+
+ String appSpecificParamsPsName = "aws:ssm:parameter:/config/service/";
+ PropertySource appSpecificParamsPs = new PropertySource(appSpecificParamsPsName,
+ APPLICATION_SPECIFIC_PROPERTIES);
+
+ String sharedParamsPsName = "aws:ssm:parameter:/config/application/";
+ PropertySource sharedParamsPs = new PropertySource(sharedParamsPsName,
+ SHARED_PROPERTIES);
+
+ Environment expected = new Environment(application, profiles, null, null, null);
+ expected.addAll(Arrays.asList(appSpecificParamsPs, sharedParamsPs));
+
+ setupAwsSsmClientMocks(expected);
+
+ // Act
+ Environment result = repository.findOne(application, profile, null);
+
+ // Assert
+ assertThat(result).usingRecursiveComparison().withStrictTypeChecking()
+ .isEqualTo(expected);
+ }
+
+ @Test
+ public void testFindOneWithExistentApplicationAndExistentProfile() {
+ // Arrange
+ String application = "service";
+ String profile = "production";
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
+
+ String appSpecificProdParamsPsName = "aws:ssm:parameter:/config/service-production/";
+ PropertySource appSpecificProdParamsPs = new PropertySource(
+ appSpecificProdParamsPsName, APPLICATION_SPECIFIC_PRODUCTION_PROPERTIES);
+
+ String sharedProdParamsPsName = "aws:ssm:parameter:/config/application-production/";
+ PropertySource sharedProdParamsPs = new PropertySource(sharedProdParamsPsName,
+ SHARED_PRODUCTION_PROPERTIES);
+
+ String appSpecificParamsPsName = "aws:ssm:parameter:/config/service/";
+ PropertySource appSpecificParamsPs = new PropertySource(appSpecificParamsPsName,
+ APPLICATION_SPECIFIC_PROPERTIES);
+
+ String sharedParamsPsName = "aws:ssm:parameter:/config/application/";
+ PropertySource sharedParamsPs = new PropertySource(sharedParamsPsName,
+ SHARED_PROPERTIES);
+
+ Environment expected = new Environment(application, profiles, null, null, null);
+
+ expected.addAll(Arrays.asList(appSpecificProdParamsPs, sharedProdParamsPs,
+ appSpecificParamsPs, sharedParamsPs));
+
+ setupAwsSsmClientMocks(expected);
+
+ // Act
+ Environment result = repository.findOne(application, profile, null);
+
+ // Assert
+ assertThat(result).usingRecursiveComparison().withStrictTypeChecking()
+ .isEqualTo(expected);
+ }
+
+ @Test
+ public void testFindOneWithExistentApplicationAndMultipleExistentProfiles() {
+ // Arrange
+ String application = "service";
+ String profile = configServerProperties.getDefaultProfile() + ",production";
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
+
+ String appSpecificProdParamsPsName = "aws:ssm:parameter:/config/service-production/";
+ PropertySource appSpecificProdParamsPs = new PropertySource(
+ appSpecificProdParamsPsName, APPLICATION_SPECIFIC_PRODUCTION_PROPERTIES);
+
+ String sharedProdParamsPsName = "aws:ssm:parameter:/config/application-production/";
+ PropertySource sharedProdParamsPs = new PropertySource(sharedProdParamsPsName,
+ SHARED_PRODUCTION_PROPERTIES);
+
+ String appSpecificDefaultParamsPsName = "aws:ssm:parameter:/config/service-default/";
+ PropertySource appSpecificDefaultParamsPs = new PropertySource(
+ appSpecificDefaultParamsPsName, APPLICATION_SPECIFIC_DEFAULT_PROPERTIES);
+
+ String sharedDefaultParamsPsName = "aws:ssm:parameter:/config/application-default/";
+ PropertySource sharedDefaultParamsPs = new PropertySource(
+ sharedDefaultParamsPsName, SHARED_DEFAULT_PROPERTIES);
+
+ String appSpecificParamsPsName = "aws:ssm:parameter:/config/service/";
+ PropertySource appSpecificParamsPs = new PropertySource(appSpecificParamsPsName,
+ APPLICATION_SPECIFIC_PROPERTIES);
+
+ String sharedParamsPsName = "aws:ssm:parameter:/config/application/";
+ PropertySource sharedParamsPs = new PropertySource(sharedParamsPsName,
+ SHARED_PROPERTIES);
+
+ Environment expected = new Environment(application, profiles, null, null, null);
+
+ expected.addAll(Arrays.asList(appSpecificProdParamsPs, sharedProdParamsPs,
+ appSpecificDefaultParamsPs, sharedDefaultParamsPs, appSpecificParamsPs,
+ sharedParamsPs));
+
+ setupAwsSsmClientMocks(expected);
+
+ // Act
+ Environment result = repository.findOne(application, profile, null);
+
+ // Assert
+ assertThat(result).usingRecursiveComparison().withStrictTypeChecking()
+ .isEqualTo(expected);
+ }
+
+ @Test
+ public void testFindOneWithOverrides() {
+ // Arrange
+ String application = configServerProperties.getDefaultApplicationName();
+ String profile = configServerProperties.getDefaultProfile();
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
+
+ Map overrides = new HashMap(4) {
+ {
+ put("logging.level.root", "boom");
+ put("logging.level.com.example.service", "boom");
+ put("spring.cache.redis.time-to-live", "-1");
+ }
+ };
+
+ configServerProperties.setOverrides(overrides);
+
+ PropertySource overridesPs = new PropertySource("overrides", overrides);
+
+ String sharedDefaultParamsPsName = "aws:ssm:parameter:/config/application-default/";
+ PropertySource sharedDefaultParamsPs = new PropertySource(
+ sharedDefaultParamsPsName, SHARED_DEFAULT_PROPERTIES);
+
+ String sharedParamsPsName = "aws:ssm:parameter:/config/application/";
+ PropertySource sharedParamsPs = new PropertySource(sharedParamsPsName,
+ SHARED_PROPERTIES);
+
+ Environment expected = new Environment(application, profiles, null, null, null);
+
+ expected.addAll(
+ Arrays.asList(overridesPs, sharedDefaultParamsPs, sharedParamsPs));
+
+ setupAwsSsmClientMocks(expected);
+
+ // Act
+ Environment result = repository.findOne(application, profile, null);
+
+ // Assert
+ assertThat(result).usingRecursiveComparison().withStrictTypeChecking()
+ .isEqualTo(expected);
+ }
+
+ @Test
+ public void testFindOneWithSlashesInTheParameterKeyPath() {
+ // Arrange
+ String application = configServerProperties.getDefaultApplicationName();
+ String profile = configServerProperties.getDefaultProfile();
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
+
+ String sharedDefaultParamsPsName = "aws:ssm:parameter:/config/application-default/";
+ PropertySource sharedDefaultParamsPs = new PropertySource(
+ sharedDefaultParamsPsName, SHARED_DEFAULT_PROPERTIES);
+
+ String sharedParamsPsName = "aws:ssm:parameter:/config/application/";
+ PropertySource sharedParamsPs = new PropertySource(sharedParamsPsName,
+ SHARED_PROPERTIES);
+
+ Environment expected = new Environment(application, profiles, null, null, null);
+ expected.addAll(Arrays.asList(sharedDefaultParamsPs, sharedParamsPs));
+
+ setupAwsSsmClientMocks(expected, true, false);
+
+ // Act
+ Environment result = repository.findOne(application, profile, null);
+
+ // Assert
+ assertThat(result).usingRecursiveComparison().withStrictTypeChecking()
+ .isEqualTo(expected);
+ }
+
+ @Test
+ public void testFindOneWithPaginatedAwsSsmClientResponse() {
+ // Arrange
+ String application = configServerProperties.getDefaultApplicationName();
+ String profile = configServerProperties.getDefaultProfile();
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
+
+ environmentProperties.setMaxResults(1);
+
+ String sharedDefaultParamsPsName = "aws:ssm:parameter:/config/application-default/";
+ PropertySource sharedDefaultParamsPs = new PropertySource(
+ sharedDefaultParamsPsName, SHARED_DEFAULT_PROPERTIES);
+
+ String sharedParamsPsName = "aws:ssm:parameter:/config/application/";
+ PropertySource sharedParamsPs = new PropertySource(sharedParamsPsName,
+ SHARED_PROPERTIES);
+
+ Environment expected = new Environment(application, profiles, null, null, null);
+ expected.addAll(Arrays.asList(sharedDefaultParamsPs, sharedParamsPs));
+
+ setupAwsSsmClientMocks(expected, false, true);
+
+ // Act
+ Environment result = repository.findOne(application, profile, null);
+
+ // Assert
+ assertThat(result).usingRecursiveComparison().withStrictTypeChecking()
+ .isEqualTo(expected);
+ }
+
+ @Test
+ public void testFindOneWithNoParametersInThePaths() {
+ // Arrange
+ String application = configServerProperties.getDefaultApplicationName();
+ String profile = configServerProperties.getDefaultProfile();
+ String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
+
+ Environment expected = new Environment(application, profiles, null, null, null);
+
+ when(awsSsmClientMock.getParametersByPath(any(GetParametersByPathRequest.class)))
+ .thenReturn(new GetParametersByPathResult());
+
+ // Act
+ Environment result = repository.findOne(application, profile, null);
+
+ // Assert
+ assertThat(result).usingRecursiveComparison().withStrictTypeChecking()
+ .isEqualTo(expected);
+ }
+
+ private void setupAwsSsmClientMocks(Environment environment) {
+ setupAwsSsmClientMocks(environment, false, false);
+ }
+
+ private void setupAwsSsmClientMocks(Environment environment,
+ boolean withSlashesForPropertyName, boolean paginatedResponse) {
+ for (PropertySource ps : environment.getPropertySources()) {
+ String path = StringUtils.delete(ps.getName(),
+ environmentProperties.getOrigin());
+
+ GetParametersByPathRequest request = new GetParametersByPathRequest()
+ .withPath(path).withRecursive(environmentProperties.isRecursive())
+ .withWithDecryption(environmentProperties.isDecryptValues())
+ .withMaxResults(environmentProperties.getMaxResults());
+
+ Set parameters = getParameters(ps, path,
+ withSlashesForPropertyName);
+
+ GetParametersByPathResult response = new GetParametersByPathResult()
+ .withParameters(parameters);
+
+ if (paginatedResponse
+ && environmentProperties.getMaxResults() < parameters.size()) {
+ List> chunks = splitParametersIntoChunks(parameters);
+
+ String nextToken = null;
+
+ for (int i = 0; i < chunks.size(); i++) {
+ Set chunk = chunks.get(i);
+
+ if (i == 0) {
+ nextToken = generateNextToken();
+
+ GetParametersByPathResult responseClone = response.clone()
+ .withParameters(chunk).withNextToken(nextToken);
+
+ when(awsSsmClientMock.getParametersByPath(eq(request)))
+ .thenReturn(responseClone);
+ }
+ else if (i == chunks.size() - 1) {
+ GetParametersByPathRequest requestClone = request.clone().withNextToken(nextToken);
+ GetParametersByPathResult responseClone = response.clone().withParameters(chunk);
+
+ when(awsSsmClientMock.getParametersByPath(eq(requestClone)))
+ .thenReturn(responseClone);
+ }
+ else {
+ String newNextToken = generateNextToken();
+
+ GetParametersByPathRequest requestClone = request.clone()
+ .withNextToken(nextToken);
+
+ GetParametersByPathResult responseClone = response.clone()
+ .withParameters(chunk).withNextToken(newNextToken);
+
+ when(awsSsmClientMock.getParametersByPath(eq(requestClone)))
+ .thenReturn(responseClone);
+
+ nextToken = newNextToken;
+ }
+ }
+ }
+ else {
+ when(awsSsmClientMock.getParametersByPath(eq(request)))
+ .thenReturn(response);
+ }
+ }
+ }
+
+ private Set getParameters(PropertySource propertySource, String path,
+ boolean withSlashesForPropertyName) {
+ Function, Parameter> mapper = p -> new Parameter()
+ .withName(path + (withSlashesForPropertyName
+ ? ((String) p.getKey()).replace(".", DEFAULT_PATH_SEPARATOR)
+ : p.getKey()))
+ .withType(ParameterType.String).withValue((String) p.getValue())
+ .withVersion(1L);
+
+ return propertySource.getSource().entrySet().stream().map(mapper)
+ .collect(Collectors.toSet());
+ }
+
+ private List> splitParametersIntoChunks(Set parameters) {
+ AtomicInteger counter = new AtomicInteger();
+
+ Collector>> collector = Collectors
+ .groupingBy(
+ p -> counter.getAndIncrement()
+ / environmentProperties.getMaxResults(),
+ Collectors.toSet());
+
+ return new ArrayList<>(parameters.stream().collect(collector).values());
+ }
+
+ private String generateNextToken() {
+ String random = randomAlphabetic(RandomUtils.nextInt(3, 33));
+
+ return Base64.getEncoder()
+ .encodeToString(random.getBytes(StandardCharsets.UTF_8));
+ }
+
+}