diff --git a/docs/pom.xml b/docs/pom.xml index eac4c560e..6e2133981 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -22,10 +22,10 @@ org.springframework.cloud spring-cloud-aws - 2.3.0.BUILD-SNAPSHOT + 3.0.0-SNAPSHOT spring-cloud-aws-docs - pom + jar Spring Cloud AWS Docs Spring Cloud AWS Docs @@ -86,6 +86,10 @@ org.apache.maven.plugins maven-antrun-plugin + + org.codehaus.mojo + build-helper-maven-plugin + maven-deploy-plugin diff --git a/pom.xml b/pom.xml index 23f1f1915..1e810bb9d 100644 --- a/pom.xml +++ b/pom.xml @@ -22,12 +22,12 @@ org.springframework.cloud spring-cloud-build - 2.3.2.BUILD-SNAPSHOT + 3.0.0-SNAPSHOT spring-cloud-aws - 2.3.0.BUILD-SNAPSHOT + 3.0.0-SNAPSHOT pom Spring Cloud AWS Spring Cloud AWS @@ -59,11 +59,13 @@ spring-cloud-aws-messaging spring-cloud-aws-autoconfigure spring-cloud-aws-parameter-store-config + spring-cloud-aws-appconfig spring-cloud-aws-secrets-manager-config spring-cloud-starter-aws spring-cloud-starter-aws-jdbc spring-cloud-starter-aws-messaging spring-cloud-starter-aws-parameter-store-config + spring-cloud-starter-aws-appconfig spring-cloud-starter-aws-secrets-manager-config spring-cloud-aws-integration-test docs diff --git a/spring-cloud-aws-appconfig/pom.xml b/spring-cloud-aws-appconfig/pom.xml new file mode 100644 index 000000000..6f43836b3 --- /dev/null +++ b/spring-cloud-aws-appconfig/pom.xml @@ -0,0 +1,49 @@ + + + + 4.0.0 + + org.springframework.cloud + spring-cloud-aws + 3.0.0-SNAPSHOT + + + spring-cloud-aws-appconfig + + Spring Cloud AWS AppConfig Configuration + Spring Cloud AWS AppConfig Configuration + + + + + org.springframework + spring-context + + + + org.springframework.cloud + spring-cloud-context + + + + com.amazonaws + aws-java-sdk-appconfig + + + diff --git a/spring-cloud-aws-appconfig/src/main/java/org/springframework/cloud/aws/appconfig/AwsAppConfigPropertySource.java b/spring-cloud-aws-appconfig/src/main/java/org/springframework/cloud/aws/appconfig/AwsAppConfigPropertySource.java new file mode 100644 index 000000000..052612664 --- /dev/null +++ b/spring-cloud-aws-appconfig/src/main/java/org/springframework/cloud/aws/appconfig/AwsAppConfigPropertySource.java @@ -0,0 +1,156 @@ +/* + * 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.appconfig; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; + +import com.amazonaws.services.appconfig.AmazonAppConfig; +import com.amazonaws.services.appconfig.model.GetConfigurationRequest; +import com.amazonaws.services.appconfig.model.GetConfigurationResult; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.util.ReflectionUtils; + +/** + * @author jarpz + */ +public class AwsAppConfigPropertySource extends EnumerablePropertySource { + + private static final String SUPPORTED_TYPE_JSON = "application/json"; + + private static final String SUPPORTED_TYPE_YAML = "application/x-yaml"; + + private final String clientId; + + private final String application; + + private final String configurationVersion; + + private final String environment; + + private Properties properties; + + public AwsAppConfigPropertySource(String name, String clientId, String application, String environment, + String configurationVersion, AmazonAppConfig appConfigClient) { + super(name, appConfigClient); + this.clientId = clientId; + this.application = application; + this.configurationVersion = configurationVersion; + this.environment = environment; + } + + public void init() { + GetConfigurationRequest request = new GetConfigurationRequest().withClientId(clientId) + .withApplication(application).withConfiguration(name) + .withClientConfigurationVersion(configurationVersion).withEnvironment(environment); + + getAppConfig(request); + } + + @Override + public String[] getPropertyNames() { + Set strings = properties.stringPropertyNames(); + return strings.toArray(new String[0]); + } + + @Override + public Object getProperty(String name) { + return properties.get(name); + } + + private void getAppConfig(GetConfigurationRequest request) { + GetConfigurationResult result = this.source.getConfiguration(request); + + logger.trace(String.format("loading file: %s/%s/%s/%s", application, name, environment, + result.getConfigurationVersion())); + + switch (result.getContentType()) { + case SUPPORTED_TYPE_YAML: + processYamlContent(result.getContent()); + break; + case SUPPORTED_TYPE_JSON: + processJsonContent(result.getContent()); + break; + default: + throw new IllegalStateException(String.format("Unsupported content type: %s", result.getContentType())); + } + } + + private void processYamlContent(ByteBuffer byteBuffer) { + YamlPropertiesFactoryBean bean = new YamlPropertiesFactoryBean(); + + bean.setResources(new ByteArrayResource(byteBuffer.array())); + + properties = bean.getObject(); + } + + private void processJsonContent(ByteBuffer byteBuffer) { + try { + Map map = new ObjectMapper().readValue(byteBuffer.array(), + new TypeReference>() { + }); + + Map result = new LinkedHashMap<>(); + this.flatten(null, result, map); + + properties = new Properties(); + properties.putAll(result); + } + catch (IOException ex) { + ReflectionUtils.rethrowRuntimeException(ex); + } + } + + /** + * flatten json structure. + */ + private void flatten(String prefix, Map result, Map map) { + String namePrefix = Objects.nonNull(prefix) ? prefix + "." : ""; + + map.forEach((key, value) -> this.extract(namePrefix + key, result, value)); + } + + private void extract(String name, Map result, Object value) { + if (value instanceof Map) { + this.flatten(name, result, (Map) value); + } + else if (value instanceof Collection) { + int index = 0; + + for (Iterator it = ((Collection) value).iterator(); it.hasNext(); ++index) { + Object object = it.next(); + this.extract(name + "[" + index + "]", result, object); + } + } + else { + result.put(name, value); + } + } + +} diff --git a/spring-cloud-aws-appconfig/src/main/java/org/springframework/cloud/aws/appconfig/AwsAppConfigPropertySourceLocator.java b/spring-cloud-aws-appconfig/src/main/java/org/springframework/cloud/aws/appconfig/AwsAppConfigPropertySourceLocator.java new file mode 100644 index 000000000..9eebf3cce --- /dev/null +++ b/spring-cloud-aws-appconfig/src/main/java/org/springframework/cloud/aws/appconfig/AwsAppConfigPropertySourceLocator.java @@ -0,0 +1,114 @@ +/* + * 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.appconfig; + +import java.util.Arrays; + +import com.amazonaws.services.appconfig.AmazonAppConfig; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.bootstrap.config.PropertySourceLocator; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertySource; +import org.springframework.util.ReflectionUtils; + +import static java.util.Objects.isNull; +import static org.springframework.util.Assert.hasText; + +/** + * @author jarpz + */ +public class AwsAppConfigPropertySourceLocator implements PropertySourceLocator { + + private final AmazonAppConfig appConfigClient; + + private final String clientId; + + private final String application; + + private final String configurationProfile; + + private final String environment; + + private final String configurationVersion; + + private final boolean failFast; + + private static final Log logger = LogFactory.getLog(AwsAppConfigPropertySourceLocator.class); + + public AwsAppConfigPropertySourceLocator(AmazonAppConfig appConfigClient, String clientId, String application, + String configurationProfile, String environment, String configurationVersion, boolean failFast) { + this.appConfigClient = appConfigClient; + this.clientId = clientId; + this.application = application; + this.configurationProfile = configurationProfile; + this.environment = environment; + this.configurationVersion = configurationVersion; + this.failFast = failFast; + } + + @Override + public PropertySource locate(Environment environment) { + if (!(environment instanceof ConfigurableEnvironment)) { + return null; + } + + ConfigurableEnvironment env = (ConfigurableEnvironment) environment; + + String appName = configurationProfile; + if (isNull(appName)) { + appName = env.getProperty("spring.application.name"); + } + String profile = this.environment; + if (isNull(profile)) { + profile = Arrays.stream(env.getActiveProfiles()).findFirst().orElse("default"); + } + + hasText(appName, "configurationProfile or spring.application.name should not be empty or null."); + hasText(profile, "environment or profiles should not be empty or null "); + + CompositePropertySource composite = new CompositePropertySource("aws-app-config"); + + try { + composite.addPropertySource(create(appName, profile)); + } + catch (Exception ex) { + if (failFast) { + logger.error("Fail fast is set and there was an error reading configuration from AWS AppConfig: {}", + ex); + ReflectionUtils.rethrowRuntimeException(ex); + } + else { + logger.warn("Unable to load AWS AppConfig from " + appName, ex); + } + } + + return composite; + } + + private AwsAppConfigPropertySource create(String appName, String profile) { + AwsAppConfigPropertySource propertySource = new AwsAppConfigPropertySource(appName, clientId, application, + profile, configurationVersion, appConfigClient); + propertySource.init(); + + return propertySource; + } + +} diff --git a/spring-cloud-aws-appconfig/src/test/java/org/springframework/cloud/aws/appconfig/AwsAppConfigPropertySourceLocatorTest.java b/spring-cloud-aws-appconfig/src/test/java/org/springframework/cloud/aws/appconfig/AwsAppConfigPropertySourceLocatorTest.java new file mode 100644 index 000000000..142bc79cd --- /dev/null +++ b/spring-cloud-aws-appconfig/src/test/java/org/springframework/cloud/aws/appconfig/AwsAppConfigPropertySourceLocatorTest.java @@ -0,0 +1,117 @@ +/* + * 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.appconfig; + +import java.nio.ByteBuffer; + +import com.amazonaws.services.appconfig.AmazonAppConfig; +import com.amazonaws.services.appconfig.model.GetConfigurationResult; +import org.apache.commons.codec.Resources; +import org.junit.jupiter.api.Test; + +import org.springframework.core.env.PropertySource; +import org.springframework.mock.env.MockEnvironment; + +import static com.amazonaws.util.IOUtils.toByteArray; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +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; + +public class AwsAppConfigPropertySourceLocatorTest { + + private AmazonAppConfig appConfigClient = mock(AmazonAppConfig.class); + + private MockEnvironment env = new MockEnvironment(); + + @Test + void whenLoadYamlAppConfigThenReturnPropertySource() throws Exception { + ByteBuffer content = ByteBuffer.wrap(toByteArray(Resources.getInputStream("config.yaml"))); + + GetConfigurationResult result = new GetConfigurationResult(); + result.setConfigurationVersion("1"); + result.setContentType("application/x-yaml"); + result.setContent(content); + + when(appConfigClient.getConfiguration(any())).thenReturn(result); + + AwsAppConfigPropertySourceLocator locator = new AwsAppConfigPropertySourceLocator(appConfigClient, "123456789", + "my-project", "api-my", "dev", null, false); + + env.setActiveProfiles("test"); + + PropertySource propertySource = locator.locate(env); + + assertThat(propertySource.getProperty("server.port")).isEqualTo(8081); + } + + @Test + void whenLoadJsonAppConfigThenReturnPropertySource() throws Exception { + ByteBuffer content = ByteBuffer.wrap(toByteArray(Resources.getInputStream("config.json"))); + + GetConfigurationResult result = new GetConfigurationResult(); + result.setConfigurationVersion("1"); + result.setContentType("application/json"); + result.setContent(content); + + when(appConfigClient.getConfiguration(any())).thenReturn(result); + + AwsAppConfigPropertySourceLocator locator = new AwsAppConfigPropertySourceLocator(appConfigClient, "123456789", + "my-project", "api-my", "dev", null, false); + + env.setActiveProfiles("test"); + + PropertySource propertySource = locator.locate(env); + + assertThat(propertySource.getProperty("server.port")).isEqualTo(8089); + } + + @Test + void whenLoadAnAppConfigAndThrowErrorThenRethrowErrorWhenFailFastTrue() { + GetConfigurationResult result = new GetConfigurationResult(); + result.setConfigurationVersion("1"); + result.setContentType("application/json"); + + when(appConfigClient.getConfiguration(any())).thenThrow(new RuntimeException("connection error")); + + AwsAppConfigPropertySourceLocator locator = new AwsAppConfigPropertySourceLocator(appConfigClient, "123456789", + "my-project", "api-my", "dev", null, true); + + env.setActiveProfiles("test"); + + assertThatThrownBy(() -> locator.locate(env)).hasMessage("connection error"); + } + + @Test + void whenLoadAnAppConfigAndThrowErrorThenReturnEmptyWhenFailFastFalse() { + GetConfigurationResult result = new GetConfigurationResult(); + result.setConfigurationVersion("1"); + result.setContentType("application/json"); + + when(appConfigClient.getConfiguration(any())).thenThrow(new RuntimeException("connection error")); + + AwsAppConfigPropertySourceLocator locator = new AwsAppConfigPropertySourceLocator(appConfigClient, "123456789", + "my-project", "api-my", "dev", null, false); + + env.setActiveProfiles("test"); + + assertThatCode(() -> locator.locate(env)).doesNotThrowAnyException(); + } + +} diff --git a/spring-cloud-aws-appconfig/src/test/java/org/springframework/cloud/aws/appconfig/AwsAppConfigPropertySourceTest.java b/spring-cloud-aws-appconfig/src/test/java/org/springframework/cloud/aws/appconfig/AwsAppConfigPropertySourceTest.java new file mode 100644 index 000000000..ebe6eeb90 --- /dev/null +++ b/spring-cloud-aws-appconfig/src/test/java/org/springframework/cloud/aws/appconfig/AwsAppConfigPropertySourceTest.java @@ -0,0 +1,103 @@ +/* + * 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.appconfig; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import com.amazonaws.services.appconfig.AmazonAppConfig; +import com.amazonaws.services.appconfig.model.GetConfigurationRequest; +import com.amazonaws.services.appconfig.model.GetConfigurationResult; +import org.apache.commons.codec.Resources; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import static com.amazonaws.util.IOUtils.toByteArray; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AwsAppConfigPropertySourceTest { + + private AmazonAppConfig appConfigClient = mock(AmazonAppConfig.class); + + @Test + void requestYamlAppConfigExpectSuccessProperties() throws IOException { + ByteBuffer content = ByteBuffer.wrap(toByteArray(Resources.getInputStream("config.yaml"))); + + ArgumentCaptor captor = ArgumentCaptor.forClass(GetConfigurationRequest.class); + + GetConfigurationResult result = new GetConfigurationResult(); + result.setConfigurationVersion("1"); + result.setContentType("application/x-yaml"); + result.setContent(content); + + when(appConfigClient.getConfiguration(captor.capture())).thenReturn(result); + + AwsAppConfigPropertySource source = new AwsAppConfigPropertySource("my-api", "123456789", "my-project", "dev", + null, appConfigClient); + + source.init(); + + assertThat(source.getProperty("spring.application.name")).isEqualTo("my-app"); + assertThat(source.getProperty("server.port")).isEqualTo(8081); + + assertThat(captor.getValue().getConfiguration()).isEqualTo("my-api"); + } + + @Test + public void requestJsonAppConfigExpectSuccessProperties() throws Exception { + ByteBuffer content = ByteBuffer.wrap(toByteArray(Resources.getInputStream("config.json"))); + + ArgumentCaptor captor = ArgumentCaptor.forClass(GetConfigurationRequest.class); + + GetConfigurationResult result = new GetConfigurationResult(); + result.setConfigurationVersion("1"); + result.setContentType("application/json"); + result.setContent(content); + + when(appConfigClient.getConfiguration(captor.capture())).thenReturn(result); + + AwsAppConfigPropertySource source = new AwsAppConfigPropertySource("my-api", "123456789", "my-project", "dev", + null, appConfigClient); + + source.init(); + + assertThat(source.getProperty("spring.application.name")).isEqualTo("my-app"); + assertThat(source.getProperty("server.port")).isEqualTo(8089); + + assertThat(captor.getValue().getConfiguration()).isEqualTo("my-api"); + } + + @Test + public void requestUnsupportedAppConfigContentType() { + ArgumentCaptor captor = ArgumentCaptor.forClass(GetConfigurationRequest.class); + + GetConfigurationResult result = new GetConfigurationResult(); + result.setConfigurationVersion("1"); + result.setContentType("text/plain"); + + when(appConfigClient.getConfiguration(captor.capture())).thenReturn(result); + + AwsAppConfigPropertySource source = new AwsAppConfigPropertySource("my-api", "123456789", "my-project", "dev", + null, appConfigClient); + + Assertions.assertThatThrownBy(source::init).isInstanceOf(IllegalStateException.class); + } + +} diff --git a/spring-cloud-aws-appconfig/src/test/resources/config.json b/spring-cloud-aws-appconfig/src/test/resources/config.json new file mode 100644 index 000000000..429518af1 --- /dev/null +++ b/spring-cloud-aws-appconfig/src/test/resources/config.json @@ -0,0 +1,10 @@ +{ + "spring": { + "application": { + "name": "my-app" + } + }, + "server": { + "port": 8089 + } +} diff --git a/spring-cloud-aws-appconfig/src/test/resources/config.yaml b/spring-cloud-aws-appconfig/src/test/resources/config.yaml new file mode 100644 index 000000000..42d0ac7d1 --- /dev/null +++ b/spring-cloud-aws-appconfig/src/test/resources/config.yaml @@ -0,0 +1,6 @@ +spring: + application.name: my-app + main: + webflux.base-path: /my-controller + +server.port: 8081 diff --git a/spring-cloud-aws-autoconfigure/pom.xml b/spring-cloud-aws-autoconfigure/pom.xml index d3c4fae3b..72bf8937e 100644 --- a/spring-cloud-aws-autoconfigure/pom.xml +++ b/spring-cloud-aws-autoconfigure/pom.xml @@ -21,7 +21,7 @@ org.springframework.cloud spring-cloud-aws - 2.3.0.BUILD-SNAPSHOT + 3.0.0-SNAPSHOT 4.0.0 @@ -48,6 +48,11 @@ spring-cloud-aws-parameter-store-config true + + org.springframework.cloud + spring-cloud-aws-appconfig + true + org.springframework.boot spring-boot-autoconfigure diff --git a/spring-cloud-aws-autoconfigure/src/main/java/org/springframework/cloud/aws/autoconfigure/appconfig/AwsAppConfigBootstrapConfiguration.java b/spring-cloud-aws-autoconfigure/src/main/java/org/springframework/cloud/aws/autoconfigure/appconfig/AwsAppConfigBootstrapConfiguration.java new file mode 100644 index 000000000..d5edd6a13 --- /dev/null +++ b/spring-cloud-aws-autoconfigure/src/main/java/org/springframework/cloud/aws/autoconfigure/appconfig/AwsAppConfigBootstrapConfiguration.java @@ -0,0 +1,75 @@ +/* + * 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.appconfig; + +import java.util.Objects; +import java.util.UUID; + +import com.amazonaws.services.appconfig.AmazonAppConfigAsync; +import com.amazonaws.services.appconfig.AmazonAppConfigClient; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.aws.appconfig.AwsAppConfigPropertySourceLocator; +import org.springframework.cloud.aws.core.config.AmazonWebserviceClientFactoryBean; +import org.springframework.cloud.aws.core.region.RegionProvider; +import org.springframework.cloud.aws.core.region.StaticRegionProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +/** + * @author jarpz + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(AwsAppConfigProperties.class) +@ConditionalOnClass({ AmazonAppConfigAsync.class, AwsAppConfigPropertySourceLocator.class }) +@ConditionalOnProperty(prefix = "spring.cloud.aws.appconfig", name = "enabled", matchIfMissing = true) +public class AwsAppConfigBootstrapConfiguration { + + private final AwsAppConfigProperties properties; + + private final RegionProvider regionProvider; + + public AwsAppConfigBootstrapConfiguration(AwsAppConfigProperties properties, + ObjectProvider regionProvider) { + this.properties = properties; + this.regionProvider = Objects.isNull(properties.getRegion()) ? regionProvider.getIfAvailable() + : new StaticRegionProvider(properties.getRegion()); + } + + @Bean + AwsAppConfigPropertySourceLocator awsAppConfigPropertySourceLocator(AmazonAppConfigClient appConfigClient) { + + String clientId = StringUtils.isEmpty(properties.getClientId()) ? UUID.randomUUID().toString() + : properties.getClientId(); + + return new AwsAppConfigPropertySourceLocator(appConfigClient, clientId, properties.getApplication(), + properties.getConfigurationProfile(), properties.getEnvironment(), properties.getConfigurationVersion(), + properties.isFailFast()); + } + + @Bean + @ConditionalOnMissingBean + public AmazonWebserviceClientFactoryBean appConfigClient() { + return new AmazonWebserviceClientFactoryBean<>(AmazonAppConfigClient.class, null, regionProvider); + } + +} diff --git a/spring-cloud-aws-autoconfigure/src/main/java/org/springframework/cloud/aws/autoconfigure/appconfig/AwsAppConfigProperties.java b/spring-cloud-aws-autoconfigure/src/main/java/org/springframework/cloud/aws/autoconfigure/appconfig/AwsAppConfigProperties.java new file mode 100644 index 000000000..237193be0 --- /dev/null +++ b/spring-cloud-aws-autoconfigure/src/main/java/org/springframework/cloud/aws/autoconfigure/appconfig/AwsAppConfigProperties.java @@ -0,0 +1,118 @@ +/* + * 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.appconfig; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +/** + * @author jarpz + */ +@ConfigurationProperties(prefix = "spring.cloud.aws.appconfig") +public class AwsAppConfigProperties implements Validator { + + private String clientId; + + private String application; + + private String environment; + + private String configurationProfile; + + private String configurationVersion; + + private String region; + + private boolean failFast; + + @Override + public boolean supports(Class clazz) { + return AwsAppConfigProperties.class.isAssignableFrom(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + AwsAppConfigProperties properties = (AwsAppConfigProperties) target; + + if (StringUtils.isEmpty(properties.getApplication())) { + errors.rejectValue("application", "NotEmpty", "application should not be empty or null."); + } + + if (StringUtils.isEmpty(properties.getEnvironment())) { + errors.rejectValue("environment", "NotEmpty", "environment should not be empty or null."); + } + } + + public String getClientId() { + return clientId; + } + + public String getApplication() { + return application; + } + + public String getEnvironment() { + return environment; + } + + public String getConfigurationProfile() { + return configurationProfile; + } + + public String getConfigurationVersion() { + return configurationVersion; + } + + public String getRegion() { + return region; + } + + public boolean isFailFast() { + return failFast; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public void setApplication(String application) { + this.application = application; + } + + public void setEnvironment(String environment) { + this.environment = environment; + } + + public void setConfigurationProfile(String configurationProfile) { + this.configurationProfile = configurationProfile; + } + + public void setConfigurationVersion(String configurationVersion) { + this.configurationVersion = configurationVersion; + } + + public void setRegion(String region) { + this.region = region; + } + + public void setFailFast(boolean failFast) { + this.failFast = failFast; + } + +} diff --git a/spring-cloud-aws-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-aws-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 038e43ca0..9d918bf9a 100644 --- a/spring-cloud-aws-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-aws-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -47,6 +47,12 @@ "name": "cloud.aws.stack.enabled", "description": "Enables Stack integration.", "type": "java.lang.Boolean" + }, + { + "defaultValue": true, + "name": "spring.cloud.aws.appconfig.enabled", + "description": "Enables AppConfig integration.", + "type": "java.lang.Boolean" } ] } diff --git a/spring-cloud-aws-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-aws-autoconfigure/src/main/resources/META-INF/spring.factories index 0afe3da43..c0ea7f066 100644 --- a/spring-cloud-aws-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-aws-autoconfigure/src/main/resources/META-INF/spring.factories @@ -1,3 +1,6 @@ +org.springframework.cloud.bootstrap.BootstrapConfiguration=\ +org.springframework.cloud.aws.autoconfigure.appconfig.AwsAppConfigBootstrapConfiguration + org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.cloud.aws.autoconfigure.context.ContextInstanceDataAutoConfiguration,\ org.springframework.cloud.aws.autoconfigure.context.ContextCredentialsAutoConfiguration,\ diff --git a/spring-cloud-aws-autoconfigure/src/test/java/org/springframework/cloud/aws/autoconfigure/appconfig/AwsAppConfigBootstrapConfigurationTest.java b/spring-cloud-aws-autoconfigure/src/test/java/org/springframework/cloud/aws/autoconfigure/appconfig/AwsAppConfigBootstrapConfigurationTest.java new file mode 100644 index 000000000..5dd63958e --- /dev/null +++ b/spring-cloud-aws-autoconfigure/src/test/java/org/springframework/cloud/aws/autoconfigure/appconfig/AwsAppConfigBootstrapConfigurationTest.java @@ -0,0 +1,59 @@ +/* + * 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.appconfig; + +import com.amazonaws.services.appconfig.AmazonAppConfig; +import com.amazonaws.services.appconfig.AmazonAppConfigClient; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +class AwsAppConfigBootstrapConfigurationTest { + + private final ApplicationContextRunner runner = new ApplicationContextRunner() + .withUserConfiguration(AwsAppConfigBootstrapConfiguration.class); + + private static final String[] properties = new String[] { "spring.cloud.aws.appconfig.region=us-east-2", + "spring.cloud.aws.appconfig.account-id=1234567", "spring.cloud.aws.appconfig.application=demo", + "spring.cloud.aws.appconfig.environment=dev" }; + + @Test + void testWithStaticRegion() { + + runner.withPropertyValues(properties).run(context -> { + AmazonAppConfig appConfig = context.getBean(AmazonAppConfig.class); + + assertThat(appConfig).isNotNull(); + + Object region = ReflectionTestUtils.getField(appConfig, "signingRegion"); + assertThat("us-east-2").isEqualTo(region); + }); + } + + @Test + void testUserAgent() { + runner.withPropertyValues(properties) + .run(context -> assertThat(context.getBean(AmazonAppConfig.class)).isNotNull() + .extracting(AmazonAppConfigClient.class::cast) + .extracting(appconfig -> appconfig.getClientConfiguration().getUserAgentSuffix()).asString() + .startsWith("spring-cloud-aws/")); + } + +} diff --git a/spring-cloud-aws-autoconfigure/src/test/java/org/springframework/cloud/aws/autoconfigure/appconfig/AwsAppConfigPropertiesTest.java b/spring-cloud-aws-autoconfigure/src/test/java/org/springframework/cloud/aws/autoconfigure/appconfig/AwsAppConfigPropertiesTest.java new file mode 100644 index 000000000..d02c42a4e --- /dev/null +++ b/spring-cloud-aws-autoconfigure/src/test/java/org/springframework/cloud/aws/autoconfigure/appconfig/AwsAppConfigPropertiesTest.java @@ -0,0 +1,73 @@ +/* + * 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.appconfig; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.Errors; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author jarpz + */ +class AwsAppConfigPropertiesTest { + + @ParameterizedTest + @MethodSource("invalidProperties") + void validationFails(AwsAppConfigProperties properties, String field, String errorCode) { + Errors errors = new BeanPropertyBindingResult(properties, "properties"); + + properties.validate(properties, errors); + + assertThat(errors.getFieldError(field)).isNotNull(); + assertThat(errors.getFieldError(field).getCode()).isEqualTo(errorCode); + } + + private static Stream invalidProperties() { + return Stream.of(Arguments.of(buildProperties(null, "", null, null), "application", "NotEmpty"), + Arguments.of(buildProperties(null, null, null, ""), "environment", "NotEmpty")); + } + + @Test + void validationSucceeds() { + AwsAppConfigProperties properties = buildProperties("12345678", "my-project", "my-app", "dev"); + + Errors errors = new BeanPropertyBindingResult(properties, "properties"); + properties.validate(properties, errors); + + assertThat(errors.getAllErrors()).isEmpty(); + } + + private static AwsAppConfigProperties buildProperties(String clientId, String application, + String configurationProfile, String environment) { + AwsAppConfigProperties properties = new AwsAppConfigProperties(); + properties.setClientId(clientId); + properties.setApplication(application); + properties.setConfigurationProfile(configurationProfile); + properties.setEnvironment(environment); + + return properties; + } + +} diff --git a/spring-cloud-aws-autoconfigure/src/test/java/org/springframework/cloud/aws/autoconfigure/context/ContextStackAutoConfigurationTest.java b/spring-cloud-aws-autoconfigure/src/test/java/org/springframework/cloud/aws/autoconfigure/context/ContextStackAutoConfigurationTest.java index eef624cf8..1bcda4e5b 100644 --- a/spring-cloud-aws-autoconfigure/src/test/java/org/springframework/cloud/aws/autoconfigure/context/ContextStackAutoConfigurationTest.java +++ b/spring-cloud-aws-autoconfigure/src/test/java/org/springframework/cloud/aws/autoconfigure/context/ContextStackAutoConfigurationTest.java @@ -92,8 +92,7 @@ void stackRegistry_manualConfigurationEnabled_returnsAutoConfiguredStackRegistry } @Test - void stackRegistry_manualConfigurationEnabledAndStackNameProvided_returnsStaticStackNameProvider() - throws Exception { + void stackRegistry_manualConfigurationEnabledAndStackNameProvided_returnsStaticStackNameProvider() { this.contextRunner .withPropertyValues("cloud.aws.stack.name:manualStackName", "cloud.aws.stack.auto:true") diff --git a/spring-cloud-aws-context/pom.xml b/spring-cloud-aws-context/pom.xml index 2a93905f4..1358c9f14 100644 --- a/spring-cloud-aws-context/pom.xml +++ b/spring-cloud-aws-context/pom.xml @@ -22,7 +22,7 @@ org.springframework.cloud spring-cloud-aws - 2.3.0.BUILD-SNAPSHOT + 3.0.0-SNAPSHOT spring-cloud-aws-context diff --git a/spring-cloud-aws-core/pom.xml b/spring-cloud-aws-core/pom.xml index 30ed846e3..a57d97902 100644 --- a/spring-cloud-aws-core/pom.xml +++ b/spring-cloud-aws-core/pom.xml @@ -22,7 +22,7 @@ org.springframework.cloud spring-cloud-aws - 2.3.0.BUILD-SNAPSHOT + 3.0.0-SNAPSHOT spring-cloud-aws-core diff --git a/spring-cloud-aws-core/src/main/java/org/springframework/cloud/aws/core/SpringCloudClientConfiguration.java b/spring-cloud-aws-core/src/main/java/org/springframework/cloud/aws/core/SpringCloudClientConfiguration.java index 89eab6e21..51f4fbd06 100644 --- a/spring-cloud-aws-core/src/main/java/org/springframework/cloud/aws/core/SpringCloudClientConfiguration.java +++ b/spring-cloud-aws-core/src/main/java/org/springframework/cloud/aws/core/SpringCloudClientConfiguration.java @@ -26,7 +26,7 @@ public final class SpringCloudClientConfiguration { private static final String NAME = "spring-cloud-aws"; - private static final String VERSION = "2.3.0"; + private static final String VERSION = "3.0.0"; private SpringCloudClientConfiguration() { diff --git a/spring-cloud-aws-dependencies/pom.xml b/spring-cloud-aws-dependencies/pom.xml index e1e05c0a9..97400f971 100644 --- a/spring-cloud-aws-dependencies/pom.xml +++ b/spring-cloud-aws-dependencies/pom.xml @@ -22,11 +22,11 @@ org.springframework.cloud spring-cloud-dependencies-parent - 2.3.2.BUILD-SNAPSHOT + 3.0.0-SNAPSHOT spring-cloud-aws-dependencies - 2.3.0.BUILD-SNAPSHOT + 3.0.0-SNAPSHOT pom Spring Cloud AWS Dependencies Spring Cloud AWS Dependencies @@ -99,6 +99,11 @@ spring-cloud-starter-aws-parameter-store-config ${project.version} + + org.springframework.cloud + spring-cloud-starter-aws-appconfig + ${project.version} + org.springframework.cloud spring-cloud-starter-aws-secrets-manager-config @@ -134,6 +139,11 @@ spring-cloud-aws-parameter-store-config ${project.version} + + org.springframework.cloud + spring-cloud-aws-appconfig + ${project.version} + org.springframework.cloud spring-cloud-aws-secrets-manager-config diff --git a/spring-cloud-aws-integration-test/pom.xml b/spring-cloud-aws-integration-test/pom.xml index c3a4ec09d..f4a7cf132 100644 --- a/spring-cloud-aws-integration-test/pom.xml +++ b/spring-cloud-aws-integration-test/pom.xml @@ -22,7 +22,7 @@ org.springframework.cloud spring-cloud-aws - 2.3.0.BUILD-SNAPSHOT + 3.0.0-SNAPSHOT spring-cloud-aws-integration-test diff --git a/spring-cloud-aws-jdbc/pom.xml b/spring-cloud-aws-jdbc/pom.xml index f0079d869..b71b94a90 100644 --- a/spring-cloud-aws-jdbc/pom.xml +++ b/spring-cloud-aws-jdbc/pom.xml @@ -22,7 +22,7 @@ org.springframework.cloud spring-cloud-aws - 2.3.0.BUILD-SNAPSHOT + 3.0.0-SNAPSHOT spring-cloud-aws-jdbc diff --git a/spring-cloud-aws-messaging/pom.xml b/spring-cloud-aws-messaging/pom.xml index b32dd8b6e..e436bd438 100644 --- a/spring-cloud-aws-messaging/pom.xml +++ b/spring-cloud-aws-messaging/pom.xml @@ -22,7 +22,7 @@ org.springframework.cloud spring-cloud-aws - 2.3.0.BUILD-SNAPSHOT + 3.0.0-SNAPSHOT spring-cloud-aws-messaging diff --git a/spring-cloud-aws-parameter-store-config/pom.xml b/spring-cloud-aws-parameter-store-config/pom.xml index 7dc61f64a..d0f702a29 100644 --- a/spring-cloud-aws-parameter-store-config/pom.xml +++ b/spring-cloud-aws-parameter-store-config/pom.xml @@ -21,7 +21,7 @@ org.springframework.cloud spring-cloud-aws - 2.3.0.BUILD-SNAPSHOT + 3.0.0-SNAPSHOT spring-cloud-aws-parameter-store-config diff --git a/spring-cloud-aws-secrets-manager-config/pom.xml b/spring-cloud-aws-secrets-manager-config/pom.xml index ff74080fe..13abb4d71 100644 --- a/spring-cloud-aws-secrets-manager-config/pom.xml +++ b/spring-cloud-aws-secrets-manager-config/pom.xml @@ -21,7 +21,7 @@ org.springframework.cloud spring-cloud-aws - 2.3.0.BUILD-SNAPSHOT + 3.0.0-SNAPSHOT spring-cloud-aws-secrets-manager-config diff --git a/spring-cloud-aws-secrets-manager-config/src/main/java/org/springframework/cloud/aws/secretsmanager/AwsSecretsManagerProperties.java b/spring-cloud-aws-secrets-manager-config/src/main/java/org/springframework/cloud/aws/secretsmanager/AwsSecretsManagerProperties.java index 44b99989d..3ffb110b3 100644 --- a/spring-cloud-aws-secrets-manager-config/src/main/java/org/springframework/cloud/aws/secretsmanager/AwsSecretsManagerProperties.java +++ b/spring-cloud-aws-secrets-manager-config/src/main/java/org/springframework/cloud/aws/secretsmanager/AwsSecretsManagerProperties.java @@ -53,7 +53,7 @@ public class AwsSecretsManagerProperties implements Validator { /** * Prefix indicating first level for every property. Value must start with a forward - * slash followed by a valid path segment or be empty. Defaults to "/config". + * slash followed by a valid path segment or be empty. Defaults to "/secret". */ private String prefix = "/secret"; diff --git a/spring-cloud-starter-aws-appconfig/pom.xml b/spring-cloud-starter-aws-appconfig/pom.xml new file mode 100644 index 000000000..6559dca6b --- /dev/null +++ b/spring-cloud-starter-aws-appconfig/pom.xml @@ -0,0 +1,52 @@ + + + + + 4.0.0 + + org.springframework.cloud + spring-cloud-aws + 3.0.0-SNAPSHOT + + + spring-cloud-starter-aws-appconfig + Spring Cloud AWS AppConfig Configuration Starter + Spring Cloud AWS AppConfig Configuration Starter + https://projects.spring.io/spring-cloud + + Pivotal Software, Inc. + https://www.spring.io + + + ${basedir}/../.. + + + + + org.springframework.cloud + spring-cloud-aws-appconfig + + + + org.springframework.cloud + spring-cloud-aws-autoconfigure + + + + diff --git a/spring-cloud-starter-aws-jdbc/pom.xml b/spring-cloud-starter-aws-jdbc/pom.xml index 28b2d83b2..d50b9894c 100644 --- a/spring-cloud-starter-aws-jdbc/pom.xml +++ b/spring-cloud-starter-aws-jdbc/pom.xml @@ -22,7 +22,7 @@ org.springframework.cloud spring-cloud-aws - 2.3.0.BUILD-SNAPSHOT + 3.0.0-SNAPSHOT spring-cloud-starter-aws-jdbc Spring Cloud AWS JDBC Starter diff --git a/spring-cloud-starter-aws-messaging/pom.xml b/spring-cloud-starter-aws-messaging/pom.xml index f899a545b..0ebca94d1 100644 --- a/spring-cloud-starter-aws-messaging/pom.xml +++ b/spring-cloud-starter-aws-messaging/pom.xml @@ -22,7 +22,7 @@ org.springframework.cloud spring-cloud-aws - 2.3.0.BUILD-SNAPSHOT + 3.0.0-SNAPSHOT spring-cloud-starter-aws-messaging Spring Cloud AWS Messaging Starter diff --git a/spring-cloud-starter-aws-parameter-store-config/pom.xml b/spring-cloud-starter-aws-parameter-store-config/pom.xml index df46a869c..d9306032c 100644 --- a/spring-cloud-starter-aws-parameter-store-config/pom.xml +++ b/spring-cloud-starter-aws-parameter-store-config/pom.xml @@ -22,7 +22,7 @@ org.springframework.cloud spring-cloud-aws - 2.3.0.BUILD-SNAPSHOT + 3.0.0-SNAPSHOT spring-cloud-starter-aws-parameter-store-config Spring Cloud AWS Parameter Store Configuration Starter diff --git a/spring-cloud-starter-aws-secrets-manager-config/pom.xml b/spring-cloud-starter-aws-secrets-manager-config/pom.xml index 49bb09186..40a4b2abe 100644 --- a/spring-cloud-starter-aws-secrets-manager-config/pom.xml +++ b/spring-cloud-starter-aws-secrets-manager-config/pom.xml @@ -22,7 +22,7 @@ org.springframework.cloud spring-cloud-aws - 2.3.0.BUILD-SNAPSHOT + 3.0.0-SNAPSHOT spring-cloud-starter-aws-secrets-manager-config Spring Cloud AWS Secrets Manager Configuration Starter diff --git a/spring-cloud-starter-aws/pom.xml b/spring-cloud-starter-aws/pom.xml index a31612ca3..e926c0032 100644 --- a/spring-cloud-starter-aws/pom.xml +++ b/spring-cloud-starter-aws/pom.xml @@ -22,7 +22,7 @@ org.springframework.cloud spring-cloud-aws - 2.3.0.BUILD-SNAPSHOT + 3.0.0-SNAPSHOT spring-cloud-starter-aws Spring Cloud AWS Starter