diff --git a/docs/src/main/asciidoc/parameter-store.adoc b/docs/src/main/asciidoc/parameter-store.adoc index beb06202c..125d245f8 100644 --- a/docs/src/main/asciidoc/parameter-store.adoc +++ b/docs/src/main/asciidoc/parameter-store.adoc @@ -88,3 +88,19 @@ turn on `DEBUG` logging on `org.springframework.cloud.aws.paramstore.AwsParamSto logging.level.org.springframework.cloud.aws.paramstore.AwsParamStorePropertySource=debug ---- ==== + +In `spring-cloud` `2020.0.0` (aka Ilford), the bootstrap phase is no longer enabled by default. In order +enable it you need an additional dependency: + +[source,xml,indent=0] +---- + + org.springframework.cloud + spring-cloud-starter-bootstrap + {spring-cloud-version} + +---- + +However, starting at `spring-cloud-aws` `2.3`, allows import default aws' parameterstore keys +(`spring.config.import=aws-parameterstore:`) or individual keys +(`spring.config.import=aws-parameterstore:config-key;other-config-key`) diff --git a/pom.xml b/pom.xml index 8cc80276a..7740a31a7 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ org.springframework.cloud spring-cloud-build - 3.0.0-M4 + 3.0.0-SNAPSHOT @@ -48,7 +48,7 @@ 1.5.5 2.8.2 1.2.0 - 3.0.0-M4 + 3.0.0-SNAPSHOT 0.0.25 diff --git a/spring-cloud-aws-dependencies/pom.xml b/spring-cloud-aws-dependencies/pom.xml index e1e05c0a9..186003c79 100644 --- a/spring-cloud-aws-dependencies/pom.xml +++ b/spring-cloud-aws-dependencies/pom.xml @@ -22,7 +22,7 @@ org.springframework.cloud spring-cloud-dependencies-parent - 2.3.2.BUILD-SNAPSHOT + 3.0.0-SNAPSHOT spring-cloud-aws-dependencies diff --git a/spring-cloud-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/paramstore/AwsParamStorePropertySource.java b/spring-cloud-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/paramstore/AwsParamStorePropertySource.java index 087d147e9..7d2b29d86 100644 --- a/spring-cloud-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/paramstore/AwsParamStorePropertySource.java +++ b/spring-cloud-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/paramstore/AwsParamStorePropertySource.java @@ -34,15 +34,16 @@ * from the AWS Parameter Store using the provided SSM client. * * @author Joris Kuipers + * @author Eddú Meléndez * @since 2.0.0 */ public class AwsParamStorePropertySource extends EnumerablePropertySource { private static final Logger LOGGER = LoggerFactory.getLogger(AwsParamStorePropertySource.class); - private String context; + private final String context; - private Map properties = new LinkedHashMap<>(); + private final Map properties = new LinkedHashMap<>(); public AwsParamStorePropertySource(String context, AWSSimpleSystemsManagement ssmClient) { super(context, ssmClient); @@ -57,21 +58,21 @@ public void init() { @Override public String[] getPropertyNames() { - Set strings = properties.keySet(); + Set strings = this.properties.keySet(); return strings.toArray(new String[strings.size()]); } @Override public Object getProperty(String name) { - return properties.get(name); + return this.properties.get(name); } private void getParameters(GetParametersByPathRequest paramsRequest) { - GetParametersByPathResult paramsResult = source.getParametersByPath(paramsRequest); + GetParametersByPathResult paramsResult = this.source.getParametersByPath(paramsRequest); for (Parameter parameter : paramsResult.getParameters()) { - String key = parameter.getName().replace(context, "").replace('/', '.'); + String key = parameter.getName().replace(this.context, "").replace('/', '.'); LOGGER.debug("Populating property retrieved from AWS Parameter Store: {}", key); - properties.put(key, parameter.getValue()); + this.properties.put(key, parameter.getValue()); } if (paramsResult.getNextToken() != null) { getParameters(paramsRequest.withNextToken(paramsResult.getNextToken())); diff --git a/spring-cloud-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/paramstore/AwsParamStorePropertySourceLocator.java b/spring-cloud-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/paramstore/AwsParamStorePropertySourceLocator.java index c15d1d58a..acbf42b9d 100644 --- a/spring-cloud-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/paramstore/AwsParamStorePropertySourceLocator.java +++ b/spring-cloud-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/paramstore/AwsParamStorePropertySourceLocator.java @@ -31,7 +31,6 @@ import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.env.PropertySource; -import org.springframework.util.ReflectionUtils; /** * Builds a {@link CompositePropertySource} with various @@ -43,6 +42,7 @@ * * @author Joris Kuipers * @author Matej Nedic + * @author Eddú Meléndez * @since 2.0.0 */ public class AwsParamStorePropertySourceLocator implements PropertySourceLocator { @@ -73,56 +73,22 @@ public PropertySource locate(Environment environment) { ConfigurableEnvironment env = (ConfigurableEnvironment) environment; - String appName = properties.getName(); - - if (appName == null) { - appName = env.getProperty("spring.application.name"); - } + AwsParamStorePropertySources sources = new AwsParamStorePropertySources(this.properties, this.logger); List profiles = Arrays.asList(env.getActiveProfiles()); - - String prefix = this.properties.getPrefix(); - - String appContext = prefix + "/" + appName; - addProfiles(this.contexts, appContext, profiles); - this.contexts.add(appContext + "/"); - - String defaultContext = prefix + "/" + this.properties.getDefaultContext(); - addProfiles(this.contexts, defaultContext, profiles); - this.contexts.add(defaultContext + "/"); + this.contexts.addAll(sources.getAutomaticContexts(profiles)); CompositePropertySource composite = new CompositePropertySource("aws-param-store"); for (String propertySourceContext : this.contexts) { - try { - composite.addPropertySource(create(propertySourceContext)); - } - catch (Exception e) { - if (this.properties.isFailFast()) { - logger.error( - "Fail fast is set and there was an error reading configuration from AWS Parameter Store:\n" - + e.getMessage()); - ReflectionUtils.rethrowRuntimeException(e); - } - else { - logger.warn("Unable to load AWS config from " + propertySourceContext, e); - } + PropertySource propertySource = sources + .createPropertySource(propertySourceContext, !this.properties.isFailFast(), this.ssmClient); + if (propertySource != null) { + composite.addPropertySource(propertySource); } } return composite; } - private AwsParamStorePropertySource create(String context) { - AwsParamStorePropertySource propertySource = new AwsParamStorePropertySource(context, this.ssmClient); - propertySource.init(); - return propertySource; - } - - private void addProfiles(Set contexts, String baseContext, List profiles) { - for (String profile : profiles) { - contexts.add(baseContext + this.properties.getProfileSeparator() + profile + "/"); - } - } - } diff --git a/spring-cloud-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/paramstore/AwsParamStorePropertySources.java b/spring-cloud-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/paramstore/AwsParamStorePropertySources.java new file mode 100644 index 000000000..d994ac538 --- /dev/null +++ b/spring-cloud-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/paramstore/AwsParamStorePropertySources.java @@ -0,0 +1,107 @@ +/* + * 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.aws.paramstore; + +import java.util.ArrayList; +import java.util.List; + +import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagement; +import org.apache.commons.logging.Log; + +import org.springframework.util.StringUtils; + +/** + * @author Eddú Meléndez + * @since 2.3 + */ +public class AwsParamStorePropertySources { + + private final AwsParamStoreProperties properties; + + private final Log log; + + public AwsParamStorePropertySources(AwsParamStoreProperties properties, Log log) { + this.properties = properties; + this.log = log; + } + + public List getAutomaticContexts(List profiles) { + List contexts = new ArrayList<>(); + String prefix = this.properties.getPrefix(); + String defaultContext = getContext(prefix, this.properties.getDefaultContext()); + + String appName = this.properties.getName(); + + String appContext = prefix + "/" + appName; + addProfiles(contexts, appContext, profiles); + contexts.add(appContext + "/"); + + addProfiles(contexts, defaultContext, profiles); + contexts.add(defaultContext + "/"); + return contexts; + } + + protected String getContext(String prefix, String context) { + if (StringUtils.hasLength(prefix)) { + return prefix + "/" + context; + } + return context; + } + + private void addProfiles(List contexts, String baseContext, List profiles) { + for (String profile : profiles) { + contexts.add(baseContext + this.properties.getProfileSeparator() + profile + "/"); + } + } + + /** + * Creates property source for given context. + * @param context property source context equivalent to the parameter name + * @param optional if creating context should fail with exception if parameter cannot + * be loaded + * @param client System Manager Management client + * @return a property source or null if parameter could not be loaded and optional is + * set to true + */ + public AwsParamStorePropertySource createPropertySource(String context, boolean optional, + AWSSimpleSystemsManagement client) { + try { + AwsParamStorePropertySource propertySource = new AwsParamStorePropertySource(context, client); + propertySource.init(); + return propertySource; + // TODO: howto call close when /refresh + } + catch (Exception e) { + if (!optional) { + throw new AwsParameterPropertySourceNotFoundException(e); + } + else { + log.warn("Unable to load AWS parameter from " + context + ". " + e.getMessage()); + } + } + return null; + } + + static class AwsParameterPropertySourceNotFoundException extends RuntimeException { + + AwsParameterPropertySourceNotFoundException(Exception source) { + super(source); + } + + } + +} diff --git a/spring-cloud-aws-parameter-store-config/src/test/java/org/springframework/cloud/aws/paramstore/AwsParamStorePropertySourceLocatorTest.java b/spring-cloud-aws-parameter-store-config/src/test/java/org/springframework/cloud/aws/paramstore/AwsParamStorePropertySourceLocatorTest.java index 7d3087e4a..afc939acc 100644 --- a/spring-cloud-aws-parameter-store-config/src/test/java/org/springframework/cloud/aws/paramstore/AwsParamStorePropertySourceLocatorTest.java +++ b/spring-cloud-aws-parameter-store-config/src/test/java/org/springframework/cloud/aws/paramstore/AwsParamStorePropertySourceLocatorTest.java @@ -25,9 +25,12 @@ import com.amazonaws.services.simplesystemsmanagement.model.Parameter; import org.junit.jupiter.api.Test; +import org.springframework.cloud.aws.paramstore.AwsParamStorePropertySources.AwsParameterPropertySourceNotFoundException; +import org.springframework.core.env.CompositePropertySource; import org.springframework.mock.env.MockEnvironment; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -36,6 +39,7 @@ * Unit test for {@link AwsParamStorePropertySourceLocator}. * * @author Matej Nedic + * @author Eddú Meléndez */ public class AwsParamStorePropertySourceLocatorTest { @@ -45,48 +49,54 @@ public class AwsParamStorePropertySourceLocatorTest { @Test void contextExpectedToHave2Elements() { - AwsParamStoreProperties properties = new AwsParamStorePropertiesBuilder().withDefaultContext("application") - .withName("application").build(); + AwsParamStoreProperties properties = new AwsParamStoreProperties(); + properties.setPrefix("application"); + properties.setName("application"); GetParametersByPathResult firstResult = getFirstResult(); GetParametersByPathResult nextResult = getNextResult(); - when(ssmClient.getParametersByPath(any(GetParametersByPathRequest.class))).thenReturn(firstResult, nextResult); + when(this.ssmClient.getParametersByPath(any(GetParametersByPathRequest.class))).thenReturn(firstResult, + nextResult); - AwsParamStorePropertySourceLocator locator = new AwsParamStorePropertySourceLocator(ssmClient, properties); - env.setActiveProfiles("test"); - locator.locate(env); + AwsParamStorePropertySourceLocator locator = new AwsParamStorePropertySourceLocator(this.ssmClient, properties); + this.env.setActiveProfiles("test"); + locator.locate(this.env); assertThat(locator.getContexts()).hasSize(2); } @Test void contextExpectedToHave4Elements() { - AwsParamStoreProperties properties = new AwsParamStorePropertiesBuilder().withDefaultContext("application") - .withName("messaging-service").build(); + AwsParamStoreProperties properties = new AwsParamStoreProperties(); + properties.setPrefix("application"); + properties.setName("messaging-service"); GetParametersByPathResult firstResult = getFirstResult(); GetParametersByPathResult nextResult = getNextResult(); - when(ssmClient.getParametersByPath(any(GetParametersByPathRequest.class))).thenReturn(firstResult, nextResult); + when(this.ssmClient.getParametersByPath(any(GetParametersByPathRequest.class))).thenReturn(firstResult, + nextResult); - AwsParamStorePropertySourceLocator locator = new AwsParamStorePropertySourceLocator(ssmClient, properties); - env.setActiveProfiles("test"); - locator.locate(env); + AwsParamStorePropertySourceLocator locator = new AwsParamStorePropertySourceLocator(this.ssmClient, properties); + this.env.setActiveProfiles("test"); + locator.locate(this.env); assertThat(locator.getContexts()).hasSize(4); } @Test void contextSpecificOrderExpected() { - AwsParamStoreProperties properties = new AwsParamStorePropertiesBuilder().withDefaultContext("application") - .withName("messaging-service").build(); + AwsParamStoreProperties properties = new AwsParamStoreProperties(); + properties.setPrefix("application"); + properties.setName("messaging-service"); GetParametersByPathResult firstResult = getFirstResult(); GetParametersByPathResult nextResult = getNextResult(); - when(ssmClient.getParametersByPath(any(GetParametersByPathRequest.class))).thenReturn(firstResult, nextResult); + when(this.ssmClient.getParametersByPath(any(GetParametersByPathRequest.class))).thenReturn(firstResult, + nextResult); - AwsParamStorePropertySourceLocator locator = new AwsParamStorePropertySourceLocator(ssmClient, properties); - env.setActiveProfiles("test"); - locator.locate(env); + AwsParamStorePropertySourceLocator locator = new AwsParamStorePropertySourceLocator(this.ssmClient, properties); + this.env.setActiveProfiles("test"); + locator.locate(this.env); List contextToBeTested = new ArrayList<>(locator.getContexts()); @@ -96,6 +106,28 @@ void contextSpecificOrderExpected() { assertThat(contextToBeTested.get(3)).isEqualTo("application/application/"); } + @Test + void whenFailFastIsTrueAndParameterDoesNotExistThrowsException() { + AwsParamStoreProperties properties = new AwsParamStoreProperties(); + properties.setFailFast(true); + + AwsParamStorePropertySourceLocator locator = new AwsParamStorePropertySourceLocator(this.ssmClient, properties); + assertThatThrownBy(() -> locator.locate(this.env)) + .isInstanceOf(AwsParameterPropertySourceNotFoundException.class); + } + + @Test + void whenFailFastIsFalseAndParameterDoesNotExistReturnsEmptyPropertySource() { + AwsParamStoreProperties properties = new AwsParamStoreProperties(); + properties.setFailFast(false); + + AwsParamStorePropertySourceLocator locator = new AwsParamStorePropertySourceLocator(this.ssmClient, properties); + + CompositePropertySource result = (CompositePropertySource) locator.locate(this.env); + + assertThat(result.getPropertySources()).isEmpty(); + } + private static GetParametersByPathResult getNextResult() { return new GetParametersByPathResult().withParameters( new Parameter().withName("/config/myservice/key3").withValue("value3"), @@ -108,27 +140,4 @@ private static GetParametersByPathResult getFirstResult() { new Parameter().withName("/config/myservice/key4").withValue("value4")); } - private static final class AwsParamStorePropertiesBuilder { - - private final AwsParamStoreProperties properties = new AwsParamStoreProperties(); - - private AwsParamStorePropertiesBuilder() { - } - - public AwsParamStorePropertiesBuilder withDefaultContext(String defaultContext) { - this.properties.setPrefix(defaultContext); - return this; - } - - public AwsParamStorePropertiesBuilder withName(String name) { - this.properties.setName(name); - return this; - } - - public AwsParamStoreProperties build() { - return this.properties; - } - - } - } diff --git a/spring-cloud-starter-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/autoconfigure/paramstore/AwsParamStoreBootstrapConfiguration.java b/spring-cloud-starter-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/autoconfigure/paramstore/AwsParamStoreBootstrapConfiguration.java index 68d44e458..1d1487257 100644 --- a/spring-cloud-starter-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/autoconfigure/paramstore/AwsParamStoreBootstrapConfiguration.java +++ b/spring-cloud-starter-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/autoconfigure/paramstore/AwsParamStoreBootstrapConfiguration.java @@ -30,6 +30,7 @@ import org.springframework.cloud.aws.paramstore.AwsParamStorePropertySourceLocator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; /** * Spring Cloud Bootstrap Configuration for setting up an @@ -46,15 +47,28 @@ @ConditionalOnProperty(prefix = AwsParamStoreProperties.CONFIG_PREFIX, name = "enabled", matchIfMissing = true) public class AwsParamStoreBootstrapConfiguration { + private final Environment environment; + + public AwsParamStoreBootstrapConfiguration(Environment environment) { + this.environment = environment; + } + @Bean AwsParamStorePropertySourceLocator awsParamStorePropertySourceLocator(AWSSimpleSystemsManagement ssmClient, AwsParamStoreProperties properties) { + if (StringUtils.isNullOrEmpty(properties.getName())) { + properties.setName(this.environment.getProperty("spring.application.name")); + } return new AwsParamStorePropertySourceLocator(ssmClient, properties); } @Bean @ConditionalOnMissingBean AWSSimpleSystemsManagement ssmClient(AwsParamStoreProperties properties) { + return createSimpleSystemManagementClient(properties); + } + + public static AWSSimpleSystemsManagement createSimpleSystemManagementClient(AwsParamStoreProperties properties) { AWSSimpleSystemsManagementClientBuilder builder = AWSSimpleSystemsManagementClientBuilder.standard() .withClientConfiguration(SpringCloudClientConfiguration.getClientConfiguration()); if (!StringUtils.isNullOrEmpty(properties.getRegion())) { diff --git a/spring-cloud-starter-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/autoconfigure/paramstore/AwsParamStoreConfigDataLoader.java b/spring-cloud-starter-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/autoconfigure/paramstore/AwsParamStoreConfigDataLoader.java new file mode 100644 index 000000000..b954e0820 --- /dev/null +++ b/spring-cloud-starter-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/autoconfigure/paramstore/AwsParamStoreConfigDataLoader.java @@ -0,0 +1,53 @@ +/* + * 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.aws.autoconfigure.paramstore; + +import java.util.Collections; + +import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagement; + +import org.springframework.boot.context.config.ConfigData; +import org.springframework.boot.context.config.ConfigDataLoader; +import org.springframework.boot.context.config.ConfigDataLoaderContext; +import org.springframework.boot.context.config.ConfigDataResourceNotFoundException; +import org.springframework.cloud.aws.paramstore.AwsParamStorePropertySource; + +/** + * @author Eddú Meléndez + * @since 2.3.0 + */ +public class AwsParamStoreConfigDataLoader implements ConfigDataLoader { + + @Override + public ConfigData load(ConfigDataLoaderContext context, AwsParamStoreConfigDataResource resource) { + try { + AWSSimpleSystemsManagement ssm = context.getBootstrapContext().get(AWSSimpleSystemsManagement.class); + AwsParamStorePropertySource propertySource = resource.getPropertySources() + .createPropertySource(resource.getContext(), resource.isOptional(), ssm); + if (propertySource != null) { + return new ConfigData(Collections.singletonList(propertySource)); + } + else { + return null; + } + } + catch (Exception e) { + throw new ConfigDataResourceNotFoundException(resource, e); + } + } + +} diff --git a/spring-cloud-starter-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/autoconfigure/paramstore/AwsParamStoreConfigDataLocationResolver.java b/spring-cloud-starter-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/autoconfigure/paramstore/AwsParamStoreConfigDataLocationResolver.java new file mode 100644 index 000000000..0661c5061 --- /dev/null +++ b/spring-cloud-starter-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/autoconfigure/paramstore/AwsParamStoreConfigDataLocationResolver.java @@ -0,0 +1,144 @@ +/* + * 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.aws.autoconfigure.paramstore; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagement; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.BootstrapContext; +import org.springframework.boot.BootstrapRegistry; +import org.springframework.boot.ConfigurableBootstrapContext; +import org.springframework.boot.context.config.ConfigDataLocation; +import org.springframework.boot.context.config.ConfigDataLocationNotFoundException; +import org.springframework.boot.context.config.ConfigDataLocationResolver; +import org.springframework.boot.context.config.ConfigDataLocationResolverContext; +import org.springframework.boot.context.config.Profiles; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.cloud.aws.paramstore.AwsParamStoreProperties; +import org.springframework.cloud.aws.paramstore.AwsParamStorePropertySources; +import org.springframework.util.StringUtils; + +/** + * @author Eddú Meléndez + * @since 2.3.0 + */ +public class AwsParamStoreConfigDataLocationResolver + implements ConfigDataLocationResolver { + + /** + * AWS ParameterStore Config Data prefix. + */ + public static final String PREFIX = "aws-parameterstore:"; + + private final Log log = LogFactory.getLog(AwsParamStoreConfigDataLocationResolver.class); + + @Override + public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) { + if (!location.hasPrefix(PREFIX)) { + return false; + } + return context.getBinder().bind(AwsParamStoreProperties.CONFIG_PREFIX + ".enabled", Boolean.class).orElse(true); + } + + @Override + public List resolve(ConfigDataLocationResolverContext context, + ConfigDataLocation location) throws ConfigDataLocationNotFoundException { + return Collections.emptyList(); + } + + @Override + public List resolveProfileSpecific( + ConfigDataLocationResolverContext resolverContext, ConfigDataLocation location, Profiles profiles) + throws ConfigDataLocationNotFoundException { + registerBean(resolverContext, AwsParamStoreProperties.class, loadProperties(resolverContext.getBinder())); + + registerAndPromoteBean(resolverContext, AWSSimpleSystemsManagement.class, + this::createSimpleSystemManagementClient); + + AwsParamStoreProperties properties = loadConfigProperties(resolverContext.getBinder()); + + AwsParamStorePropertySources sources = new AwsParamStorePropertySources(properties, log); + + List contexts = location.getValue().equals(PREFIX) + ? sources.getAutomaticContexts(profiles.getAccepted()) + : getCustomContexts(location.getNonPrefixedValue(PREFIX)); + + List locations = new ArrayList<>(); + contexts.forEach(propertySourceContext -> locations + .add(new AwsParamStoreConfigDataResource(propertySourceContext, location.isOptional(), sources))); + + return locations; + } + + private List getCustomContexts(String keys) { + if (StringUtils.hasLength(keys)) { + return Arrays.asList(keys.split(";")); + } + return Collections.emptyList(); + } + + protected void registerAndPromoteBean(ConfigDataLocationResolverContext context, Class type, + BootstrapRegistry.InstanceSupplier supplier) { + registerBean(context, type, supplier); + context.getBootstrapContext().addCloseListener(event -> { + T instance = event.getBootstrapContext().get(type); + event.getApplicationContext().getBeanFactory().registerSingleton("configData" + type.getSimpleName(), + instance); + }); + } + + public void registerBean(ConfigDataLocationResolverContext context, Class type, T instance) { + context.getBootstrapContext().registerIfAbsent(type, BootstrapRegistry.InstanceSupplier.of(instance)); + } + + protected void registerBean(ConfigDataLocationResolverContext context, Class type, + BootstrapRegistry.InstanceSupplier supplier) { + ConfigurableBootstrapContext bootstrapContext = context.getBootstrapContext(); + bootstrapContext.registerIfAbsent(type, supplier); + } + + protected AWSSimpleSystemsManagement createSimpleSystemManagementClient(BootstrapContext context) { + AwsParamStoreProperties properties = context.get(AwsParamStoreProperties.class); + + return AwsParamStoreBootstrapConfiguration.createSimpleSystemManagementClient(properties); + } + + protected AwsParamStoreProperties loadProperties(Binder binder) { + return binder.bind(AwsParamStoreProperties.CONFIG_PREFIX, Bindable.of(AwsParamStoreProperties.class)) + .orElseGet(AwsParamStoreProperties::new); + } + + protected AwsParamStoreProperties loadConfigProperties(Binder binder) { + AwsParamStoreProperties properties = binder + .bind(AwsParamStoreProperties.CONFIG_PREFIX, Bindable.of(AwsParamStoreProperties.class)) + .orElse(new AwsParamStoreProperties()); + + if (!StringUtils.hasLength(properties.getName())) { + properties.setName(binder.bind("spring.application.name", String.class).orElse("application")); + } + + return properties; + } + +} diff --git a/spring-cloud-starter-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/autoconfigure/paramstore/AwsParamStoreConfigDataResource.java b/spring-cloud-starter-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/autoconfigure/paramstore/AwsParamStoreConfigDataResource.java new file mode 100644 index 000000000..625526895 --- /dev/null +++ b/spring-cloud-starter-aws-parameter-store-config/src/main/java/org/springframework/cloud/aws/autoconfigure/paramstore/AwsParamStoreConfigDataResource.java @@ -0,0 +1,89 @@ +/* + * 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.aws.autoconfigure.paramstore; + +import java.util.Objects; + +import org.springframework.boot.context.config.ConfigDataResource; +import org.springframework.cloud.aws.paramstore.AwsParamStorePropertySources; +import org.springframework.core.style.ToStringCreator; + +/** + * Config data resource for AWS System Manager Management integration. + * + * @author Eddú Meléndez + * @since 2.3.0 + */ +public class AwsParamStoreConfigDataResource extends ConfigDataResource { + + private final String context; + + private final boolean optional; + + private final AwsParamStorePropertySources propertySources; + + public AwsParamStoreConfigDataResource(String context, boolean optional, + AwsParamStorePropertySources propertySources) { + this.context = context; + this.optional = optional; + this.propertySources = propertySources; + } + + /** + * Returns context which is equal to Secret Manager secret name. + * @return the context + */ + public String getContext() { + return this.context; + } + + /** + * If application startup should fail when secret cannot be loaded or does not exist. + * @return is optional + */ + public boolean isOptional() { + return this.optional; + } + + public AwsParamStorePropertySources getPropertySources() { + return this.propertySources; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AwsParamStoreConfigDataResource that = (AwsParamStoreConfigDataResource) o; + return this.optional == that.optional && this.context.equals(that.context); + } + + @Override + public int hashCode() { + return Objects.hash(this.optional, this.context); + } + + @Override + public String toString() { + return new ToStringCreator(this).append("context", context).append("optional", optional).toString(); + + } + +} diff --git a/spring-cloud-starter-aws-parameter-store-config/src/main/resources/META-INF/spring.factories b/spring-cloud-starter-aws-parameter-store-config/src/main/resources/META-INF/spring.factories index 3d915d8a6..1bec81093 100644 --- a/spring-cloud-starter-aws-parameter-store-config/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-starter-aws-parameter-store-config/src/main/resources/META-INF/spring.factories @@ -1,2 +1,10 @@ org.springframework.cloud.bootstrap.BootstrapConfiguration=\ org.springframework.cloud.aws.autoconfigure.paramstore.AwsParamStoreBootstrapConfiguration + +# ConfigData Location Resolvers +org.springframework.boot.context.config.ConfigDataLocationResolver=\ +org.springframework.cloud.aws.autoconfigure.paramstore.AwsParamStoreConfigDataLocationResolver + +# ConfigData Loaders +org.springframework.boot.context.config.ConfigDataLoader=\ +org.springframework.cloud.aws.autoconfigure.paramstore.AwsParamStoreConfigDataLoader diff --git a/spring-cloud-starter-aws-parameter-store-config/src/test/java/org/springframework/cloud/aws/autoconfigure/paramstore/AwsParamStoreConfigDataLocationResolverTest.java b/spring-cloud-starter-aws-parameter-store-config/src/test/java/org/springframework/cloud/aws/autoconfigure/paramstore/AwsParamStoreConfigDataLocationResolverTest.java new file mode 100644 index 000000000..bf5579c60 --- /dev/null +++ b/spring-cloud-starter-aws-parameter-store-config/src/test/java/org/springframework/cloud/aws/autoconfigure/paramstore/AwsParamStoreConfigDataLocationResolverTest.java @@ -0,0 +1,91 @@ +/* + * 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.aws.autoconfigure.paramstore; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.BootstrapRegistry; +import org.springframework.boot.context.config.ConfigDataLocation; +import org.springframework.boot.context.config.ConfigDataLocationResolverContext; +import org.springframework.boot.context.config.Profiles; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class AwsParamStoreConfigDataLocationResolverTest { + + @Test + void testResolveProfileSpecificWithAutomaticPaths() { + String location = "aws-parameterstore:"; + List locations = testResolveProfileSpecific(location); + assertThat(locations).hasSize(4); + assertThat(toContexts(locations)).containsExactly("/config/testapp_dev/", "/config/testapp/", + "/config/application_dev/", "/config/application/"); + } + + @Test + void testResolveProfileSpecificWithCustomPaths() { + String location = "aws-parameterstore:/mypath1;/mypath2;/mypath3"; + List locations = testResolveProfileSpecific(location); + assertThat(locations).hasSize(3); + assertThat(toContexts(locations)).containsExactly("/mypath1", "/mypath2", "/mypath3"); + } + + private List toContexts(List locations) { + return locations.stream().map(AwsParamStoreConfigDataResource::getContext).collect(Collectors.toList()); + } + + private List testResolveProfileSpecific(String location) { + AwsParamStoreConfigDataLocationResolver resolver = createResolver(); + ConfigDataLocationResolverContext context = mock(ConfigDataLocationResolverContext.class); + MockEnvironment env = new MockEnvironment(); + env.setProperty("spring.application.name", "testapp"); + when(context.getBinder()).thenReturn(Binder.get(env)); + Profiles profiles = mock(Profiles.class); + when(profiles.getAccepted()).thenReturn(Collections.singletonList("dev")); + return resolver.resolveProfileSpecific(context, ConfigDataLocation.of(location), profiles); + } + + private AwsParamStoreConfigDataLocationResolver createResolver() { + return new AwsParamStoreConfigDataLocationResolver() { + @Override + public void registerBean(ConfigDataLocationResolverContext context, Class type, T instance) { + // do nothing + } + + @Override + protected void registerBean(ConfigDataLocationResolverContext context, Class type, + BootstrapRegistry.InstanceSupplier supplier) { + // do nothing + } + + @Override + protected void registerAndPromoteBean(ConfigDataLocationResolverContext context, Class type, + BootstrapRegistry.InstanceSupplier supplier) { + // do nothing + } + }; + } + +}