From 39cb1a2104e2447e778f05e08b2922f217ef6f7a Mon Sep 17 00:00:00 2001 From: Iulian Antohe Date: Tue, 27 Jul 2021 17:01:38 -0400 Subject: [PATCH] Add support for the AWS Systems Manager Parameter Store Fixes gh-1598 Fixes gh-850 --- .../main/asciidoc/spring-cloud-config.adoc | 107 ++- pom.xml | 7 +- spring-cloud-config-server/pom.xml | 5 + .../EnvironmentRepositoryConfiguration.java | 39 +- ...wsParameterStoreEnvironmentProperties.java | 178 ++++ ...wsParameterStoreEnvironmentRepository.java | 174 ++++ ...eterStoreEnvironmentRepositoryFactory.java | 69 ++ ...gleSecretManagerEnvironmentProperties.java | 3 +- ...gleSecretManagerEnvironmentRepository.java | 39 +- ...etManagerEnvironmentRepositoryFactory.java | 9 +- ...gleSecretManagerAccessStrategyFactory.java | 23 +- .../GoogleSecretManagerV1AccessStrategy.java | 81 +- .../HttpHeaderGoogleConfigProvider.java | 3 +- .../cloud/config/server/AdhocTestSuite.java | 5 +- ...ameterStoreEnvironmentRepositoryTests.java | 856 ++++++++++++++++++ ...cretManagerEnvironmentRepositoryTests.java | 43 +- 16 files changed, 1506 insertions(+), 135 deletions(-) create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsParameterStoreEnvironmentProperties.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsParameterStoreEnvironmentRepository.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsParameterStoreEnvironmentRepositoryFactory.java create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/AwsParameterStoreEnvironmentRepositoryTests.java 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 271ba57c..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,18 +110,20 @@ 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, - GoogleSecretManagerRepositoryConfiguration.class, DefaultRepositoryConfiguration.class }) + AwsParameterStoreRepositoryConfiguration.class,, + GoogleSecretManagerRepositoryConfiguration.class, DefaultRepositoryConfiguration.class }) public class EnvironmentRepositoryConfiguration { @Bean @@ -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 { @@ -516,8 +548,7 @@ class GoogleSecretManagerRepositoryConfiguration { @Bean public GoogleSecretManagerEnvironmentRepository googleSecretManagerEnvironmentRepository( GoogleSecretManagerEnvironmentRepositoryFactory factory, - GoogleSecretManagerEnvironmentProperties environmentProperties) - throws Exception { + GoogleSecretManagerEnvironmentProperties environmentProperties) throws Exception { return factory.build(environmentProperties); } 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/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentProperties.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentProperties.java index 2c1bbd7e..dd472d39 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentProperties.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentProperties.java @@ -24,8 +24,7 @@ import org.springframework.core.Ordered; * @author Jose Maria Alvarez */ @ConfigurationProperties("spring.cloud.config.server.gcp-secret-manager") -public class GoogleSecretManagerEnvironmentProperties - implements EnvironmentRepositoryProperties { +public class GoogleSecretManagerEnvironmentProperties implements EnvironmentRepositoryProperties { private int order = Ordered.LOWEST_PRECEDENCE; diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepository.java index f76da9fc..ee30ad81 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepository.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepository.java @@ -49,14 +49,12 @@ public class GoogleSecretManagerEnvironmentRepository implements EnvironmentRepo private GoogleConfigProvider configProvider; - public GoogleSecretManagerEnvironmentRepository( - ObjectProvider request, RestTemplate rest, - GoogleSecretManagerEnvironmentProperties properties) { + public GoogleSecretManagerEnvironmentRepository(ObjectProvider request, RestTemplate rest, + GoogleSecretManagerEnvironmentProperties properties) { this.applicationLabel = properties.getApplicationLabel(); this.profileLabel = properties.getProfileLabel(); this.configProvider = new HttpHeaderGoogleConfigProvider(request); - this.accessStrategy = GoogleSecretManagerAccessStrategyFactory.forVersion(rest, - configProvider, properties); + this.accessStrategy = GoogleSecretManagerAccessStrategyFactory.forVersion(rest, configProvider, properties); this.tokenMandatory = properties.getTokenMandatory(); } @@ -72,7 +70,7 @@ public class GoogleSecretManagerEnvironmentRepository implements EnvironmentRepo profile = "default," + profile; } String[] profiles = org.springframework.util.StringUtils - .trimArrayElements(org.springframework.util.StringUtils.commaDelimitedListToStringArray(profile)); + .trimArrayElements(org.springframework.util.StringUtils.commaDelimitedListToStringArray(profile)); Environment result = new Environment(application, profile, label, null, null); if (tokenMandatory) { if (accessStrategy.checkRemotePermissions()) { @@ -85,13 +83,11 @@ public class GoogleSecretManagerEnvironmentRepository implements EnvironmentRepo return result; } - private void addPropertySource(String application, String[] profiles, - Environment result) { + private void addPropertySource(String application, String[] profiles, Environment result) { for (String profileUnit : profiles) { Map secrets = getSecrets(application, profileUnit); if (!secrets.isEmpty()) { - result.add(new PropertySource("gsm:" + application + "-" + profileUnit, - secrets)); + result.add(new PropertySource("gsm:" + application + "-" + profileUnit, secrets)); } } } @@ -103,23 +99,16 @@ public class GoogleSecretManagerEnvironmentRepository implements EnvironmentRepo */ private Map getSecrets(String application, String profile) { Map result = new HashMap<>(); - String prefix = configProvider - .getValue(HttpHeaderGoogleConfigProvider.PREFIX_HEADER, false); + String prefix = configProvider.getValue(HttpHeaderGoogleConfigProvider.PREFIX_HEADER, false); for (Secret secret : accessStrategy.getSecrets()) { - if (secret.getLabelsOrDefault(applicationLabel, "application") - .equalsIgnoreCase(application) - && secret.getLabelsOrDefault(profileLabel, "profile") - .equalsIgnoreCase(profile)) { - result.put(accessStrategy.getSecretName(secret), accessStrategy - .getSecretValue(secret, new GoogleSecretComparatorByVersion())); + if (secret.getLabelsOrDefault(applicationLabel, "application").equalsIgnoreCase(application) + && secret.getLabelsOrDefault(profileLabel, "profile").equalsIgnoreCase(profile)) { + result.put(accessStrategy.getSecretName(secret), + accessStrategy.getSecretValue(secret, new GoogleSecretComparatorByVersion())); } - else if (StringUtils.isNotBlank(prefix) - && accessStrategy.getSecretName(secret).startsWith(prefix)) { - result.put( - StringUtils.removeStart(accessStrategy.getSecretName(secret), - prefix), - accessStrategy.getSecretValue(secret, - new GoogleSecretComparatorByVersion())); + else if (StringUtils.isNotBlank(prefix) && accessStrategy.getSecretName(secret).startsWith(prefix)) { + result.put(StringUtils.removeStart(accessStrategy.getSecretName(secret), prefix), + accessStrategy.getSecretValue(secret, new GoogleSecretComparatorByVersion())); } } return result; diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepositoryFactory.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepositoryFactory.java index 063967de..16ddf3d0 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepositoryFactory.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepositoryFactory.java @@ -29,17 +29,14 @@ public class GoogleSecretManagerEnvironmentRepositoryFactory implements private final ObjectProvider request; - public GoogleSecretManagerEnvironmentRepositoryFactory( - ObjectProvider request) { + public GoogleSecretManagerEnvironmentRepositoryFactory(ObjectProvider request) { this.request = request; } @Override public GoogleSecretManagerEnvironmentRepository build( - GoogleSecretManagerEnvironmentProperties environmentProperties) - throws Exception { - return new GoogleSecretManagerEnvironmentRepository(request, new RestTemplate(), - environmentProperties); + GoogleSecretManagerEnvironmentProperties environmentProperties) throws Exception { + return new GoogleSecretManagerEnvironmentRepository(request, new RestTemplate(), environmentProperties); } } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretManagerAccessStrategyFactory.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretManagerAccessStrategyFactory.java index 9ef225ba..030bf834 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretManagerAccessStrategyFactory.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretManagerAccessStrategyFactory.java @@ -28,40 +28,33 @@ public final class GoogleSecretManagerAccessStrategyFactory { throw new IllegalStateException("Can't instantiate an utility class"); } - public static GoogleSecretManagerAccessStrategy forVersion(RestTemplate rest, - GoogleConfigProvider configProvider, - GoogleSecretManagerEnvironmentProperties properties) { + public static GoogleSecretManagerAccessStrategy forVersion(RestTemplate rest, GoogleConfigProvider configProvider, + GoogleSecretManagerEnvironmentProperties properties) { switch (properties.getVersion()) { case 1: try { - return new GoogleSecretManagerV1AccessStrategy(rest, configProvider, - properties.getServiceAccount()); + return new GoogleSecretManagerV1AccessStrategy(rest, configProvider, properties.getServiceAccount()); } catch (Exception e) { throw new RepositoryException("Cannot create service client", e); } default: throw new IllegalArgumentException( - "No support for given Google Secret manager backend version " - + properties.getVersion()); + "No support for given Google Secret manager backend version " + properties.getVersion()); } } - public static GoogleSecretManagerAccessStrategy forVersion(RestTemplate rest, - GoogleConfigProvider configProvider, - GoogleSecretManagerEnvironmentProperties properties, - SecretManagerServiceClient client) { + public static GoogleSecretManagerAccessStrategy forVersion(RestTemplate rest, GoogleConfigProvider configProvider, + GoogleSecretManagerEnvironmentProperties properties, SecretManagerServiceClient client) { switch (properties.getVersion()) { case 1: - return new GoogleSecretManagerV1AccessStrategy(rest, configProvider, - client); + return new GoogleSecretManagerV1AccessStrategy(rest, configProvider, client); default: throw new IllegalArgumentException( - "No support for given Google Secret manager backend version " - + properties.getVersion()); + "No support for given Google Secret manager backend version " + properties.getVersion()); } } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretManagerV1AccessStrategy.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretManagerV1AccessStrategy.java index 6b4b5943..57a94b21 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretManagerV1AccessStrategy.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretManagerV1AccessStrategy.java @@ -55,8 +55,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.web.client.RestTemplate; -public class GoogleSecretManagerV1AccessStrategy - implements GoogleSecretManagerAccessStrategy { +public class GoogleSecretManagerV1AccessStrategy implements GoogleSecretManagerAccessStrategy { private final SecretManagerServiceClient client; @@ -68,19 +67,14 @@ public class GoogleSecretManagerV1AccessStrategy private static final String ACCESS_SECRET_PERMISSION = "secretmanager.versions.access"; - private static Log logger = LogFactory - .getLog(GoogleSecretManagerV1AccessStrategy.class); + private static Log logger = LogFactory.getLog(GoogleSecretManagerV1AccessStrategy.class); - public GoogleSecretManagerV1AccessStrategy(RestTemplate rest, - GoogleConfigProvider configProvider, String serviceAccountFile) - throws IOException { + public GoogleSecretManagerV1AccessStrategy(RestTemplate rest, GoogleConfigProvider configProvider, + String serviceAccountFile) throws IOException { if (StringUtils.isNotEmpty(serviceAccountFile)) { - GoogleCredentials creds = GoogleCredentials - .fromStream(new FileInputStream(new File(serviceAccountFile))); - this.client = SecretManagerServiceClient.create(SecretManagerServiceSettings - .newBuilder() - .setCredentialsProvider(FixedCredentialsProvider.create(creds)) - .build()); + GoogleCredentials creds = GoogleCredentials.fromStream(new FileInputStream(new File(serviceAccountFile))); + this.client = SecretManagerServiceClient.create(SecretManagerServiceSettings.newBuilder() + .setCredentialsProvider(FixedCredentialsProvider.create(creds)).build()); } else { this.client = SecretManagerServiceClient.create(); @@ -89,8 +83,8 @@ public class GoogleSecretManagerV1AccessStrategy this.configProvider = configProvider; } - public GoogleSecretManagerV1AccessStrategy(RestTemplate rest, - GoogleConfigProvider configProvider, SecretManagerServiceClient client) { + public GoogleSecretManagerV1AccessStrategy(RestTemplate rest, GoogleConfigProvider configProvider, + SecretManagerServiceClient client) { this.client = client; this.rest = rest; this.configProvider = configProvider; @@ -102,12 +96,11 @@ public class GoogleSecretManagerV1AccessStrategy ProjectName project = ProjectName.of(getProjectId()); // Create the request. - ListSecretsRequest listSecretRequest = ListSecretsRequest.newBuilder() - .setParent(project.toString()).build(); + ListSecretsRequest listSecretRequest = ListSecretsRequest.newBuilder().setParent(project.toString()).build(); // Get all secrets. SecretManagerServiceClient.ListSecretsPagedResponse pagedListSecretResponse = client - .listSecrets(listSecretRequest); + .listSecrets(listSecretRequest); List result = new ArrayList(); pagedListSecretResponse.iterateAll().forEach(result::add); @@ -120,12 +113,12 @@ public class GoogleSecretManagerV1AccessStrategy SecretName parent = SecretName.parse(secret.getName()); // Create the request. - ListSecretVersionsRequest listVersionRequest = ListSecretVersionsRequest - .newBuilder().setParent(parent.toString()).build(); + ListSecretVersionsRequest listVersionRequest = ListSecretVersionsRequest.newBuilder() + .setParent(parent.toString()).build(); // Get all versions. SecretManagerServiceClient.ListSecretVersionsPagedResponse pagedListVersionResponse = client - .listSecretVersions(listVersionRequest); + .listSecretVersions(listVersionRequest); List result = new ArrayList(); pagedListVersionResponse.iterateAll().forEach(result::add); return result; @@ -137,17 +130,16 @@ public class GoogleSecretManagerV1AccessStrategy List versions = getSecretVersions(secret); SecretVersion winner = null; for (SecretVersion secretVersion : versions) { - if ((secretVersion.getState() - .getNumber() == SecretVersion.State.ENABLED_VALUE) - && comparator.compare(secretVersion, winner) > 0) { + if ((secretVersion.getState().getNumber() == SecretVersion.State.ENABLED_VALUE) + && comparator.compare(secretVersion, winner) > 0) { winner = secretVersion; } } if (winner != null) { SecretVersionName name = SecretVersionName.parse(winner.getName()); // Access the secret version. - AccessSecretVersionRequest request = AccessSecretVersionRequest.newBuilder() - .setName(name.toString()).build(); + AccessSecretVersionRequest request = AccessSecretVersionRequest.newBuilder().setName(name.toString()) + .build(); AccessSecretVersionResponse response = client.accessSecretVersion(request); result = response.getPayload().getData().toStringUtf8(); } @@ -166,27 +158,22 @@ public class GoogleSecretManagerV1AccessStrategy try { AccessToken accessToken = new AccessToken(getAccessToken(), null); GoogleCredentials credential = new GoogleCredentials(accessToken); - HttpRequestInitializer requestInitializer = new HttpCredentialsAdapter( - credential); - service = new CloudResourceManager.Builder( - GoogleNetHttpTransport.newTrustedTransport(), - JacksonFactory.getDefaultInstance(), requestInitializer) - .setApplicationName(APPLICATION_NAME).build(); + HttpRequestInitializer requestInitializer = new HttpCredentialsAdapter(credential); + service = new CloudResourceManager.Builder(GoogleNetHttpTransport.newTrustedTransport(), + JacksonFactory.getDefaultInstance(), requestInitializer).setApplicationName(APPLICATION_NAME) + .build(); List permissionsList = Arrays.asList(ACCESS_SECRET_PERMISSION); - TestIamPermissionsRequest requestBody = new TestIamPermissionsRequest() - .setPermissions(permissionsList); + TestIamPermissionsRequest requestBody = new TestIamPermissionsRequest().setPermissions(permissionsList); TestIamPermissionsResponse testIamPermissionsResponse = service.projects() - .testIamPermissions(getProjectId(), requestBody).execute(); + .testIamPermissions(getProjectId(), requestBody).execute(); - if (testIamPermissionsResponse.getPermissions() != null - && testIamPermissionsResponse.size() >= 1) { + if (testIamPermissionsResponse.getPermissions() != null && testIamPermissionsResponse.size() >= 1) { return Boolean.TRUE; } else { - logger.warn( - "Access token has no permissions to access secrets in project"); + logger.warn("Access token has no permissions to access secrets in project"); return Boolean.FALSE; } } @@ -197,26 +184,22 @@ public class GoogleSecretManagerV1AccessStrategy } private String getAccessToken() { - return configProvider.getValue(HttpHeaderGoogleConfigProvider.ACCESS_TOKEN_HEADER, - true); + return configProvider.getValue(HttpHeaderGoogleConfigProvider.ACCESS_TOKEN_HEADER, true); } /** - * @return + * @return the Project Id. */ private String getProjectId() { String result = null; try { - result = configProvider - .getValue(HttpHeaderGoogleConfigProvider.PROJECT_ID_HEADER, true); + result = configProvider.getValue(HttpHeaderGoogleConfigProvider.PROJECT_ID_HEADER, true); } catch (Exception e) { // not in GCP - HttpEntity entity = new HttpEntity("parameters", - getMetadataHttpHeaders()); - result = rest.exchange( - GoogleSecretManagerEnvironmentProperties.GOOGLE_METADATA_PROJECT_URL, - HttpMethod.GET, entity, String.class).getBody(); + HttpEntity entity = new HttpEntity("parameters", getMetadataHttpHeaders()); + result = rest.exchange(GoogleSecretManagerEnvironmentProperties.GOOGLE_METADATA_PROJECT_URL, HttpMethod.GET, + entity, String.class).getBody(); } return result; } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/HttpHeaderGoogleConfigProvider.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/HttpHeaderGoogleConfigProvider.java index cee762e2..653c8729 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/HttpHeaderGoogleConfigProvider.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/HttpHeaderGoogleConfigProvider.java @@ -53,8 +53,7 @@ public class HttpHeaderGoogleConfigProvider implements GoogleConfigProvider { } String value = request.getHeader(key); if (!StringUtils.hasLength(value) && mandatory) { - throw new IllegalArgumentException( - "Missing required header in HttpServletRequest: " + key); + throw new IllegalArgumentException("Missing required header in HttpServletRequest: " + key); } return value; } 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)); + } + +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepositoryTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepositoryTests.java index e6377b13..b0528e73 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepositoryTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepositoryTests.java @@ -53,8 +53,8 @@ public class GoogleSecretManagerEnvironmentRepositoryTests { GoogleSecretManagerEnvironmentProperties properties = new GoogleSecretManagerEnvironmentProperties(); SecretManagerServiceClient mock = mock(SecretManagerServiceClient.class); properties.setVersion(1); - assertThat(GoogleSecretManagerAccessStrategyFactory.forVersion(null, null, - properties, mock) instanceof GoogleSecretManagerV1AccessStrategy).isTrue(); + assertThat(GoogleSecretManagerAccessStrategyFactory.forVersion(null, null, properties, + mock) instanceof GoogleSecretManagerV1AccessStrategy).isTrue(); } @Test(expected = IllegalArgumentException.class) @@ -70,19 +70,16 @@ public class GoogleSecretManagerEnvironmentRepositoryTests { public void testGetSecrets() throws IOException { RestTemplate rest = mock(RestTemplate.class); GoogleConfigProvider provider = mock(HttpHeaderGoogleConfigProvider.class); - when(provider.getValue(HttpHeaderGoogleConfigProvider.PROJECT_ID_HEADER, true)) - .thenReturn("test-project"); + when(provider.getValue(HttpHeaderGoogleConfigProvider.PROJECT_ID_HEADER, true)).thenReturn("test-project"); SecretManagerServiceClient mock = mock(SecretManagerServiceClient.class); SecretManagerServiceClient.ListSecretsPagedResponse response = mock( SecretManagerServiceClient.ListSecretsPagedResponse.class); - Secret secret = Secret.newBuilder().setName("projects/test-project/secrets/test") - .build(); + Secret secret = Secret.newBuilder().setName("projects/test-project/secrets/test").build(); List secrets = new ArrayList(); secrets.add(secret); when(response.iterateAll()).thenReturn(secrets); Mockito.doReturn(response).when(mock).listSecrets(any(ListSecretsRequest.class)); - GoogleSecretManagerV1AccessStrategy strategy = new GoogleSecretManagerV1AccessStrategy( - rest, provider, mock); + GoogleSecretManagerV1AccessStrategy strategy = new GoogleSecretManagerV1AccessStrategy(rest, provider, mock); assertThat(strategy.getSecrets().size()).isEqualTo(1); } @@ -91,27 +88,21 @@ public class GoogleSecretManagerEnvironmentRepositoryTests { public void testGetSecretValues() throws IOException { RestTemplate rest = mock(RestTemplate.class); GoogleConfigProvider provider = mock(HttpHeaderGoogleConfigProvider.class); - when(provider.getValue(HttpHeaderGoogleConfigProvider.PROJECT_ID_HEADER, true)) - .thenReturn("test-project"); + when(provider.getValue(HttpHeaderGoogleConfigProvider.PROJECT_ID_HEADER, true)).thenReturn("test-project"); SecretManagerServiceClient mock = mock(SecretManagerServiceClient.class); SecretManagerServiceClient.ListSecretVersionsPagedResponse response = mock( SecretManagerServiceClient.ListSecretVersionsPagedResponse.class); - SecretVersion secret1 = SecretVersion.newBuilder() - .setName("projects/test-project/secrets/test/versions/1") + SecretVersion secret1 = SecretVersion.newBuilder().setName("projects/test-project/secrets/test/versions/1") .setState(SecretVersion.State.ENABLED).build(); - SecretVersion secret2 = SecretVersion.newBuilder() - .setName("projects/test-project/secrets/test/versions/2") + SecretVersion secret2 = SecretVersion.newBuilder().setName("projects/test-project/secrets/test/versions/2") .setState(SecretVersion.State.DISABLED).build(); List secrets = new ArrayList(); secrets.add(secret1); secrets.add(secret2); when(response.iterateAll()).thenReturn(secrets); - Mockito.doReturn(response).when(mock) - .listSecretVersions(any(ListSecretVersionsRequest.class)); - GoogleSecretManagerV1AccessStrategy strategy = new GoogleSecretManagerV1AccessStrategy( - rest, provider, mock); - AccessSecretVersionResponse accessSecretVersionResponse = mock( - AccessSecretVersionResponse.class); + Mockito.doReturn(response).when(mock).listSecretVersions(any(ListSecretVersionsRequest.class)); + GoogleSecretManagerV1AccessStrategy strategy = new GoogleSecretManagerV1AccessStrategy(rest, provider, mock); + AccessSecretVersionResponse accessSecretVersionResponse = mock(AccessSecretVersionResponse.class); SecretPayload payload = mock(SecretPayload.class); ByteString data = mock(ByteString.class); when(accessSecretVersionResponse.getPayload()).thenReturn(payload); @@ -119,19 +110,15 @@ public class GoogleSecretManagerEnvironmentRepositoryTests { when(data.toStringUtf8()).thenReturn("test-value"); ArgumentMatcher matcher = new ArgumentMatcher() { @Override - public boolean matches( - AccessSecretVersionRequest accessSecretVersionRequest) { - if (accessSecretVersionRequest.getName() - .equals("projects/test-project/secrets/test/versions/1")) { + public boolean matches(AccessSecretVersionRequest accessSecretVersionRequest) { + if (accessSecretVersionRequest.getName().equals("projects/test-project/secrets/test/versions/1")) { return true; } return false; } }; - Mockito.doReturn(accessSecretVersionResponse).when(mock) - .accessSecretVersion(ArgumentMatchers.argThat(matcher)); - assertThat(strategy.getSecretValue( - Secret.newBuilder().setName("projects/test-project/secrets/test").build(), + Mockito.doReturn(accessSecretVersionResponse).when(mock).accessSecretVersion(ArgumentMatchers.argThat(matcher)); + assertThat(strategy.getSecretValue(Secret.newBuilder().setName("projects/test-project/secrets/test").build(), new GoogleSecretComparatorByVersion())).isEqualTo("test-value"); }