From d0a31811d9a1f7909978d7f48cf0d2b9b3ee81e0 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 24 Feb 2020 20:17:01 -0800 Subject: [PATCH] AppConfig 1.1.2 Updates (#630) * Updated to match 1.2.2 * Updated Feature Gate for 1.1.2 * Fixed pom file indentation --- pom.xml | 1 + .../pom.xml | 27 ++ .../AppConfigurationWebAutoConfiguration.java | 27 ++ .../cloud/config/web}/ConfigListener.java | 12 +- .../main/resources/META-INF/spring.factories | 2 + ...ConfigurationWebAutoConfigurationTest.java | 35 +++ .../cloud/config/web/TestConstants.java | 22 ++ .../spring/cloud/config/web/TestUtils.java | 49 ++++ .../pom.xml | 32 +-- ...=> AppConfigurationAutoConfiguration.java} | 17 +- ...pConfigurationBootstrapConfiguration.java} | 48 ++-- ...> AppConfigurationCredentialProvider.java} | 2 +- ...s.java => AppConfigurationProperties.java} | 26 +- ...va => AppConfigurationPropertySource.java} | 84 +++---- ...ppConfigurationPropertySourceLocator.java} | 55 ++-- ...> AppConfigurationProviderProperties.java} | 4 +- ...resh.java => AppConfigurationRefresh.java} | 24 +- .../azure/spring/cloud/config/HostType.java | 5 +- .../cloud/config/RequestTracingConstants.java | 12 +- .../spring/cloud/config/StateHolder.java | 35 ++- .../feature/management/entity/Feature.java | 17 +- .../FeatureFilterEvaluationContext.java | 8 +- .../feature/management/entity/FeatureSet.java | 7 - .../policies/BaseAppConfigurationPolicy.java | 32 ++- .../AppConfigManagedIdentityProperties.java | 22 ++ .../cloud/config/stores/ClientStore.java | 19 +- .../cloud/config/stores/ConfigStore.java | 12 +- .../cloud/config/stores/KeyVaultClient.java | 35 ++- .../main/resources/META-INF/spring.factories | 4 +- ...figurationBootstrapConfigurationTest.java} | 31 +-- ...va => AppConfigurationPropertiesTest.java} | 27 +- ...figurationPropertySourceKeyVaultTest.java} | 23 +- ...nfigurationPropertySourceLocatorTest.java} | 183 +++++++++++--- ...> AppConfigurationPropertySourceTest.java} | 94 +++++-- ....java => AppConfigurationRefreshTest.java} | 47 ++-- ...AzureCloudConfigAutoConfigurationTest.java | 33 --- .../spring/cloud/config/TestConstants.java | 2 + .../azure/spring/cloud/config/TestUtils.java | 8 +- .../BaseAppConfigurationPolicyTest.java | 8 +- .../AzureConfigPropertySourceLocatorTest.java | 87 ------- .../cloud/config/stores/ClientStoreTest.java | 237 ++++++++++++++++++ .../cloud/config/stores/ConfigStoreTest.java | 30 +++ .../config/stores/KeyVaultClientTest.java | 151 +++++++++++ .../org.mockito.plugins.MockMaker | 0 .../pom.xml | 12 - .../cloud/feature/manager/FeatureHandler.java | 11 +- .../FeatureManagementWebConfiguration.java | 6 +- .../feature/manager/FeatureHandlerTest.java | 40 ++- .../manager/FeatureManagerSnapshotTest.java | 12 + .../README.md | 64 +++-- spring-cloud-azure-feature-management/pom.xml | 1 - .../FeatureManagementConfiguration.java | 2 +- .../cloud/feature/manager/FeatureManager.java | 34 ++- .../manager/FilterNotFoundException.java | 9 +- .../feature/manager/FilterParameters.java | 6 +- .../feature/manager/entities/Feature.java | 27 +- .../FeatureFilterEvaluationContext.java | 4 +- .../feature/manager/FeatureManagerTest.java | 93 +++++-- .../feature/filters/PercentageFilterTest.java | 59 +++++ .../feature/filters/TimeWindowFilterTest.java | 93 +++++++ .../azure-appconfiguration-sample/pom.xml | 2 +- .../java/com/example/ConsoleApplication.java | 10 +- .../src/main/resources/application.yml | 22 +- .../java/com/example/HelloController.java | 12 +- .../src/main/resources/application.yml | 24 +- .../src/main/resources/bootstrap.yaml | 8 - .../README.md | 84 +++++-- .../pom.xml | 4 - 68 files changed, 1603 insertions(+), 672 deletions(-) create mode 100644 spring-cloud-azure-appconfiguration-config-web/pom.xml create mode 100644 spring-cloud-azure-appconfiguration-config-web/src/main/java/com/microsoft/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java rename {spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config => spring-cloud-azure-appconfiguration-config-web/src/main/java/com/microsoft/azure/spring/cloud/config/web}/ConfigListener.java (67%) create mode 100644 spring-cloud-azure-appconfiguration-config-web/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-azure-appconfiguration-config-web/src/test/java/com/microsoft/azure/spring/cloud/config/web/AppConfigurationWebAutoConfigurationTest.java create mode 100644 spring-cloud-azure-appconfiguration-config-web/src/test/java/com/microsoft/azure/spring/cloud/config/web/TestConstants.java create mode 100644 spring-cloud-azure-appconfiguration-config-web/src/test/java/com/microsoft/azure/spring/cloud/config/web/TestUtils.java rename spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/{AzureCloudConfigAutoConfiguration.java => AppConfigurationAutoConfiguration.java} (58%) rename spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/{AzureConfigBootstrapConfiguration.java => AppConfigurationBootstrapConfiguration.java} (63%) rename spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/{AppConfigCredentialProvider.java => AppConfigurationCredentialProvider.java} (85%) rename spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/{AzureCloudConfigProperties.java => AppConfigurationProperties.java} (84%) rename spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/{AzureConfigPropertySource.java => AppConfigurationPropertySource.java} (80%) rename spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/{AzureConfigPropertySourceLocator.java => AppConfigurationPropertySourceLocator.java} (81%) rename spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/{AppConfigProviderProperties.java => AppConfigurationProviderProperties.java} (94%) rename spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/{AzureCloudConfigRefresh.java => AppConfigurationRefresh.java} (88%) create mode 100644 spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/resource/AppConfigManagedIdentityProperties.java rename spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/{AzureConfigBootstrapConfigurationTest.java => AppConfigurationBootstrapConfigurationTest.java} (76%) rename spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/{AzureCloudConfigPropertiesTest.java => AppConfigurationPropertiesTest.java} (87%) rename spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/{AzureConfigPropertySourceKeyVaultTest.java => AppConfigurationPropertySourceKeyVaultTest.java} (90%) rename spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/{AzureConfigPropertySourceLocatorTest.java => AppConfigurationPropertySourceLocatorTest.java} (55%) rename spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/{AzureConfigPropertySourceTest.java => AppConfigurationPropertySourceTest.java} (78%) rename spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/{AzureConfigCloudRefreshTest.java => AppConfigurationRefreshTest.java} (78%) delete mode 100644 spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureCloudConfigAutoConfigurationTest.java delete mode 100644 spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/stores/AzureConfigPropertySourceLocatorTest.java create mode 100644 spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/stores/ClientStoreTest.java create mode 100644 spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/stores/ConfigStoreTest.java create mode 100644 spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/stores/KeyVaultClientTest.java rename spring-cloud-azure-appconfiguration-config/src/{main => test}/resources/mockito-extensions/org.mockito.plugins.MockMaker (100%) create mode 100644 spring-cloud-azure-feature-management/src/test/java/com/microsoft/azure/spring/cloud/feature/manager/feature/filters/PercentageFilterTest.java create mode 100644 spring-cloud-azure-feature-management/src/test/java/com/microsoft/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilterTest.java delete mode 100644 spring-cloud-azure-samples/feature-management-web-sample/src/main/resources/bootstrap.yaml diff --git a/pom.xml b/pom.xml index 3eeaeee7c..a3f6dbf1b 100644 --- a/pom.xml +++ b/pom.xml @@ -53,6 +53,7 @@ spring-cloud-azure-appconfiguration-config + spring-cloud-azure-appconfiguration-config-web spring-cloud-azure-autoconfigure spring-cloud-azure-context spring-cloud-azure-dependencies diff --git a/spring-cloud-azure-appconfiguration-config-web/pom.xml b/spring-cloud-azure-appconfiguration-config-web/pom.xml new file mode 100644 index 000000000..6ff36ad72 --- /dev/null +++ b/spring-cloud-azure-appconfiguration-config-web/pom.xml @@ -0,0 +1,27 @@ + + + + com.microsoft.azure + spring-cloud-azure + 1.1.2-SNAPSHOT + ../pom.xml + + 4.0.0 + + spring-cloud-azure-appconfiguration-config-web + Spring Cloud Azure App Configuration Config Web + Integration of Spring Cloud Config and Azure App Configuration Service + + + + com.microsoft.azure + spring-cloud-azure-appconfiguration-config + + + org.springframework + spring-web + + + diff --git a/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/microsoft/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java b/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/microsoft/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java new file mode 100644 index 000000000..abb153f48 --- /dev/null +++ b/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/microsoft/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for + * license information. + */ +package com.microsoft.azure.spring.cloud.config.web; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.cloud.endpoint.RefreshEndpoint; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.microsoft.azure.spring.cloud.config.AppConfigurationRefresh; + +@Configuration +public class AppConfigurationWebAutoConfiguration { + + @Configuration + @ConditionalOnClass(RefreshEndpoint.class) + static class AppConfigurationWatchAutoConfiguration { + + @Bean + public ConfigListener configListener(AppConfigurationRefresh appConfigurationRefresh) { + return new ConfigListener(appConfigurationRefresh); + } + } +} diff --git a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/ConfigListener.java b/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/microsoft/azure/spring/cloud/config/web/ConfigListener.java similarity index 67% rename from spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/ConfigListener.java rename to spring-cloud-azure-appconfiguration-config-web/src/main/java/com/microsoft/azure/spring/cloud/config/web/ConfigListener.java index 3a44188d0..43407dcd3 100644 --- a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/ConfigListener.java +++ b/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/microsoft/azure/spring/cloud/config/web/ConfigListener.java @@ -3,7 +3,7 @@ * Licensed under the MIT License. See LICENSE in the project root for * license information. */ -package com.microsoft.azure.spring.cloud.config; +package com.microsoft.azure.spring.cloud.config.web; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,20 +11,22 @@ import org.springframework.stereotype.Component; import org.springframework.web.context.support.ServletRequestHandledEvent; +import com.microsoft.azure.spring.cloud.config.AppConfigurationRefresh; + @Component public class ConfigListener implements ApplicationListener { private static final Logger LOGGER = LoggerFactory.getLogger(ConfigListener.class); - private AzureCloudConfigRefresh azureCloudConfigRefresh; + private AppConfigurationRefresh appConfigurationRefresh; - public ConfigListener(AzureCloudConfigRefresh azureCloudConfigRefresh) { - this.azureCloudConfigRefresh = azureCloudConfigRefresh; + public ConfigListener(AppConfigurationRefresh appConfigurationRefresh) { + this.appConfigurationRefresh = appConfigurationRefresh; } @Override public void onApplicationEvent(ServletRequestHandledEvent event) { try { - azureCloudConfigRefresh.refreshConfigurations(); + appConfigurationRefresh.refreshConfigurations(); } catch (Exception e) { LOGGER.error("Refresh failed with unexpected exception.", e); } diff --git a/spring-cloud-azure-appconfiguration-config-web/src/main/resources/META-INF/spring.factories b/spring-cloud-azure-appconfiguration-config-web/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..7227be43e --- /dev/null +++ b/spring-cloud-azure-appconfiguration-config-web/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.microsoft.azure.spring.cloud.config.web.AppConfigurationWebAutoConfiguration diff --git a/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/microsoft/azure/spring/cloud/config/web/AppConfigurationWebAutoConfigurationTest.java b/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/microsoft/azure/spring/cloud/config/web/AppConfigurationWebAutoConfigurationTest.java new file mode 100644 index 000000000..6b0660462 --- /dev/null +++ b/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/microsoft/azure/spring/cloud/config/web/AppConfigurationWebAutoConfigurationTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for + * license information. + */ +package com.microsoft.azure.spring.cloud.config.web; + +import static com.microsoft.azure.spring.cloud.config.web.TestConstants.CONN_STRING_PROP; +import static com.microsoft.azure.spring.cloud.config.web.TestConstants.STORE_ENDPOINT_PROP; +import static com.microsoft.azure.spring.cloud.config.web.TestConstants.TEST_CONN_STRING; +import static com.microsoft.azure.spring.cloud.config.web.TestConstants.TEST_STORE_NAME; +import static com.microsoft.azure.spring.cloud.config.web.TestUtils.propPair; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import com.microsoft.azure.spring.cloud.config.AppConfigurationAutoConfiguration; +import com.microsoft.azure.spring.cloud.config.AppConfigurationBootstrapConfiguration; + +public class AppConfigurationWebAutoConfigurationTest { + private static final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues(propPair(CONN_STRING_PROP, TEST_CONN_STRING), + propPair(STORE_ENDPOINT_PROP, TEST_STORE_NAME)) + .withConfiguration(AutoConfigurations.of(AppConfigurationBootstrapConfiguration.class, + AppConfigurationAutoConfiguration.class, AppConfigurationWebAutoConfiguration.class)); + + @Test + public void watchEnabledNotConfiguredShouldNotCreateWatch() { + contextRunner.run(context -> { + assertThat(context).hasSingleBean(ConfigListener.class); + }); + } +} diff --git a/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/microsoft/azure/spring/cloud/config/web/TestConstants.java b/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/microsoft/azure/spring/cloud/config/web/TestConstants.java new file mode 100644 index 000000000..55a6df957 --- /dev/null +++ b/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/microsoft/azure/spring/cloud/config/web/TestConstants.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for + * license information. + */ +package com.microsoft.azure.spring.cloud.config.web; + +/** + * Test constants which can be shared across different test classes + */ +public class TestConstants { + private TestConstants() { + } + + // Store specific configuration + public static final String CONFIG_ENABLED_PROP = "spring.cloud.azure.appconfiguration.enabled"; + public static final String CONN_STRING_PROP = "spring.cloud.azure.appconfiguration.stores[0].connection-string"; + public static final String STORE_ENDPOINT_PROP = "spring.cloud.azure.appconfiguration.stores[0].endpoint"; + public static final String TEST_CONN_STRING = + "Endpoint=https://fake.test.config.io;Id=fake-conn-id;Secret=ZmFrZS1jb25uLXNlY3JldA=="; + public static final String TEST_STORE_NAME = "store1"; +} diff --git a/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/microsoft/azure/spring/cloud/config/web/TestUtils.java b/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/microsoft/azure/spring/cloud/config/web/TestUtils.java new file mode 100644 index 000000000..603cc9db4 --- /dev/null +++ b/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/microsoft/azure/spring/cloud/config/web/TestUtils.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for + * license information. + */ +package com.microsoft.azure.spring.cloud.config.web; + +import java.util.List; + +import com.azure.data.appconfiguration.models.ConfigurationSetting; +import com.microsoft.azure.spring.cloud.config.AppConfigurationProperties; +import com.microsoft.azure.spring.cloud.config.stores.ConfigStore; + +/** + * Utility methods which can be used across different test classes + */ +public class TestUtils { + private TestUtils() { + } + + static String propPair(String propName, String propValue) { + return String.format("%s=%s", propName, propValue); + } + + static ConfigurationSetting createItem(String context, String key, String value, String label, String contentType) { + ConfigurationSetting item = new ConfigurationSetting(); + item.setKey(context + key); + item.setValue(value); + item.setLabel(label); + item.setContentType(contentType); + + return item; + } + + static void addStore(AppConfigurationProperties properties, String storeEndpoint, String connectionString) { + addStore(properties, storeEndpoint, connectionString, null); + } + + static void addStore(AppConfigurationProperties properties, String storeEndpoint, String connectionString, + String label) { + List stores = properties.getStores(); + ConfigStore store = new ConfigStore(); + store.setConnectionString(connectionString); + store.setEndpoint(storeEndpoint); + store.setLabel(label); + stores.add(store); + properties.setStores(stores); + } +} diff --git a/spring-cloud-azure-appconfiguration-config/pom.xml b/spring-cloud-azure-appconfiguration-config/pom.xml index c6099ce4e..defacd3fa 100644 --- a/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/spring-cloud-azure-appconfiguration-config/pom.xml @@ -1,7 +1,7 @@ + xmlns="http://maven.apache.org/POM/4.0.0" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> com.microsoft.azure spring-cloud-azure @@ -20,31 +20,22 @@ spring-boot-autoconfigure-processor - org.springframework.cloud - spring-cloud-context + org.springframework.boot + spring-boot-autoconfigure - commons-codec - commons-codec + org.springframework.boot + spring-boot-configuration-processor + true - org.apache.httpcomponents - httpclient - - - commons-logging - commons-logging - - + org.springframework.cloud + spring-cloud-context org.slf4j jcl-over-slf4j - - commons-io - commons-io - com.fasterxml.jackson.core jackson-annotations @@ -57,10 +48,6 @@ org.springframework.boot spring-boot-autoconfigure - - com.microsoft.azure - spring-cloud-azure-context - org.springframework spring-web @@ -70,6 +57,7 @@ spring-boot-configuration-processor true + com.azure azure-core diff --git a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AzureCloudConfigAutoConfiguration.java b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationAutoConfiguration.java similarity index 58% rename from spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AzureCloudConfigAutoConfiguration.java rename to spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationAutoConfiguration.java index 9e7aa31cf..27511bf36 100644 --- a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AzureCloudConfigAutoConfiguration.java +++ b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationAutoConfiguration.java @@ -14,22 +14,17 @@ import com.microsoft.azure.spring.cloud.config.stores.ClientStore; @Configuration -@ConditionalOnProperty(prefix = AzureCloudConfigProperties.CONFIG_PREFIX, name = "enabled", matchIfMissing = true) -public class AzureCloudConfigAutoConfiguration { +@ConditionalOnProperty(prefix = AppConfigurationProperties.CONFIG_PREFIX, name = "enabled", matchIfMissing = true) +public class AppConfigurationAutoConfiguration { @Configuration @ConditionalOnClass(RefreshEndpoint.class) - static class CloudWatchAutoConfiguration { + static class AppConfigurationWatchAutoConfiguration { @Bean - public AzureCloudConfigRefresh getConfigWatch(AzureCloudConfigProperties properties, - AzureConfigPropertySourceLocator sourceLocator, ClientStore clientStore) { - return new AzureCloudConfigRefresh(properties, sourceLocator.getStoreContextsMap(), clientStore); - } - - @Bean - public ConfigListener configListener(AzureCloudConfigRefresh azureCloudConfigWatch) { - return new ConfigListener(azureCloudConfigWatch); + public AppConfigurationRefresh getConfigWatch(AppConfigurationProperties properties, + AppConfigurationPropertySourceLocator sourceLocator, ClientStore clientStore) { + return new AppConfigurationRefresh(properties, sourceLocator.getStoreContextsMap(), clientStore); } } } diff --git a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AzureConfigBootstrapConfiguration.java b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationBootstrapConfiguration.java similarity index 63% rename from spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AzureConfigBootstrapConfiguration.java rename to spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationBootstrapConfiguration.java index 6263ccaa1..0c35324ee 100644 --- a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AzureConfigBootstrapConfiguration.java +++ b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationBootstrapConfiguration.java @@ -22,22 +22,22 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import com.microsoft.azure.spring.cloud.config.resource.AppConfigManagedIdentityProperties; import com.microsoft.azure.spring.cloud.config.resource.Connection; import com.microsoft.azure.spring.cloud.config.resource.ConnectionPool; import com.microsoft.azure.spring.cloud.config.stores.ClientStore; import com.microsoft.azure.spring.cloud.config.stores.ConfigStore; -import com.microsoft.azure.spring.cloud.context.core.config.AzureManagedIdentityProperties; @Configuration -@EnableConfigurationProperties({ AzureCloudConfigProperties.class, AppConfigProviderProperties.class }) -@ConditionalOnClass(AzureConfigPropertySourceLocator.class) -@ConditionalOnProperty(prefix = AzureCloudConfigProperties.CONFIG_PREFIX, name = "enabled", matchIfMissing = true) -public class AzureConfigBootstrapConfiguration { +@EnableConfigurationProperties({ AppConfigurationProperties.class, AppConfigurationProviderProperties.class }) +@ConditionalOnClass(AppConfigurationPropertySourceLocator.class) +@ConditionalOnProperty(prefix = AppConfigurationProperties.CONFIG_PREFIX, name = "enabled", matchIfMissing = true) +public class AppConfigurationBootstrapConfiguration { - private static final Logger LOGGER = LoggerFactory.getLogger(AzureConfigBootstrapConfiguration.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationBootstrapConfiguration.class); @Bean - public ConnectionPool initConnectionString(AzureCloudConfigProperties properties) { + public ConnectionPool initConnectionString(AppConfigurationProperties properties) { ConnectionPool pool = new ConnectionPool(); List stores = properties.getStores(); @@ -45,7 +45,7 @@ public ConnectionPool initConnectionString(AzureCloudConfigProperties properties if (StringUtils.hasText(store.getEndpoint()) && StringUtils.hasText(store.getConnectionString())) { pool.put(store.getEndpoint(), new Connection(store.getConnectionString())); } else if (StringUtils.hasText(store.getEndpoint())) { - AzureManagedIdentityProperties msiProps = properties.getManagedIdentity(); + AppConfigManagedIdentityProperties msiProps = properties.getManagedIdentity(); if (msiProps != null && msiProps.getClientId() != null) { pool.put(store.getEndpoint(), new Connection(store.getEndpoint(), msiProps.getClientId())); } else { @@ -62,39 +62,35 @@ public ConnectionPool initConnectionString(AzureCloudConfigProperties properties @Bean public CloseableHttpClient closeableHttpClient() { - return HttpClients.createDefault(); + return HttpClients.createSystem(); } @Bean - public AzureConfigPropertySourceLocator sourceLocator(AzureCloudConfigProperties properties, - AppConfigProviderProperties appProperties, ClientStore clients, ApplicationContext context) { + public AppConfigurationPropertySourceLocator sourceLocator(AppConfigurationProperties properties, + AppConfigurationProviderProperties appProperties, ClientStore clients, ApplicationContext context) { KeyVaultCredentialProvider keyVaultCredentialProvider = null; try { keyVaultCredentialProvider = context.getBean(KeyVaultCredentialProvider.class); } catch (NoUniqueBeanDefinitionException e) { - LOGGER.error("Failed to find unique TokenCredentialProvider Bean for authentication.", e); - if (properties.isFailFast()) { - throw e; - } + throw new RuntimeException("Failed to find unique KeyVaultCredentialProvider Bean for authentication.", e); } catch (NoSuchBeanDefinitionException e) { - LOGGER.info("No TokenCredentialProvider found."); + LOGGER.debug("No KeyVaultCredentialProvider found."); } - return new AzureConfigPropertySourceLocator(properties, appProperties, clients, keyVaultCredentialProvider); + return new AppConfigurationPropertySourceLocator(properties, appProperties, clients, + keyVaultCredentialProvider); } @Bean - public ClientStore buildClientStores(AzureCloudConfigProperties properties, - AppConfigProviderProperties appProperties, ConnectionPool pool, ApplicationContext context) { - AppConfigCredentialProvider tokenCredentialProvider = null; + public ClientStore buildClientStores(AppConfigurationProperties properties, + AppConfigurationProviderProperties appProperties, ConnectionPool pool, ApplicationContext context) { + AppConfigurationCredentialProvider tokenCredentialProvider = null; try { - tokenCredentialProvider = context.getBean(AppConfigCredentialProvider.class); + tokenCredentialProvider = context.getBean(AppConfigurationCredentialProvider.class); } catch (NoUniqueBeanDefinitionException e) { - LOGGER.error("Failed to find unique TokenCredentialProvider Bean for authentication.", e); - if (properties.isFailFast()) { - throw e; - } + throw new RuntimeException( + "Failed to find unique AppConfigurationCredentialProvider Bean for authentication.", e); } catch (NoSuchBeanDefinitionException e) { - LOGGER.info("No TokenCredentialProvider found."); + LOGGER.debug("No AppConfigurationCredentialProvider found."); } return new ClientStore(appProperties, pool, tokenCredentialProvider); } diff --git a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigCredentialProvider.java b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationCredentialProvider.java similarity index 85% rename from spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigCredentialProvider.java rename to spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationCredentialProvider.java index 782658f7c..d65ad9d36 100644 --- a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigCredentialProvider.java +++ b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationCredentialProvider.java @@ -7,7 +7,7 @@ import com.azure.core.credential.TokenCredential; -public interface AppConfigCredentialProvider { +public interface AppConfigurationCredentialProvider { public TokenCredential getAppConfigCredential(String uri); diff --git a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AzureCloudConfigProperties.java b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationProperties.java similarity index 84% rename from spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AzureCloudConfigProperties.java rename to spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationProperties.java index 95f2ca656..06cb5cf24 100644 --- a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AzureCloudConfigProperties.java +++ b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationProperties.java @@ -22,13 +22,13 @@ import org.springframework.util.StringUtils; import org.springframework.validation.annotation.Validated; +import com.microsoft.azure.spring.cloud.config.resource.AppConfigManagedIdentityProperties; import com.microsoft.azure.spring.cloud.config.stores.ConfigStore; -import com.microsoft.azure.spring.cloud.context.core.config.AzureManagedIdentityProperties; @Validated -@ConfigurationProperties(prefix = AzureCloudConfigProperties.CONFIG_PREFIX) -@Import({ AppConfigProviderProperties.class }) -public class AzureCloudConfigProperties { +@ConfigurationProperties(prefix = AppConfigurationProperties.CONFIG_PREFIX) +@Import({ AppConfigurationProviderProperties.class }) +public class AppConfigurationProperties { public static final String CONFIG_PREFIX = "spring.cloud.azure.appconfiguration"; public static final String LABEL_SEPARATOR = ","; @@ -46,15 +46,13 @@ public class AzureCloudConfigProperties { private String name; @NestedConfigurationProperty - private AzureManagedIdentityProperties managedIdentity; + private AppConfigManagedIdentityProperties managedIdentity; // Profile separator for the key name, e.g., /foo-app_dev/db.connection.key @NotEmpty @Pattern(regexp = "^[a-zA-Z0-9_@]+$") private String profileSeparator = "_"; - private boolean failFast = true; - private Duration cacheExpiration = Duration.ofSeconds(30); public boolean isEnabled() { @@ -90,11 +88,11 @@ public void setName(@Nullable String name) { this.name = name; } - public AzureManagedIdentityProperties getManagedIdentity() { + public AppConfigManagedIdentityProperties getManagedIdentity() { return managedIdentity; } - public void setManagedIdentity(AzureManagedIdentityProperties managedIdentity) { + public void setManagedIdentity(AppConfigManagedIdentityProperties managedIdentity) { this.managedIdentity = managedIdentity; } @@ -106,14 +104,6 @@ public void setProfileSeparator(String profileSeparator) { this.profileSeparator = profileSeparator; } - public boolean isFailFast() { - return failFast; - } - - public void setFailFast(boolean failFast) { - this.failFast = failFast; - } - public Duration getCacheExpiration() { return cacheExpiration; } @@ -122,7 +112,7 @@ public Duration getCacheExpiration() { * The minimum time between checks. The minimum valid cache time is 1s. The default * cache time is 30s. * - * @param cache minimum time between refresh checks + * @param cacheExpiration minimum time between refresh checks */ public void setCacheExpiration(Duration cacheExpiration) { this.cacheExpiration = cacheExpiration; diff --git a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AzureConfigPropertySource.java b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationPropertySource.java similarity index 80% rename from spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AzureConfigPropertySource.java rename to spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationPropertySource.java index 5f87a232c..2b22c7a63 100644 --- a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AzureConfigPropertySource.java +++ b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationPropertySource.java @@ -35,6 +35,7 @@ import com.azure.security.keyvault.secrets.models.KeyVaultSecret; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.microsoft.azure.spring.cloud.config.feature.management.entity.Feature; import com.microsoft.azure.spring.cloud.config.feature.management.entity.FeatureManagementItem; import com.microsoft.azure.spring.cloud.config.feature.management.entity.FeatureSet; @@ -42,8 +43,8 @@ import com.microsoft.azure.spring.cloud.config.stores.ConfigStore; import com.microsoft.azure.spring.cloud.config.stores.KeyVaultClient; -public class AzureConfigPropertySource extends EnumerablePropertySource { - private static final Logger LOGGER = LoggerFactory.getLogger(AzureConfigPropertySource.class); +public class AppConfigurationPropertySource extends EnumerablePropertySource { + private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationPropertySource.class); private final String context; @@ -51,7 +52,7 @@ public class AzureConfigPropertySource extends EnumerablePropertySource> storeContextsMap; - AzureConfigPropertySource(String context, ConfigStore configStore, String label, - AzureCloudConfigProperties azureProperties, ClientStore clients, AppConfigProviderProperties appProperties, - KeyVaultCredentialProvider keyVaultCredentialProvider, Map> storeContextsMap) { + AppConfigurationPropertySource(String context, ConfigStore configStore, String label, + AppConfigurationProperties appConfigurationProperties, ClientStore clients, + AppConfigurationProviderProperties appProperties, KeyVaultCredentialProvider keyVaultCredentialProvider, + Map> storeContextsMap) { // The context alone does not uniquely define a PropertySource, append storeName // and label to uniquely define a PropertySource super(context + configStore.getEndpoint() + "/" + label); this.context = context; this.configStore = configStore; this.label = label; - this.azureProperties = azureProperties; + this.appConfigurationProperties = appConfigurationProperties; this.appProperties = appProperties; this.keyVaultClients = new HashMap(); this.clients = clients; @@ -103,7 +105,7 @@ public Object getProperty(String name) { *

* Note: Doesn't update Feature Management, just stores values in cache. Call * {@code initFeatures} to update Feature Management, but make sure its done in the - * last {@code AzureConfigPropertySource} + * last {@code AppConfigurationPropertySource} *

* * @param featureSet The set of Feature Management Flags from various config stores. @@ -127,12 +129,8 @@ FeatureSet initProperties(FeatureSet featureSet) throws IOException { settingSelector.setKeyFilter(".appconfig*"); List features = clients.listSettings(settingSelector, storeName); - if (settings == null) { - if (!azureProperties.isFailFast()) { - return featureSet; - } else { - throw new IOException("Unable to load properties from App Configuration Store."); - } + if (settings == null || features == null) { + throw new IOException("Unable to load properties from App Configuration Store."); } for (ConfigurationSetting setting : settings) { String key = setting.getKey().trim().substring(context.length()).replace('/', '.'); @@ -163,12 +161,17 @@ FeatureSet initProperties(FeatureSet featureSet) throws IOException { List featureRevisions = clients.listSettingRevisons(settingSelector, storeName); if (configurationRevisions != null && !configurationRevisions.isEmpty()) { - StateHolder.setState(configStore.getEndpoint() + CONFIGURATION_SUFFIX, configurationRevisions.get(0)); + StateHolder.setEtagState(configStore.getEndpoint() + CONFIGURATION_SUFFIX, configurationRevisions.get(0)); + } else { + StateHolder.setEtagState(configStore.getEndpoint() + CONFIGURATION_SUFFIX, new ConfigurationSetting()); } if (featureRevisions != null && !featureRevisions.isEmpty()) { - StateHolder.setState(configStore.getEndpoint() + FEATURE_SUFFIX, featureRevisions.get(0)); + StateHolder.setEtagState(configStore.getEndpoint() + FEATURE_SUFFIX, featureRevisions.get(0)); + } else { + StateHolder.setEtagState(configStore.getEndpoint() + FEATURE_SUFFIX, new ConfigurationSetting()); } + StateHolder.setLoadState(configStore.getEndpoint(), true); return featureSet; } @@ -191,28 +194,20 @@ private String getKeyVaultEntry(String value) { JsonNode kvReference = mapper.readTree(value); uri = new URI(kvReference.at("/uri").asText()); } catch (URISyntaxException e) { - if (azureProperties.isFailFast()) { - LOGGER.error("Error Processing Key Vault Entry URI."); - ReflectionUtils.rethrowRuntimeException(e); - } else { - LOGGER.error("Error Processing Key Vault Entry URI.", e); - } + LOGGER.error("Error Processing Key Vault Entry URI."); + ReflectionUtils.rethrowRuntimeException(e); } // If no entry found don't connect to Key Vault if (uri == null) { - if (azureProperties.isFailFast()) { - ReflectionUtils - .rethrowRuntimeException(new IOException("Invaid URI when parsing Key Vault Reference.")); - } else { - return null; - } + ReflectionUtils.rethrowRuntimeException( + new IOException("Invaid URI when parsing Key Vault Reference.")); } // Check if we already have a client for this key vault, if not we will make // one if (!keyVaultClients.containsKey(uri.getHost())) { - KeyVaultClient client = new KeyVaultClient(uri, keyVaultCredentialProvider, azureProperties); + KeyVaultClient client = new KeyVaultClient(appConfigurationProperties, uri, keyVaultCredentialProvider); keyVaultClients.put(uri.getHost(), client); } KeyVaultSecret secret = keyVaultClients.get(uri.getHost()).getSecret(uri, appProperties.getMaxRetryTime()); @@ -221,26 +216,23 @@ private String getKeyVaultEntry(String value) { } secretValue = secret.getValue(); } catch (RuntimeException | IOException e) { - if (!azureProperties.isFailFast()) { - LOGGER.error("Error Retreiving Key Vault Entry", e); - } else { - LOGGER.error("Error Retreiving Key Vault Entry"); - ReflectionUtils.rethrowRuntimeException(e); - } + LOGGER.error("Error Retreiving Key Vault Entry"); + ReflectionUtils.rethrowRuntimeException(e); } return secretValue; } /** * Initializes Feature Management configurations. Only one - * {@code AzureConfigPropertySource} can call this, and it needs to be done after the - * rest have run initProperties. - * + * {@code AppConfigurationPropertySource} can call this, and it needs to be done after + * the rest have run initProperties. * @param featureSet Feature Flag info to be set to this property source. */ void initFeatures(FeatureSet featureSet) { + ObjectMapper featureMapper = new ObjectMapper(); + featureMapper.setPropertyNamingStrategy(PropertyNamingStrategy.KEBAB_CASE); properties.put(FEATURE_MANAGEMENT_KEY, - mapper.convertValue(featureSet.getFeatureManagement(), LinkedHashMap.class)); + featureMapper.convertValue(featureSet.getFeatureManagement(), LinkedHashMap.class)); } /** @@ -293,21 +285,13 @@ private Object createFeature(ConfigurationSetting item) throws IOException { return feature; } catch (IOException e) { - LOGGER.error("Unabled to parse Feature Management values from Azure.", e); - if (azureProperties.isFailFast()) { - throw e; - } + throw new IOException("Unabled to parse Feature Management values from Azure.", e); } } else { String message = String.format("Found Feature Flag %s with invalid Content Type of %s", item.getKey(), item.getContentType()); - - if (azureProperties.isFailFast()) { - throw new IOException(message); - } - LOGGER.error(message); + throw new IOException(message); } - return feature; } } diff --git a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AzureConfigPropertySourceLocator.java b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationPropertySourceLocator.java similarity index 81% rename from spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AzureConfigPropertySourceLocator.java rename to spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationPropertySourceLocator.java index 0ae89abab..7af1d992c 100644 --- a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AzureConfigPropertySourceLocator.java +++ b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationPropertySourceLocator.java @@ -26,13 +26,12 @@ import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; -import com.google.common.collect.Lists; import com.microsoft.azure.spring.cloud.config.feature.management.entity.FeatureSet; import com.microsoft.azure.spring.cloud.config.stores.ClientStore; import com.microsoft.azure.spring.cloud.config.stores.ConfigStore; -public class AzureConfigPropertySourceLocator implements PropertySourceLocator { - private static final Logger LOGGER = LoggerFactory.getLogger(AzureConfigPropertySourceLocator.class); +public class AppConfigurationPropertySourceLocator implements PropertySourceLocator { + private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationPropertySourceLocator.class); private static final String SPRING_APP_NAME_PROP = "spring.application.name"; @@ -40,7 +39,7 @@ public class AzureConfigPropertySourceLocator implements PropertySourceLocator { private static final String PATH_SPLITTER = "/"; - private final AzureCloudConfigProperties properties; + private final AppConfigurationProperties properties; private final String profileSeparator; @@ -48,14 +47,16 @@ public class AzureConfigPropertySourceLocator implements PropertySourceLocator { private final Map> storeContextsMap = new ConcurrentHashMap<>(); - private AppConfigProviderProperties appProperties; + private AppConfigurationProviderProperties appProperties; private ClientStore clients; private KeyVaultCredentialProvider keyVaultCredentialProvider; - public AzureConfigPropertySourceLocator(AzureCloudConfigProperties properties, - AppConfigProviderProperties appProperties, ClientStore clients, + private static Boolean startup = true; + + public AppConfigurationPropertySourceLocator(AppConfigurationProperties properties, + AppConfigurationProviderProperties appProperties, ClientStore clients, KeyVaultCredentialProvider keyVaultCredentialProvider) { this.properties = properties; this.appProperties = appProperties; @@ -87,10 +88,16 @@ public PropertySource locate(Environment environment) { // Feature Management needs to be set in the last config store. while (configStoreIterator.hasNext()) { ConfigStore configStore = configStoreIterator.next(); - addPropertySource(composite, configStore, applicationName, profiles, storeContextsMap, - !configStoreIterator.hasNext()); + if (startup || (!startup && StateHolder.getLoadState(configStore.getEndpoint()))) { + addPropertySource(composite, configStore, applicationName, profiles, storeContextsMap, + !configStoreIterator.hasNext()); + } else { + LOGGER.warn("Not loading configurations from {} as it failed on startup.", configStore.getEndpoint()); + } } + startup = false; + return composite; } @@ -121,7 +128,7 @@ private void addPropertySource(CompositePropertySource composite, ConfigStore st contexts.addAll(generateContexts(this.properties.getDefaultContext(), profiles, store)); contexts.addAll(generateContexts(applicationName, profiles, store)); - // There is only one Feature Set for all AzureConfigPropertySources + // There is only one Feature Set for all AppConfigurationPropertySources FeatureSet featureSet = new FeatureSet(); // Reverse in order to add Profile specific properties earlier, and last profile @@ -129,17 +136,20 @@ private void addPropertySource(CompositePropertySource composite, ConfigStore st Collections.reverse(contexts); for (String sourceContext : contexts) { try { - List sourceList = create(sourceContext, store, storeContextsMap, + List sourceList = create(sourceContext, store, storeContextsMap, initFeatures, featureSet); sourceList.forEach(composite::addPropertySource); LOGGER.debug("PropertySource context [{}] is added.", sourceContext); } catch (Exception e) { - if (properties.isFailFast()) { - LOGGER.error("Fail fast is set and there was an error reading configuration from Azure Config " + - "Service for " + sourceContext); + if (store.isFailFast() || !startup) { + LOGGER.error( + "Fail fast is set and there was an error reading configuration from Azure App " + + "Configuration Service for " + sourceContext); ReflectionUtils.rethrowRuntimeException(e); } else { - LOGGER.warn("Unable to load configuration from Azure Config Service for " + sourceContext, e); + LOGGER.warn("Unable to load configuration from Azure AppConfiguration Service for " + sourceContext, + e); + StateHolder.setLoadState(store.getEndpoint(), false); } } } @@ -174,23 +184,23 @@ private String propWithProfile(String context, String profile) { } /** - * Creates a new set of AzureConfigProertySources, 1 per Label. + * Creates a new set of AppConfigurationProertySources, 1 per Label. * * @param context Context of the application, part of uniquely define a PropertySource * @param store Config Store the PropertySource is being generated from * @param storeContextsMap the Map storing the storeName -> List of contexts map * @param initFeatures determines if Feature Management is set in the PropertySource. * When generating more than one it needs to be in the last one. - * @return a list of AzureConfigPropertySources + * @return a list of AppConfigurationPropertySources */ - private List create(String context, ConfigStore store, + private List create(String context, ConfigStore store, Map> storeContextsMap, boolean initFeatures, FeatureSet featureSet) throws Exception { - List sourceList = new ArrayList<>(); + List sourceList = new ArrayList<>(); try { for (String label : store.getLabels()) { putStoreContext(store.getEndpoint(), context, storeContextsMap); - AzureConfigPropertySource propertySource = new AzureConfigPropertySource(context, store, + AppConfigurationPropertySource propertySource = new AppConfigurationPropertySource(context, store, label, properties, clients, appProperties, keyVaultCredentialProvider, storeContextsMap); propertySource.initProperties(featureSet); @@ -221,10 +231,9 @@ private void putStoreContext(String storeName, String context, List contexts = storeContextsMap.get(storeName); if (contexts == null) { - contexts = Lists.newArrayList(context); - } else { - contexts.add(context); + contexts = new ArrayList(); } + contexts.add(context); storeContextsMap.put(storeName, contexts); } diff --git a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigProviderProperties.java b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationProviderProperties.java similarity index 94% rename from spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigProviderProperties.java rename to spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationProviderProperties.java index ed3199579..400b88bfc 100644 --- a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigProviderProperties.java +++ b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationProviderProperties.java @@ -19,8 +19,8 @@ @Configuration @Validated @PropertySource("classpath:appConfiguration.yaml") -@ConfigurationProperties(prefix = AppConfigProviderProperties.CONFIG_PREFIX) -public class AppConfigProviderProperties { +@ConfigurationProperties(prefix = AppConfigurationProviderProperties.CONFIG_PREFIX) +public class AppConfigurationProviderProperties { public static final String CONFIG_PREFIX = "spring.cloud.appconfiguration"; @NotEmpty diff --git a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AzureCloudConfigRefresh.java b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationRefresh.java similarity index 88% rename from spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AzureCloudConfigRefresh.java rename to spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationRefresh.java index 3d89b4b91..dc960833a 100644 --- a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AzureCloudConfigRefresh.java +++ b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/AppConfigurationRefresh.java @@ -30,8 +30,8 @@ import com.microsoft.azure.spring.cloud.config.stores.ClientStore; import com.microsoft.azure.spring.cloud.config.stores.ConfigStore; -public class AzureCloudConfigRefresh implements ApplicationEventPublisherAware { - private static final Logger LOGGER = LoggerFactory.getLogger(AzureCloudConfigRefresh.class); +public class AppConfigurationRefresh implements ApplicationEventPublisherAware { + private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationRefresh.class); private final AtomicBoolean running = new AtomicBoolean(false); @@ -49,7 +49,7 @@ public class AzureCloudConfigRefresh implements ApplicationEventPublisherAware { private String eventDataInfo; - public AzureCloudConfigRefresh(AzureCloudConfigProperties properties, Map> storeContextsMap, + public AppConfigurationRefresh(AppConfigurationProperties properties, Map> storeContextsMap, ClientStore clientStore) { this.configStores = properties.getStores(); this.storeContextsMap = storeContextsMap; @@ -80,7 +80,6 @@ public Future refreshConfigurations() { * Goes through each config store and checks if any of its keys need to be refreshed. * If any store has a value that needs to be updated a refresh event is called after * every store is checked. - * * @return If a refresh event is called. */ private boolean refreshStores() { @@ -97,11 +96,14 @@ private boolean refreshStores() { Date date = new Date(); if (notCachedTime == null || date.after(notCachedTime)) { for (ConfigStore configStore : configStores) { - String watchedKeyNames = clientStore.watchedKeyNames(configStore, storeContextsMap); - willRefresh = refresh(configStore, CONFIGURATION_SUFFIX, watchedKeyNames) ? true : willRefresh; - // Refresh Feature Flags - willRefresh = refresh(configStore, FEATURE_SUFFIX, FEATURE_STORE_WATCH_KEY) ? true - : willRefresh; + if (StateHolder.getLoadState(configStore.getEndpoint())) { + String watchedKeyNames = clientStore.watchedKeyNames(configStore, storeContextsMap); + willRefresh = refresh(configStore, CONFIGURATION_SUFFIX, watchedKeyNames) ? true + : willRefresh; + // Refresh Feature Flags + willRefresh = refresh(configStore, FEATURE_SUFFIX, FEATURE_STORE_WATCH_KEY) ? true + : willRefresh; + } } // Resetting last Checked date to now. lastCheckedTime = new Date(); @@ -142,7 +144,7 @@ private boolean refresh(ConfigStore store, String storeSuffix, String watchedKey etag = items.get(0).getETag(); } - if (StateHolder.getState(storeNameWithSuffix) == null) { + if (StateHolder.getEtagState(storeNameWithSuffix) == null) { // Should never be the case as Property Source should set the state, but if // etag != null return true. if (etag != null) { @@ -151,7 +153,7 @@ private boolean refresh(ConfigStore store, String storeSuffix, String watchedKey return false; } - if (!etag.equals(StateHolder.getState(storeNameWithSuffix).getETag())) { + if (!etag.equals(StateHolder.getEtagState(storeNameWithSuffix).getETag())) { LOGGER.trace("Some keys in store [{}] matching [{}] is updated, will send refresh event.", store.getEndpoint(), watchedKeyNames); if (this.eventDataInfo.isEmpty()) { diff --git a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/HostType.java b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/HostType.java index 8eb8392a0..326f9208e 100644 --- a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/HostType.java +++ b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/HostType.java @@ -10,9 +10,10 @@ */ public enum HostType { - NONE("None"), + UNIDENTIFIED(""), AZURE_WEB_APP("AzureWebApp"), - AZURE_FUNCTION("AzureFunction"); + AZURE_FUNCTION("AzureFunction"), + KUBERNETES("Kubernetes"); private final String text; diff --git a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/RequestTracingConstants.java b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/RequestTracingConstants.java index 990f5f63b..c44462561 100644 --- a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/RequestTracingConstants.java +++ b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/RequestTracingConstants.java @@ -10,11 +10,13 @@ */ public enum RequestTracingConstants { - AZURE_APP_CONFIGURATION_TRACING_DISABLED("AZURE_APP_CONFIGURATION_TRACING_DISABLED"), - FUNCTIONS_EXTENSION_VERSION("FUNCTIONS_EXTENSION_VERSION"), - WEBSITE_NODE_DEFAULT_VERSION("WEBSITE_NODE_DEFAULT_VERSION"), - REQUEST_TYPE("RequestType"), - HOST("Host"); + REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE("AZURE_APP_CONFIGURATION_TRACING_DISABLED"), + AZURE_FUNCTIONS_ENVIRONMENT_VARIABLE("FUNCTIONS_EXTENSION_VERSION"), + AZURE_WEB_APP_ENVIRONMENT_VARIABLE("WEBSITE_SITE_NAME"), + KUBERNETES_ENVIRONMENT_VARIABLE("KUBERNETES_PORT"), + REQUEST_TYPE_KEY("RequestType"), + HOST_TYPE_KEY("Host"), + CORRELATION_CONTEXT_HEADER("Correlation-Context"); private final String text; diff --git a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/StateHolder.java b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/StateHolder.java index e8f3ec399..77b5e3e4a 100644 --- a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/StateHolder.java +++ b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/StateHolder.java @@ -9,21 +9,44 @@ import com.azure.data.appconfiguration.models.ConfigurationSetting; -public final class StateHolder { +final class StateHolder { private StateHolder() { throw new IllegalStateException("Should not be callable."); } - private static ConcurrentHashMap state = + private static ConcurrentHashMap etagState = new ConcurrentHashMap(); - public static ConfigurationSetting getState(String name) { - return state.get(name); + private static ConcurrentHashMap loadState = new ConcurrentHashMap(); + + /** + * @return the etagState + */ + public static ConfigurationSetting getEtagState(String name) { + return etagState.get(name); + } + + /** + * @param etagState the etagState to set + */ + static void setEtagState(String name, ConfigurationSetting config) { + etagState.put(name, config); + } + + /** + * @return the loadState + */ + public static Boolean getLoadState(String name) { + Boolean loadstate = loadState.get(name); + return loadstate == null ? false : loadstate; } - public static void setState(String name, ConfigurationSetting setting) { - state.put(name, setting); + /** + * @param loadState the loadState to set + */ + public static void setLoadState(String name, Boolean loaded) { + loadState.put(name, loaded); } } diff --git a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/feature/management/entity/Feature.java b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/feature/management/entity/Feature.java index 916840560..593350ae9 100644 --- a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/feature/management/entity/Feature.java +++ b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/feature/management/entity/Feature.java @@ -5,6 +5,7 @@ */ package com.microsoft.azure.spring.cloud.config.feature.management.entity; +import java.util.HashMap; import java.util.List; import com.fasterxml.jackson.annotation.JsonAlias; @@ -17,14 +18,20 @@ public class Feature { @JsonProperty("key") private String key; - @JsonAlias("EnabledFor") - private List enabledFor; + @JsonAlias("enabled-for") + private HashMap enabledFor; public Feature() {} public Feature(String key, FeatureManagementItem featureItem) { this.key = key; - this.enabledFor = featureItem.getConditions().getClientFilters(); + List filterMapper = featureItem.getConditions().getClientFilters(); + + enabledFor = new HashMap(); + + for (int i = 0; i < filterMapper.size(); i++) { + enabledFor.put(i, filterMapper.get(i)); + } } /** @@ -44,14 +51,14 @@ public void setKey(String key) { /** * @return the enabledFor */ - public List getEnabledFor() { + public HashMap getEnabledFor() { return enabledFor; } /** * @param enabledFor the enabledFor to set */ - public void setEnabledFor(List enabledFor) { + public void setEnabledFor(HashMap enabledFor) { this.enabledFor = enabledFor; } diff --git a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/feature/management/entity/FeatureFilterEvaluationContext.java b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/feature/management/entity/FeatureFilterEvaluationContext.java index 39e5009da..df3bb4e72 100644 --- a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/feature/management/entity/FeatureFilterEvaluationContext.java +++ b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/feature/management/entity/FeatureFilterEvaluationContext.java @@ -14,12 +14,12 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class FeatureFilterEvaluationContext { - @JsonProperty("Name") - @JsonAlias("name") + @JsonProperty("name") + @JsonAlias("Name") private String name; - @JsonProperty("Parameters") - @JsonAlias("parameters") + @JsonProperty("parameters") + @JsonAlias("Parameters") private LinkedHashMap parameters; /** diff --git a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/feature/management/entity/FeatureSet.java b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/feature/management/entity/FeatureSet.java index 0e07678cc..f2360de36 100644 --- a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/feature/management/entity/FeatureSet.java +++ b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/feature/management/entity/FeatureSet.java @@ -22,13 +22,6 @@ public class FeatureSet { public HashMap getFeatureManagement() { return featureManagement; } - - /** - * @param featureManagement the featureManagement to set - */ - public void setFeatureManagement(HashMap featureManagement) { - this.featureManagement = featureManagement; - } public void addFeature(String key, Object feature) { if (featureManagement == null) { diff --git a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/pipline/policies/BaseAppConfigurationPolicy.java b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/pipline/policies/BaseAppConfigurationPolicy.java index 48a9b9c18..1b9afaf94 100644 --- a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/pipline/policies/BaseAppConfigurationPolicy.java +++ b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/pipline/policies/BaseAppConfigurationPolicy.java @@ -34,7 +34,8 @@ public class BaseAppConfigurationPolicy implements HttpPipelinePolicy { public Mono process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) { String sdkUserAgent = context.getHttpRequest().getHeaders().get(HttpHeaders.USER_AGENT).getValue(); context.getHttpRequest().getHeaders().put(HttpHeaders.USER_AGENT, USER_AGENT + " " + sdkUserAgent); - context.getHttpRequest().getHeaders().put("Correlation-Context", getTracingInfo(context.getHttpRequest())); + context.getHttpRequest().getHeaders().put(RequestTracingConstants.CORRELATION_CONTEXT_HEADER.toString(), + getTracingInfo(context.getHttpRequest())); return next.process(); } @@ -48,7 +49,7 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN * @throws URISyntaxException */ private static String getTracingInfo(HttpRequest request) { - String track = System.getenv(RequestTracingConstants.AZURE_APP_CONFIGURATION_TRACING_DISABLED.toString()); + String track = System.getenv(RequestTracingConstants.REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE.toString()); if (track != null && track.equalsIgnoreCase("false")) { return ""; } @@ -60,25 +61,32 @@ private static String getTracingInfo(HttpRequest request) { if (requestTypeValue.equals(RequestType.WATCH.toString())) { watchRequests = true; } - String requestType = RequestTracingConstants.REQUEST_TYPE.toString() + "=" + requestTypeValue; - String host = RequestTracingConstants.HOST + "=" + getHostType(); + String tracingInfo = RequestTracingConstants.REQUEST_TYPE_KEY.toString() + "=" + requestTypeValue; + String hostType = getHostType(); - return requestType + "," + host; + if (!hostType.isEmpty()) { + tracingInfo += "," + RequestTracingConstants.HOST_TYPE_KEY + "=" + getHostType(); + } + + return tracingInfo; } /** - * Gets the current host machines type; Azure Function, Azure Web App, or None. + * Gets the current host machines type; Azure Function, Azure Web App, Kubernetes, or Empty. * * @return String of Host Type */ private static String getHostType() { - String azureFunctionVersion = System.getenv(RequestTracingConstants.FUNCTIONS_EXTENSION_VERSION.toString()); - String azureWebsiteVersion = System.getenv(RequestTracingConstants.WEBSITE_NODE_DEFAULT_VERSION.toString()); - HostType hostType = azureFunctionVersion != null ? HostType.AZURE_FUNCTION - : azureWebsiteVersion != null - ? HostType.AZURE_WEB_APP - : HostType.NONE; + HostType hostType = HostType.UNIDENTIFIED; + + if (System.getenv(RequestTracingConstants.AZURE_FUNCTIONS_ENVIRONMENT_VARIABLE.toString()) != null) { + hostType = HostType.AZURE_FUNCTION; + } else if (System.getenv(RequestTracingConstants.AZURE_WEB_APP_ENVIRONMENT_VARIABLE.toString()) != null) { + hostType = HostType.AZURE_WEB_APP; + } else if (System.getenv(RequestTracingConstants.KUBERNETES_ENVIRONMENT_VARIABLE.toString()) != null) { + hostType = HostType.KUBERNETES; + } return hostType.toString(); diff --git a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/resource/AppConfigManagedIdentityProperties.java b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/resource/AppConfigManagedIdentityProperties.java new file mode 100644 index 000000000..627997d6b --- /dev/null +++ b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/resource/AppConfigManagedIdentityProperties.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for + * license information. + */ +package com.microsoft.azure.spring.cloud.config.resource; + +import org.springframework.lang.Nullable; + +public class AppConfigManagedIdentityProperties { + @Nullable + private String clientId; // Optional: client_id of the managed identity + + @Nullable + public String getClientId() { + return clientId; + } + + public void setClientId(@Nullable String clientId) { + this.clientId = clientId; + } +} diff --git a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/stores/ClientStore.java b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/stores/ClientStore.java index 7d6d24e60..5df862d44 100644 --- a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/stores/ClientStore.java +++ b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/stores/ClientStore.java @@ -23,29 +23,29 @@ import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.data.appconfiguration.models.SettingSelector; import com.azure.identity.ManagedIdentityCredentialBuilder; -import com.microsoft.azure.spring.cloud.config.AppConfigCredentialProvider; -import com.microsoft.azure.spring.cloud.config.AppConfigProviderProperties; +import com.microsoft.azure.spring.cloud.config.AppConfigurationCredentialProvider; +import com.microsoft.azure.spring.cloud.config.AppConfigurationProviderProperties; import com.microsoft.azure.spring.cloud.config.pipline.policies.BaseAppConfigurationPolicy; import com.microsoft.azure.spring.cloud.config.resource.Connection; import com.microsoft.azure.spring.cloud.config.resource.ConnectionPool; public class ClientStore { - private AppConfigProviderProperties appProperties; + private AppConfigurationProviderProperties appProperties; private ConnectionPool pool; - private AppConfigCredentialProvider tokenCredentialProvider; + private AppConfigurationCredentialProvider tokenCredentialProvider; - public ClientStore(AppConfigProviderProperties appProperties, - ConnectionPool pool, AppConfigCredentialProvider tokenCredentialProvider) { + public ClientStore(AppConfigurationProviderProperties appProperties, + ConnectionPool pool, AppConfigurationCredentialProvider tokenCredentialProvider) { this.appProperties = appProperties; this.pool = pool; this.tokenCredentialProvider = tokenCredentialProvider; } private ConfigurationAsyncClient buildClient(String store) throws IllegalArgumentException { - ConfigurationClientBuilder builder = new ConfigurationClientBuilder(); + ConfigurationClientBuilder builder = getBuilder(); ExponentialBackoff retryPolicy = new ExponentialBackoff(appProperties.getMaxRetries(), Duration.ofMillis(800), Duration.ofSeconds(8)); builder = builder.addPolicy(new BaseAppConfigurationPolicy()).retryPolicy(new RetryPolicy( @@ -103,7 +103,6 @@ private ConfigurationAsyncClient buildClient(String store) throws IllegalArgumen */ public final List listSettingRevisons(SettingSelector settingSelector, String storeName) { ConfigurationAsyncClient client = buildClient(storeName); - return client.listRevisions(settingSelector).collectList().block(); } @@ -157,4 +156,8 @@ private String genKey(@NonNull String context, @Nullable String watchedKey) { return String.format("%s%s", context, trimmedWatchedKey); } + + ConfigurationClientBuilder getBuilder() { + return new ConfigurationClientBuilder(); + } } diff --git a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/stores/ConfigStore.java b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/stores/ConfigStore.java index 5c876e8b2..c3b818784 100644 --- a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/stores/ConfigStore.java +++ b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/stores/ConfigStore.java @@ -5,7 +5,7 @@ */ package com.microsoft.azure.spring.cloud.config.stores; -import static com.microsoft.azure.spring.cloud.config.AzureCloudConfigProperties.LABEL_SEPARATOR; +import static com.microsoft.azure.spring.cloud.config.AppConfigurationProperties.LABEL_SEPARATOR; import java.net.URI; import java.net.URISyntaxException; @@ -41,6 +41,8 @@ public class ConfigStore { // The keys to be watched, won't take effect if watch not enabled @NotEmpty private String watchedKey = "*"; + + private boolean failFast = true; public ConfigStore() { } @@ -84,6 +86,14 @@ public String getWatchedKey() { public void setWatchedKey(String watchedKey) { this.watchedKey = watchedKey; } + + public boolean isFailFast() { + return failFast; + } + + public void setFailFast(boolean failFast) { + this.failFast = failFast; + } @PostConstruct public void validateAndInit() { diff --git a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/stores/KeyVaultClient.java b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/stores/KeyVaultClient.java index 9520800b6..3957735ec 100644 --- a/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/stores/KeyVaultClient.java +++ b/spring-cloud-azure-appconfiguration-config/src/main/java/com/microsoft/azure/spring/cloud/config/stores/KeyVaultClient.java @@ -15,13 +15,19 @@ import com.azure.security.keyvault.secrets.SecretAsyncClient; import com.azure.security.keyvault.secrets.SecretClientBuilder; import com.azure.security.keyvault.secrets.models.KeyVaultSecret; -import com.microsoft.azure.spring.cloud.config.AzureCloudConfigProperties; +import com.microsoft.azure.spring.cloud.config.AppConfigurationProperties; import com.microsoft.azure.spring.cloud.config.KeyVaultCredentialProvider; -import com.microsoft.azure.spring.cloud.context.core.config.AzureManagedIdentityProperties; +import com.microsoft.azure.spring.cloud.config.resource.AppConfigManagedIdentityProperties; public class KeyVaultClient { private SecretAsyncClient secretClient; + + private AppConfigurationProperties properties; + + private URI uri; + + private TokenCredential tokenCredential; /** * Builds an Async client to a Key Vaults Secrets @@ -31,15 +37,18 @@ public class KeyVaultClient { * Vault * @param properties Azure Configuration Managed Identity credentials */ - public KeyVaultClient(URI uri, KeyVaultCredentialProvider tokenCredentialProvider, - AzureCloudConfigProperties properties) { - SecretClientBuilder builder = new SecretClientBuilder(); - TokenCredential tokenCredential = null; + public KeyVaultClient(AppConfigurationProperties properties, URI uri, + KeyVaultCredentialProvider tokenCredentialProvider) { + this.properties = properties; + this.uri = uri; if (tokenCredentialProvider != null) { - tokenCredential = tokenCredentialProvider.getKeyVaultCredential("https://" + uri.getHost()); + this.tokenCredential = tokenCredentialProvider.getKeyVaultCredential("https://" + uri.getHost()); } - - AzureManagedIdentityProperties msiProps = properties.getManagedIdentity(); + } + + KeyVaultClient build() { + SecretClientBuilder builder = getBuilder(); + AppConfigManagedIdentityProperties msiProps = properties.getManagedIdentity(); if (tokenCredential != null && msiProps != null) { throw new IllegalArgumentException("More than 1 Conncetion method was set for connecting to Key Vault."); } @@ -55,6 +64,7 @@ public KeyVaultClient(URI uri, KeyVaultCredentialProvider tokenCredentialProvide builder.credential(new ManagedIdentityCredentialBuilder().build()); } secretClient = builder.vaultUrl("https://" + uri.getHost()).buildAsyncClient(); + return this; } /** @@ -65,6 +75,9 @@ public KeyVaultClient(URI uri, KeyVaultCredentialProvider tokenCredentialProvide * @return Secret values that matches the secretIdentifier */ public KeyVaultSecret getSecret(URI secretIdentifier, int timeout) { + if (secretClient == null) { + build(); + } String[] tokens = secretIdentifier.getPath().split("/"); String name = (tokens.length >= 3 ? tokens[2] : null); @@ -72,4 +85,8 @@ public KeyVaultSecret getSecret(URI secretIdentifier, int timeout) { return secretClient.getSecret(name, version).block(Duration.ofSeconds(timeout)); } + SecretClientBuilder getBuilder() { + return new SecretClientBuilder(); + } + } diff --git a/spring-cloud-azure-appconfiguration-config/src/main/resources/META-INF/spring.factories b/spring-cloud-azure-appconfiguration-config/src/main/resources/META-INF/spring.factories index 05d33ff9e..1a3ee3c8d 100644 --- a/spring-cloud-azure-appconfiguration-config/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-azure-appconfiguration-config/src/main/resources/META-INF/spring.factories @@ -1,6 +1,6 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -com.microsoft.azure.spring.cloud.config.AzureCloudConfigAutoConfiguration +com.microsoft.azure.spring.cloud.config.AppConfigurationAutoConfiguration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ -com.microsoft.azure.spring.cloud.config.AzureConfigBootstrapConfiguration +com.microsoft.azure.spring.cloud.config.AppConfigurationBootstrapConfiguration diff --git a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureConfigBootstrapConfigurationTest.java b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AppConfigurationBootstrapConfigurationTest.java similarity index 76% rename from spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureConfigBootstrapConfigurationTest.java rename to spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AppConfigurationBootstrapConfigurationTest.java index f5e35eb20..54f4d9c4a 100644 --- a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureConfigBootstrapConfigurationTest.java +++ b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AppConfigurationBootstrapConfigurationTest.java @@ -24,34 +24,20 @@ import org.apache.http.message.BasicStatusLine; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import com.fasterxml.jackson.databind.ObjectMapper; -import com.microsoft.azure.credentials.MSICredentials; -import com.microsoft.azure.keyvault.KeyVaultClient; import com.microsoft.azure.spring.cloud.config.stores.ClientStore; -import com.microsoft.rest.RestClient; -import com.microsoft.rest.RestClient.Builder; -@RunWith(PowerMockRunner.class) -@PrepareForTest(AzureConfigBootstrapConfiguration.class) -@PowerMockIgnore({ "javax.net.ssl.*", "javax.crypto.*" }) -public class AzureConfigBootstrapConfigurationTest { +public class AppConfigurationBootstrapConfigurationTest { private static final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues(propPair(CONN_STRING_PROP, TEST_CONN_STRING), propPair(STORE_ENDPOINT_PROP, TEST_STORE_NAME)) - .withConfiguration(AutoConfigurations.of(AzureConfigBootstrapConfiguration.class)); - - @Mock - private MSICredentials msiCredentials; + .withConfiguration(AutoConfigurations.of(AppConfigurationBootstrapConfiguration.class)); @Mock private CloseableHttpResponse mockClosableHttpResponse; @@ -62,15 +48,6 @@ public class AzureConfigBootstrapConfigurationTest { @Mock InputStream mockInputStream; - @Mock - Builder builderMock; - - @Mock - RestClient restClientMock; - - @Mock - KeyVaultClient keyVaultClientMock; - @Mock ObjectMapper mockObjectMapper; @@ -86,8 +63,6 @@ public void setup() { .thenReturn(new BasicStatusLine(new ProtocolVersion("", 0, 0), 200, "")); when(mockClosableHttpResponse.getEntity()).thenReturn(mockHttpEntity); when(mockHttpEntity.getContent()).thenReturn(mockInputStream); - - whenNew(Builder.class).withNoArguments().thenReturn(builderMock); } catch (Exception e) { fail(); } @@ -97,7 +72,7 @@ public void setup() { public void propertySourceLocatorBeanCreated() throws Exception { whenNew(ClientStore.class).withAnyArguments().thenReturn(clientStoreMock); contextRunner.withPropertyValues(propPair(FAIL_FAST_PROP, "false")) - .run(context -> assertThat(context).hasSingleBean(AzureConfigPropertySourceLocator.class)); + .run(context -> assertThat(context).hasSingleBean(AppConfigurationPropertySourceLocator.class)); } @Test diff --git a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureCloudConfigPropertiesTest.java b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AppConfigurationPropertiesTest.java similarity index 87% rename from spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureCloudConfigPropertiesTest.java rename to spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AppConfigurationPropertiesTest.java index a0a8c136b..bfee42c99 100644 --- a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureCloudConfigPropertiesTest.java +++ b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AppConfigurationPropertiesTest.java @@ -35,31 +35,21 @@ import org.apache.http.message.BasicStatusLine; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.context.properties.ConfigurationPropertiesBindException; -import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Configuration; import com.fasterxml.jackson.databind.ObjectMapper; -@RunWith(PowerMockRunner.class) -@PrepareForTest(AzureConfigBootstrapConfiguration.class) -@PowerMockIgnore({ "javax.net.ssl.*", "javax.crypto.*", "org.mockito.*" }) -public class AzureCloudConfigPropertiesTest { +public class AppConfigurationPropertiesTest { @InjectMocks private ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(AzureConfigBootstrapConfiguration.class)); + .withConfiguration(AutoConfigurations.of(AppConfigurationBootstrapConfiguration.class)); private static final String NO_ENDPOINT_CONN_STRING = "Id=fake-conn-id;Secret=ZmFrZS1jb25uLXNlY3JldA=="; @@ -101,7 +91,6 @@ public class AzureCloudConfigPropertiesTest { public void setup() { MockitoAnnotations.initMocks(this); try { - PowerMockito.whenNew(ObjectMapper.class).withAnyArguments().thenReturn(mockObjectMapper); when(mockClosableHttpResponse.getStatusLine()) .thenReturn(new BasicStatusLine(new ProtocolVersion("", 0, 0), 200, "")); when(mockClosableHttpResponse.getEntity()).thenReturn(mockHttpEntity); @@ -115,7 +104,7 @@ public void setup() { public void validInputShouldCreatePropertiesBean() { this.contextRunner.withPropertyValues(propPair(CONN_STRING_PROP, TEST_CONN_STRING)) .withPropertyValues(propPair(FAIL_FAST_PROP, "false")).run(context -> { - assertThat(context).hasSingleBean(AzureCloudConfigProperties.class); + assertThat(context).hasSingleBean(AppConfigurationProperties.class); }); } @@ -192,7 +181,7 @@ public void storeNameCanBeInitIfConnectionStringConfigured() { this.contextRunner.withPropertyValues(propPair(CONN_STRING_PROP, TEST_CONN_STRING), propPair(STORE_ENDPOINT_PROP, "")).withPropertyValues(propPair(FAIL_FAST_PROP, "false")) .run(context -> { - AzureCloudConfigProperties properties = context.getBean(AzureCloudConfigProperties.class); + AppConfigurationProperties properties = context.getBean(AppConfigurationProperties.class); assertThat(properties.getStores()).isNotNull(); assertThat(properties.getStores().size()).isEqualTo(1); assertThat(properties.getStores().get(0).getEndpoint()).isEqualTo("https://fake.test.config.io"); @@ -221,7 +210,7 @@ public void minValidWatchTime() { this.contextRunner.withPropertyValues(propPair(CONN_STRING_PROP, TEST_CONN_STRING)) .withPropertyValues(propPair(CACHE_EXPIRATION_PROP, "1s")) .run(context -> { - assertThat(context).hasSingleBean(AzureCloudConfigProperties.class); + assertThat(context).hasSingleBean(AppConfigurationProperties.class); }); } @@ -231,9 +220,3 @@ private void assertInvalidField(AssertableApplicationContext context, String fie .hasStackTraceContaining(String.format("field '%s': rejected value", fieldName)); } } - -@Configuration -@EnableConfigurationProperties(AzureCloudConfigProperties.class) -class PropertiesTestConfiguration { - // Do nothing -} diff --git a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureConfigPropertySourceKeyVaultTest.java b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AppConfigurationPropertySourceKeyVaultTest.java similarity index 90% rename from spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureConfigPropertySourceKeyVaultTest.java rename to spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AppConfigurationPropertySourceKeyVaultTest.java index 8eb9c2986..6cc03ed77 100644 --- a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureConfigPropertySourceKeyVaultTest.java +++ b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AppConfigurationPropertySourceKeyVaultTest.java @@ -61,11 +61,11 @@ import reactor.core.publisher.Mono; @RunWith(PowerMockRunner.class) -@PrepareForTest({ AzureConfigPropertySource.class }) -public class AzureConfigPropertySourceKeyVaultTest { +@PrepareForTest({ AppConfigurationPropertySource.class }) +public class AppConfigurationPropertySourceKeyVaultTest { private static final String EMPTY_CONTENT_TYPE = ""; - private static final AzureCloudConfigProperties TEST_PROPS = new AzureCloudConfigProperties(); + private static final AppConfigurationProperties TEST_PROPS = new AppConfigurationProperties(); public static final List TEST_ITEMS = new ArrayList<>(); @@ -85,11 +85,11 @@ public class AzureConfigPropertySourceKeyVaultTest { public List testItems = new ArrayList<>(); - private AzureConfigPropertySource propertySource; + private AppConfigurationPropertySource propertySource; - private AzureCloudConfigProperties azureProperties; + private AppConfigurationProperties appConfigurationProperties; - private AppConfigProviderProperties appProperties; + private AppConfigurationProviderProperties appProperties; @Mock private ClientStore clientStoreMock; @@ -114,7 +114,7 @@ public class AzureConfigPropertySourceKeyVaultTest { @Rule public ExpectedException expected = ExpectedException.none(); - + private KeyVaultCredentialProvider tokenCredentialProvider = null; @BeforeClass @@ -127,9 +127,8 @@ public static void init() { @Before public void setup() { MockitoAnnotations.initMocks(this); - azureProperties = new AzureCloudConfigProperties(); - azureProperties.setFailFast(true); - appProperties = new AppConfigProviderProperties(); + appConfigurationProperties = new AppConfigurationProperties(); + appProperties = new AppConfigurationProviderProperties(); appProperties.setMaxRetryTime(0); ConfigStore testStore = new ConfigStore(); testStore.setEndpoint(TEST_STORE_NAME); @@ -137,8 +136,8 @@ public void setup() { ArrayList contexts = new ArrayList(); contexts.add("/application/*"); storeContextsMap.put(TEST_STORE_NAME, contexts); - propertySource = new AzureConfigPropertySource(TEST_CONTEXT, testStore, "\0", - azureProperties, clientStoreMock, appProperties, tokenCredentialProvider, storeContextsMap); + propertySource = new AppConfigurationPropertySource(TEST_CONTEXT, testStore, "\0", + appConfigurationProperties, clientStoreMock, appProperties, tokenCredentialProvider, storeContextsMap); testItems = new ArrayList(); testItems.add(item1); diff --git a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureConfigPropertySourceLocatorTest.java b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AppConfigurationPropertySourceLocatorTest.java similarity index 55% rename from spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureConfigPropertySourceLocatorTest.java rename to spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AppConfigurationPropertySourceLocatorTest.java index f5b7977fe..b180e5c97 100644 --- a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureConfigPropertySourceLocatorTest.java +++ b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AppConfigurationPropertySourceLocatorTest.java @@ -15,14 +15,20 @@ import static com.microsoft.azure.spring.cloud.config.TestConstants.TEST_STORE_NAME_2; import static com.microsoft.azure.spring.cloud.config.TestUtils.createItem; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.IOException; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; +import java.util.Date; import java.util.Iterator; import java.util.List; +import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; @@ -33,6 +39,7 @@ import org.mockito.MockitoAnnotations; 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 com.azure.core.http.rest.PagedFlux; @@ -40,10 +47,11 @@ import com.azure.data.appconfiguration.ConfigurationAsyncClient; import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.microsoft.azure.spring.cloud.config.stores.ClientStore; +import com.microsoft.azure.spring.cloud.config.stores.ConfigStore; import reactor.core.publisher.Flux; -public class AzureConfigPropertySourceLocatorTest { +public class AppConfigurationPropertySourceLocatorTest { private static final String APPLICATION_NAME = "foo"; private static final String PROFILE_NAME_1 = "dev"; @@ -64,7 +72,10 @@ public class AzureConfigPropertySourceLocatorTest { private ConfigurableEnvironment environment; @Mock - private ClientStore configStoreMock; + private ClientStore clientStoreMock; + + @Mock + private ConfigStore configStore; @Mock private ConfigurationAsyncClient configClientMock; @@ -84,11 +95,24 @@ public class AzureConfigPropertySourceLocatorTest { @Mock private PagedResponse pagedMock; - private AzureCloudConfigProperties properties; + @Mock + private List configStoresMock; + + @Mock + private ConfigStore configStoreMock; + + @Mock + private Iterator configStoreIterator; + + @Mock + private AppConfigurationProviderProperties appPropertiesMock; + + @Mock + private AppConfigurationProperties properties; - private AzureConfigPropertySourceLocator locator; + private AppConfigurationPropertySourceLocator locator; - private AppConfigProviderProperties appProperties; + private AppConfigurationProviderProperties appProperties; private KeyVaultCredentialProvider tokenCredentialProvider = null; @@ -102,9 +126,17 @@ public void setup() { MockitoAnnotations.initMocks(this); when(environment.getActiveProfiles()).thenReturn(new String[] { PROFILE_NAME_1, PROFILE_NAME_2 }); - properties = new AzureCloudConfigProperties(); - TestUtils.addStore(properties, TEST_STORE_NAME, TEST_CONN_STRING); - properties.setName(APPLICATION_NAME); + when(properties.getName()).thenReturn(APPLICATION_NAME); + when(properties.getProfileSeparator()).thenReturn("_"); + when(properties.getStores()).thenReturn(configStoresMock); + when(properties.isEnabled()).thenReturn(true); + when(configStoresMock.iterator()).thenReturn(configStoreIterator); + when(configStoreIterator.hasNext()).thenReturn(true).thenReturn(false); + when(configStoreIterator.next()).thenReturn(configStoreMock); + + when(configStoreMock.getConnectionString()).thenReturn(TEST_CONN_STRING); + when(configStoreMock.getEndpoint()).thenReturn(TEST_STORE_NAME); + when(configStoreMock.getPrefix()).thenReturn(null); when(configClientMock.listConfigurationSettings(Mockito.any())).thenReturn(settingsMock); when(settingsMock.byPage()).thenReturn(pageMock); @@ -113,15 +145,29 @@ public void setup() { when(iteratorMock.next()).thenReturn(pagedMock); when(pagedMock.getItems()).thenReturn(new ArrayList()); - appProperties = new AppConfigProviderProperties(); + appProperties = new AppConfigurationProviderProperties(); appProperties.setVersion("1.0"); appProperties.setMaxRetries(12); appProperties.setMaxRetryTime(0); } + @After + public void cleanup() + throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { + Field field = AppConfigurationPropertySourceLocator.class.getDeclaredField("startup"); + field.setAccessible(true); + field.set(null, true); + StateHolder.setLoadState(TEST_STORE_NAME, false); + } + @Test public void compositeSourceIsCreated() { - locator = new AzureConfigPropertySourceLocator(properties, appProperties, configStoreMock, + String[] labels = new String[1]; + labels[0] = "\0"; + when(configStoreMock.getLabels()).thenReturn(labels); + when(properties.getDefaultContext()).thenReturn("application"); + + locator = new AppConfigurationPropertySourceLocator(properties, appProperties, clientStoreMock, tokenCredentialProvider); PropertySource source = locator.locate(environment); assertThat(source).isInstanceOf(CompositePropertySource.class); @@ -139,8 +185,12 @@ public void compositeSourceIsCreated() { @Test public void compositeSourceIsCreatedForPrefixedConfig() { - properties.getStores().get(0).setPrefix(PREFIX); - locator = new AzureConfigPropertySourceLocator(properties, appProperties, configStoreMock, + String[] labels = new String[1]; + labels[0] = "\0"; + when(configStoreMock.getLabels()).thenReturn(labels); + when(configStoreMock.getPrefix()).thenReturn(PREFIX); + when(properties.getDefaultContext()).thenReturn("application"); + locator = new AppConfigurationPropertySourceLocator(properties, appProperties, clientStoreMock, tokenCredentialProvider); PropertySource source = locator.locate(environment); @@ -161,11 +211,16 @@ public void compositeSourceIsCreatedForPrefixedConfig() { @Test public void nullApplicationNameCreateDefaultContextOnly() { + String[] labels = new String[1]; + labels[0] = "\0"; + when(configStoreMock.getLabels()).thenReturn(labels); when(environment.getActiveProfiles()).thenReturn(new String[] {}); when(environment.getProperty("spring.application.name")).thenReturn(null); - properties.setName(null); - locator = new AzureConfigPropertySourceLocator(properties, appProperties, - configStoreMock, tokenCredentialProvider); + when(properties.getDefaultContext()).thenReturn("application"); + when(properties.getName()).thenReturn(null); + + locator = new AppConfigurationPropertySourceLocator(properties, appProperties, + clientStoreMock, tokenCredentialProvider); PropertySource source = locator.locate(environment); assertThat(source).isInstanceOf(CompositePropertySource.class); @@ -180,11 +235,15 @@ public void nullApplicationNameCreateDefaultContextOnly() { @Test public void emptyApplicationNameCreateDefaultContextOnly() { + String[] labels = new String[1]; + labels[0] = "\0"; + when(configStoreMock.getLabels()).thenReturn(labels); when(environment.getActiveProfiles()).thenReturn(new String[] {}); when(environment.getProperty("spring.application.name")).thenReturn(""); - properties.setName(""); - locator = new AzureConfigPropertySourceLocator(properties, appProperties, - configStoreMock, tokenCredentialProvider); + when(properties.getName()).thenReturn(""); + when(properties.getDefaultContext()).thenReturn("application"); + locator = new AppConfigurationPropertySourceLocator(properties, appProperties, + clientStoreMock, tokenCredentialProvider); PropertySource source = locator.locate(environment); assertThat(source).isInstanceOf(CompositePropertySource.class); @@ -199,36 +258,67 @@ public void emptyApplicationNameCreateDefaultContextOnly() { @Test public void defaultFailFastThrowException() throws IOException { - expected.expect(RuntimeException.class); + expected.expect(NullPointerException.class); - locator = new AzureConfigPropertySourceLocator(properties, appProperties, - configStoreMock, tokenCredentialProvider); + when(configStoreMock.isFailFast()).thenReturn(true); + when(properties.getDefaultContext()).thenReturn("application"); + + locator = new AppConfigurationPropertySourceLocator(properties, appProperties, + clientStoreMock, tokenCredentialProvider); + + when(clientStoreMock.listSettings(Mockito.any(), Mockito.anyString())).thenThrow(new RuntimeException()); + locator.locate(environment); + verify(configStoreMock, times(1)).isFailFast(); + } + + @Test + public void refreshThrowException() throws IOException, NoSuchFieldException, SecurityException, + IllegalArgumentException, IllegalAccessException { + Field field = AppConfigurationPropertySourceLocator.class.getDeclaredField("startup"); + field.setAccessible(true); + field.set(null, false); + StateHolder.setLoadState(TEST_STORE_NAME, true); + + expected.expect(NullPointerException.class); + + when(environment.getActiveProfiles()).thenReturn(new String[] {}); + when(environment.getProperty("spring.application.name")).thenReturn(null); + + locator = new AppConfigurationPropertySourceLocator(properties, appProperties, + clientStoreMock, tokenCredentialProvider); - when(configStoreMock.listSettings(Mockito.any(), Mockito.anyString())).thenThrow(new RuntimeException()); - assertThat(properties.isFailFast()).isTrue(); locator.locate(environment); } @Test - public void notFailFastShouldPass() { - properties.setFailFast(false); - locator = new AzureConfigPropertySourceLocator(properties, appProperties, - configStoreMock, tokenCredentialProvider); + public void notFailFastShouldPass() throws IOException { + when(configStoreMock.isFailFast()).thenReturn(false); + locator = new AppConfigurationPropertySourceLocator(properties, appProperties, + clientStoreMock, tokenCredentialProvider); + + when(configStoreMock.isFailFast()).thenReturn(false); + when(clientStoreMock.listSettings(Mockito.any(), Mockito.anyString())).thenThrow(new RuntimeException()); + when(configStoreMock.getEndpoint()).thenReturn(TEST_STORE_NAME); PropertySource source = locator.locate(environment); assertThat(source).isInstanceOf(CompositePropertySource.class); + verify(configStoreMock, times(3)).isFailFast(); } @Test public void multiplePropertySourcesExistForMultiStores() { + String[] labels = new String[1]; + labels[0] = "\0"; + when(configStoreMock.getLabels()).thenReturn(labels); when(environment.getActiveProfiles()).thenReturn(new String[] {}); + when(configStoreMock.getEndpoint()).thenReturn(TEST_STORE_NAME); - properties = new AzureCloudConfigProperties(); + properties = new AppConfigurationProperties(); TestUtils.addStore(properties, TEST_STORE_NAME_1, TEST_CONN_STRING); TestUtils.addStore(properties, TEST_STORE_NAME_2, TEST_CONN_STRING_2); - locator = new AzureConfigPropertySourceLocator(properties, appProperties, - configStoreMock, tokenCredentialProvider); + locator = new AppConfigurationPropertySourceLocator(properties, appProperties, + clientStoreMock, tokenCredentialProvider); PropertySource source = locator.locate(environment); assertThat(source).isInstanceOf(CompositePropertySource.class); @@ -239,4 +329,37 @@ public void multiplePropertySourcesExistForMultiStores() { assertThat(sources.size()).isEqualTo(2); assertThat(sources.stream().map(s -> s.getName()).toArray()).containsExactly(expectedSourceNames); } + + @Test + public void awaitOnError() throws Exception { + List configStores = new ArrayList(); + configStores.add(configStore); + AppConfigurationProperties properties = new AppConfigurationProperties(); + properties.setProfileSeparator("_"); + properties.setName("TestStoreName"); + properties.setStores(configStores); + + appPropertiesMock.setPrekillTime(5); + + Environment env = Mockito.mock(ConfigurableEnvironment.class); + String[] array = {}; + when(env.getActiveProfiles()).thenReturn(array); + String[] labels = { "" }; + when(configStore.getLabels()).thenReturn(labels); + when(clientStoreMock.listSettings(Mockito.any(), Mockito.any())).thenThrow(new NullPointerException("")); + when(appPropertiesMock.getPrekillTime()).thenReturn(-60); + when(appPropertiesMock.getStartDate()).thenReturn(new Date()); + + locator = new AppConfigurationPropertySourceLocator(properties, appPropertiesMock, clientStoreMock, + tokenCredentialProvider); + + boolean threwException = false; + try { + locator.locate(env); + } catch (Exception e) { + threwException = true; + } + assertTrue(threwException); + verify(appPropertiesMock, times(1)).getPrekillTime(); + } } diff --git a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureConfigPropertySourceTest.java b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AppConfigurationPropertySourceTest.java similarity index 78% rename from spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureConfigPropertySourceTest.java rename to spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AppConfigurationPropertySourceTest.java index 2654488eb..9eea25715 100644 --- a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureConfigPropertySourceTest.java +++ b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AppConfigurationPropertySourceTest.java @@ -6,9 +6,10 @@ package com.microsoft.azure.spring.cloud.config; import static com.microsoft.azure.spring.cloud.config.Constants.FEATURE_FLAG_CONTENT_TYPE; +import static com.microsoft.azure.spring.cloud.config.TestConstants.FEATURE_BOOLEAN_VALUE; import static com.microsoft.azure.spring.cloud.config.TestConstants.FEATURE_LABEL; import static com.microsoft.azure.spring.cloud.config.TestConstants.FEATURE_VALUE; -import static com.microsoft.azure.spring.cloud.config.TestConstants.FEATURE_BOOLEAN_VALUE; +import static com.microsoft.azure.spring.cloud.config.TestConstants.FEATURE_VALUE_PARAMETERS; import static com.microsoft.azure.spring.cloud.config.TestConstants.TEST_CONN_STRING; import static com.microsoft.azure.spring.cloud.config.TestConstants.TEST_CONTEXT; import static com.microsoft.azure.spring.cloud.config.TestConstants.TEST_KEY_1; @@ -52,6 +53,7 @@ import com.azure.data.appconfiguration.ConfigurationAsyncClient; import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.microsoft.azure.spring.cloud.config.feature.management.entity.Feature; import com.microsoft.azure.spring.cloud.config.feature.management.entity.FeatureFilterEvaluationContext; import com.microsoft.azure.spring.cloud.config.feature.management.entity.FeatureSet; @@ -61,10 +63,10 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -public class AzureConfigPropertySourceTest { +public class AppConfigurationPropertySourceTest { private static final String EMPTY_CONTENT_TYPE = ""; - private static final AzureCloudConfigProperties TEST_PROPS = new AzureCloudConfigProperties(); + private static final AppConfigurationProperties TEST_PROPS = new AppConfigurationProperties(); public static final List TEST_ITEMS = new ArrayList<>(); @@ -80,7 +82,8 @@ public class AzureConfigPropertySourceTest { EMPTY_CONTENT_TYPE); private static final ConfigurationSetting item3Null = createItem(TEST_CONTEXT, TEST_KEY_3, TEST_VALUE_3, - TEST_LABEL_3, null); + TEST_LABEL_3, + null); private static final ConfigurationSetting featureItem = createItem(".appconfig.featureflag/", "Alpha", FEATURE_VALUE, FEATURE_LABEL, FEATURE_FLAG_CONTENT_TYPE); @@ -88,18 +91,22 @@ public class AzureConfigPropertySourceTest { private static final ConfigurationSetting featureItem2 = createItem(".appconfig.featureflag/", "Beta", FEATURE_BOOLEAN_VALUE, FEATURE_LABEL, FEATURE_FLAG_CONTENT_TYPE); + private static final ConfigurationSetting featureItem3 = createItem(".appconfig.featureflag/", "Gamma", + FEATURE_VALUE_PARAMETERS, FEATURE_LABEL, FEATURE_FLAG_CONTENT_TYPE); + private static final ConfigurationSetting featureItemNull = createItem(".appconfig.featureflag/", "Alpha", - FEATURE_VALUE, FEATURE_LABEL, null); + FEATURE_VALUE, + FEATURE_LABEL, null); public List testItems = new ArrayList<>(); private static final String FEATURE_MANAGEMENT_KEY = "feature-management.featureManagement"; - private AzureConfigPropertySource propertySource; + private AppConfigurationPropertySource propertySource; private static ObjectMapper mapper = new ObjectMapper(); - private AzureCloudConfigProperties azureProperties; + private AppConfigurationProperties appConfigurationProperties; @Mock private ClientStore clientStoreMock; @@ -128,7 +135,7 @@ public class AzureConfigPropertySourceTest { @Rule public ExpectedException expected = ExpectedException.none(); - private AppConfigProviderProperties appProperties; + private AppConfigurationProviderProperties appProperties; private KeyVaultCredentialProvider tokenCredentialProvider = null; @@ -139,22 +146,23 @@ public static void init() { featureItem.setContentType(FEATURE_FLAG_CONTENT_TYPE); FEATURE_ITEMS.add(featureItem); FEATURE_ITEMS.add(featureItem2); + FEATURE_ITEMS.add(featureItem3); + mapper.setPropertyNamingStrategy(PropertyNamingStrategy.KEBAB_CASE); } @Before public void setup() { MockitoAnnotations.initMocks(this); - azureProperties = new AzureCloudConfigProperties(); - azureProperties.setFailFast(true); - appProperties = new AppConfigProviderProperties(); + appConfigurationProperties = new AppConfigurationProperties(); + appProperties = new AppConfigurationProviderProperties(); ConfigStore configStore = new ConfigStore(); configStore.setEndpoint(TEST_STORE_NAME); Map> storeContextsMap = new HashMap>(); ArrayList contexts = new ArrayList(); contexts.add("/application/*"); storeContextsMap.put(TEST_STORE_NAME, contexts); - propertySource = new AzureConfigPropertySource(TEST_CONTEXT, configStore, "\0", azureProperties, - clientStoreMock, appProperties, tokenCredentialProvider, storeContextsMap); + propertySource = new AppConfigurationPropertySource(TEST_CONTEXT, configStore, "\0", + appConfigurationProperties, clientStoreMock, appProperties, tokenCredentialProvider, storeContextsMap); testItems = new ArrayList(); testItems.add(item1); @@ -173,6 +181,8 @@ public void setup() { public void testPropCanBeInitAndQueried() throws IOException { when(clientStoreMock.listSettings(Mockito.any(), Mockito.anyString())).thenReturn(testItems) .thenReturn(FEATURE_ITEMS); + when(clientStoreMock.listSettingRevisons(Mockito.any(), Mockito.anyString())).thenReturn(testItems) + .thenReturn(FEATURE_ITEMS); FeatureSet featureSet = new FeatureSet(); try { propertySource.initProperties(featureSet); @@ -182,8 +192,8 @@ public void testPropCanBeInitAndQueried() throws IOException { propertySource.initFeatures(featureSet); String[] keyNames = propertySource.getPropertyNames(); - String[] expectedKeyNames = testItems.stream().map(t -> t.getKey().substring(TEST_CONTEXT.length())) - .toArray(String[]::new); + String[] expectedKeyNames = testItems.stream() + .map(t -> t.getKey().substring(TEST_CONTEXT.length())).toArray(String[]::new); String[] allExpectedKeyNames = ArrayUtils.addAll(expectedKeyNames, FEATURE_MANAGEMENT_KEY); assertThat(keyNames).containsExactlyInAnyOrder(allExpectedKeyNames); @@ -233,13 +243,25 @@ public void testFeatureFlagCanBeInitedAndQueried() throws IOException { FeatureSet featureSetExpected = new FeatureSet(); Feature feature = new Feature(); feature.setKey("Alpha"); - ArrayList filters = new ArrayList(); + HashMap filters = + new HashMap(); FeatureFilterEvaluationContext ffec = new FeatureFilterEvaluationContext(); ffec.setName("TestFilter"); - filters.add(ffec); + filters.put(0, ffec); feature.setEnabledFor(filters); + Feature gamma = new Feature(); + gamma.setKey("Gamma"); + filters = new HashMap(); + ffec = new FeatureFilterEvaluationContext(); + ffec.setName("TestFilter"); + LinkedHashMap parameters = new LinkedHashMap(); + parameters.put("key", "value"); + ffec.setParameters(parameters); + filters.put(0, ffec); + gamma.setEnabledFor(filters); featureSetExpected.addFeature("Alpha", feature); featureSetExpected.addFeature("Beta", true); + featureSetExpected.addFeature("Gamma", gamma); LinkedHashMap convertedValue = mapper.convertValue(featureSetExpected.getFeatureManagement(), LinkedHashMap.class); @@ -269,15 +291,37 @@ public void testFeatureFlagBuildError() throws IOException { propertySource.initFeatures(featureSet); FeatureSet featureSetExpected = new FeatureSet(); - Feature feature = new Feature(); - feature.setKey("Alpha"); - ArrayList filters = new ArrayList(); + + HashMap filters = + new HashMap(); FeatureFilterEvaluationContext ffec = new FeatureFilterEvaluationContext(); ffec.setName("TestFilter"); - filters.add(ffec); - feature.setEnabledFor(filters); - featureSetExpected.addFeature("Alpha", feature); + + filters.put(0, ffec); + + Feature alpha = new Feature(); + alpha.setKey("Alpha"); + alpha.setEnabledFor(filters); + + HashMap filters2 = + new HashMap(); + FeatureFilterEvaluationContext ffec2 = new FeatureFilterEvaluationContext(); + ffec2.setName("TestFilter"); + + filters2.put(0, ffec2); + + LinkedHashMap parameters = new LinkedHashMap(); + parameters.put("key", "value"); + ffec2.setParameters(parameters); + + Feature gamma = new Feature(); + gamma.setKey("Gamma"); + gamma.setEnabledFor(filters2); + filters2.put(0, ffec2); + + featureSetExpected.addFeature("Alpha", alpha); featureSetExpected.addFeature("Beta", true); + featureSetExpected.addFeature("Gamma", gamma); LinkedHashMap convertedValue = mapper.convertValue(featureSetExpected.getFeatureManagement(), LinkedHashMap.class); @@ -299,8 +343,8 @@ public void initNullValidContentTypeTest() throws IOException { } String[] keyNames = propertySource.getPropertyNames(); - String[] expectedKeyNames = items.stream().map(t -> t.getKey().substring(TEST_CONTEXT.length())) - .toArray(String[]::new); + String[] expectedKeyNames = items.stream() + .map(t -> t.getKey().substring(TEST_CONTEXT.length())).toArray(String[]::new); assertThat(keyNames).containsExactlyInAnyOrder(expectedKeyNames); } diff --git a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureConfigCloudRefreshTest.java b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AppConfigurationRefreshTest.java similarity index 78% rename from spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureConfigCloudRefreshTest.java rename to spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AppConfigurationRefreshTest.java index eeb0dfea9..45d3b220b 100644 --- a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureConfigCloudRefreshTest.java +++ b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AppConfigurationRefreshTest.java @@ -33,7 +33,6 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -import org.powermock.api.mockito.PowerMockito; import org.powermock.modules.junit4.PowerMockRunner; import org.springframework.cloud.endpoint.event.RefreshEvent; import org.springframework.context.ApplicationEventPublisher; @@ -42,28 +41,27 @@ import com.microsoft.azure.spring.cloud.config.stores.ClientStore; import com.microsoft.azure.spring.cloud.config.stores.ConfigStore; -@RunWith(PowerMockRunner.class) -public class AzureConfigCloudRefreshTest { +public class AppConfigurationRefreshTest { @Mock private ApplicationEventPublisher eventPublisher; @Mock - private AzureCloudConfigProperties properties; + private AppConfigurationProperties properties; private ArrayList keys; @Mock private Map> contextsMap; - AzureCloudConfigRefresh configRefresh; + AppConfigurationRefresh configRefresh; @Mock private Date date; @Mock private ClientStore clientStoreMock; - + private static final String WATCHED_KEYS = "/application/*"; @Before @@ -75,7 +73,7 @@ public void setup() { store.setConnectionString(TEST_CONN_STRING); store.setWatchedKey(WATCHED_KEYS); - properties = new AzureCloudConfigProperties(); + properties = new AppConfigurationProperties(); properties.setStores(Arrays.asList(store)); properties.setCacheExpiration(Duration.ofMinutes(-60)); @@ -91,22 +89,23 @@ public void setup() { ConfigurationSetting item = new ConfigurationSetting(); item.setKey("fake-etag/application/test.key"); item.setETag("fake-etag"); - + when(clientStoreMock.watchedKeyNames(Mockito.any(), Mockito.any())).thenReturn(WATCHED_KEYS); - configRefresh = new AzureCloudConfigRefresh(properties, contextsMap, clientStoreMock); + configRefresh = new AppConfigurationRefresh(properties, contextsMap, clientStoreMock); + StateHolder.setLoadState(TEST_STORE_NAME, true); } @After public void cleanupMethod() { - StateHolder.setState(TEST_STORE_NAME + CONFIGURATION_SUFFIX, new ConfigurationSetting()); - StateHolder.setState(TEST_STORE_NAME + FEATURE_SUFFIX, new ConfigurationSetting()); + StateHolder.setEtagState(TEST_STORE_NAME + CONFIGURATION_SUFFIX, new ConfigurationSetting()); + StateHolder.setEtagState(TEST_STORE_NAME + FEATURE_SUFFIX, new ConfigurationSetting()); } @Test public void nonUpdatedEtagShouldntPublishEvent() throws Exception { - StateHolder.setState(TEST_STORE_NAME + CONFIGURATION_SUFFIX, initialResponse().get(0)); - StateHolder.setState(TEST_STORE_NAME + FEATURE_SUFFIX, initialResponse().get(0)); + StateHolder.setEtagState(TEST_STORE_NAME + CONFIGURATION_SUFFIX, initialResponse().get(0)); + StateHolder.setEtagState(TEST_STORE_NAME + FEATURE_SUFFIX, initialResponse().get(0)); configRefresh.setApplicationEventPublisher(eventPublisher); @@ -118,9 +117,9 @@ public void nonUpdatedEtagShouldntPublishEvent() throws Exception { @Test public void updatedEtagShouldPublishEvent() throws Exception { - StateHolder.setState(TEST_STORE_NAME + CONFIGURATION_SUFFIX, initialResponse().get(0)); - StateHolder.setState(TEST_STORE_NAME + FEATURE_SUFFIX, initialResponse().get(0)); - + StateHolder.setEtagState(TEST_STORE_NAME + CONFIGURATION_SUFFIX, initialResponse().get(0)); + StateHolder.setEtagState(TEST_STORE_NAME + FEATURE_SUFFIX, initialResponse().get(0)); + when(clientStoreMock.listSettingRevisons(Mockito.any(), Mockito.anyString())).thenReturn(initialResponse()); configRefresh.setApplicationEventPublisher(eventPublisher); @@ -133,9 +132,9 @@ public void updatedEtagShouldPublishEvent() throws Exception { // If there is a change it should update assertTrue(configRefresh.refreshConfigurations().get()); verify(eventPublisher, times(1)).publishEvent(any(RefreshEvent.class)); - - StateHolder.setState(TEST_STORE_NAME + CONFIGURATION_SUFFIX, updatedResponse().get(0)); - StateHolder.setState(TEST_STORE_NAME + FEATURE_SUFFIX, updatedResponse().get(0)); + + StateHolder.setEtagState(TEST_STORE_NAME + CONFIGURATION_SUFFIX, updatedResponse().get(0)); + StateHolder.setEtagState(TEST_STORE_NAME + FEATURE_SUFFIX, updatedResponse().get(0)); HashMap map = new HashMap(); map.put("store1_configuration", "fake-etag-updated"); @@ -144,7 +143,7 @@ public void updatedEtagShouldPublishEvent() throws Exception { ConfigurationSetting updated = new ConfigurationSetting(); updated.setETag("fake-etag-updated"); - StateHolder.setState(TEST_STORE_NAME + CONFIGURATION_SUFFIX, updated); + StateHolder.setEtagState(TEST_STORE_NAME + CONFIGURATION_SUFFIX, updated); // If there is no change it shouldn't update assertFalse(configRefresh.refreshConfigurations().get()); @@ -153,11 +152,11 @@ public void updatedEtagShouldPublishEvent() throws Exception { @Test public void notRefreshTime() throws Exception { - StateHolder.setState(TEST_STORE_NAME + CONFIGURATION_SUFFIX, initialResponse().get(0)); - StateHolder.setState(TEST_STORE_NAME + FEATURE_SUFFIX, initialResponse().get(0)); - + StateHolder.setEtagState(TEST_STORE_NAME + CONFIGURATION_SUFFIX, initialResponse().get(0)); + StateHolder.setEtagState(TEST_STORE_NAME + FEATURE_SUFFIX, initialResponse().get(0)); + properties.setCacheExpiration(Duration.ofMinutes(60)); - AzureCloudConfigRefresh watchLargeDelay = new AzureCloudConfigRefresh(properties, contextsMap, clientStoreMock); + AppConfigurationRefresh watchLargeDelay = new AppConfigurationRefresh(properties, contextsMap, clientStoreMock); watchLargeDelay.setApplicationEventPublisher(eventPublisher); watchLargeDelay.refreshConfigurations().get(); diff --git a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureCloudConfigAutoConfigurationTest.java b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureCloudConfigAutoConfigurationTest.java deleted file mode 100644 index 0aa67e589..000000000 --- a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/AzureCloudConfigAutoConfigurationTest.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE in the project root for - * license information. - */ -package com.microsoft.azure.spring.cloud.config; - -import static com.microsoft.azure.spring.cloud.config.TestConstants.CONFIG_ENABLED_PROP; -import static com.microsoft.azure.spring.cloud.config.TestConstants.CONN_STRING_PROP; -import static com.microsoft.azure.spring.cloud.config.TestConstants.STORE_ENDPOINT_PROP; -import static com.microsoft.azure.spring.cloud.config.TestConstants.TEST_CONN_STRING; -import static com.microsoft.azure.spring.cloud.config.TestConstants.TEST_STORE_NAME; -import static com.microsoft.azure.spring.cloud.config.TestUtils.propPair; -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.Test; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -public class AzureCloudConfigAutoConfigurationTest { - private static final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues(propPair(CONN_STRING_PROP, TEST_CONN_STRING), - propPair(STORE_ENDPOINT_PROP, TEST_STORE_NAME)) - .withConfiguration(AutoConfigurations.of(AzureConfigBootstrapConfiguration.class, - AzureCloudConfigAutoConfiguration.class)); - - @Test - public void watchEnabledNotConfiguredShouldNotCreateWatch() { - contextRunner.withPropertyValues(propPair(CONFIG_ENABLED_PROP, "true")).run(context -> { - assertThat(context).hasSingleBean(ConfigListener.class); - }); - } -} diff --git a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/TestConstants.java b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/TestConstants.java index 45772dbf2..96166a1d7 100644 --- a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/TestConstants.java +++ b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/TestConstants.java @@ -76,6 +76,8 @@ private TestConstants() { + "\"conditions\":{\"client_filters\":[{\"Name\":\"TestFilter\"}]}}"; public static final String FEATURE_BOOLEAN_VALUE = "{\"id\":\"Beta\",\"description\":\"\",\"enabled\":true," + "\"conditions\":{\"client_filters\":[]}}"; + public static final String FEATURE_VALUE_PARAMETERS = "{\"id\":\"Alpha\",\"description\":\"\",\"enabled\":true," + + "\"conditions\":{\"client_filters\":[{\"Name\":\"TestFilter\",\"Parameters\":{\"key\":\"value\"}}]}}"; public static final String FEATURE_LABEL = ""; public static final String LIST_KEY_1 = "test.list[0].key"; diff --git a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/TestUtils.java b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/TestUtils.java index ee83885f7..f97eb8c2f 100644 --- a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/TestUtils.java +++ b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/TestUtils.java @@ -31,16 +31,16 @@ static ConfigurationSetting createItem(String context, String key, String value, return item; } - static void addStore(AzureCloudConfigProperties properties, String storeName, String connectionString) { - addStore(properties, storeName, connectionString, null); + static void addStore(AppConfigurationProperties properties, String storeEndpoint, String connectionString) { + addStore(properties, storeEndpoint, connectionString, null); } - static void addStore(AzureCloudConfigProperties properties, String storeName, String connectionString, + static void addStore(AppConfigurationProperties properties, String storeEndpoint, String connectionString, String label) { List stores = properties.getStores(); ConfigStore store = new ConfigStore(); store.setConnectionString(connectionString); - store.setEndpoint(storeName); + store.setEndpoint(storeEndpoint); store.setLabel(label); stores.add(store); properties.setStores(stores); diff --git a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/pipline/policies/BaseAppConfigurationPolicyTest.java b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/pipline/policies/BaseAppConfigurationPolicyTest.java index fa9a6314a..90271ee3e 100644 --- a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/pipline/policies/BaseAppConfigurationPolicyTest.java +++ b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/pipline/policies/BaseAppConfigurationPolicyTest.java @@ -51,7 +51,7 @@ public void processTest() throws MalformedURLException { String userAgent = contextMock.getHttpRequest().getHeaders().get(HttpHeaders.USER_AGENT).getValue(); assertEquals("null/null " + PRE_USER_AGENT, userAgent); - assertEquals("RequestType=Startup,Host=None", + assertEquals("RequestType=Startup", contextMock.getHttpRequest().getHeaders().get("Correlation-Context").getValue()); } @@ -69,7 +69,7 @@ public void watchUpdateTest() throws MalformedURLException { String userAgent = contextMock.getHttpRequest().getHeaders().get(HttpHeaders.USER_AGENT).getValue(); assertEquals("null/null " + PRE_USER_AGENT, userAgent); - assertEquals("RequestType=Startup,Host=None", + assertEquals("RequestType=Startup", contextMock.getHttpRequest().getHeaders().get("Correlation-Context").getValue()); url = new URL("https://www.test.url/revisions"); @@ -82,7 +82,7 @@ public void watchUpdateTest() throws MalformedURLException { assertEquals("null/null " + PRE_USER_AGENT, userAgent); - assertEquals("RequestType=Watch,Host=None", + assertEquals("RequestType=Watch", contextMock.getHttpRequest().getHeaders().get("Correlation-Context").getValue()); url = new URL("https://www.test.url/kv"); @@ -94,7 +94,7 @@ public void watchUpdateTest() throws MalformedURLException { policy.process(contextMock, nextMock); assertEquals("null/null " + PRE_USER_AGENT, userAgent); - assertEquals("RequestType=Watch,Host=None", + assertEquals("RequestType=Watch", contextMock.getHttpRequest().getHeaders().get("Correlation-Context").getValue()); } diff --git a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/stores/AzureConfigPropertySourceLocatorTest.java b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/stores/AzureConfigPropertySourceLocatorTest.java deleted file mode 100644 index 64f4def35..000000000 --- a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/stores/AzureConfigPropertySourceLocatorTest.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE in the project root for - * license information. - */ -package com.microsoft.azure.spring.cloud.config.stores; - -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.Environment; - -import com.microsoft.azure.spring.cloud.config.AppConfigProviderProperties; -import com.microsoft.azure.spring.cloud.config.AzureCloudConfigProperties; -import com.microsoft.azure.spring.cloud.config.AzureConfigPropertySourceLocator; -import com.microsoft.azure.spring.cloud.config.KeyVaultCredentialProvider; - -public class AzureConfigPropertySourceLocatorTest { - - @Mock - private AppConfigProviderProperties appProperties; - - @Mock - private AzureCloudConfigProperties properties; - - @Mock - private ClientStore clients; - - @Mock - private ConfigStore configStore; - - private AzureConfigPropertySourceLocator azureConfigPropertySourceLocator; - - private KeyVaultCredentialProvider tokenCredentialProvider = null; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - - } - - @Test - public void awaitOnError() throws Exception { - List configStores = new ArrayList(); - configStores.add(configStore); - AzureCloudConfigProperties properties = new AzureCloudConfigProperties(); - properties.setProfileSeparator("_"); - properties.setName("TestStoreName"); - properties.setStores(configStores); - - appProperties.setPrekillTime(5); - - Environment env = Mockito.mock(ConfigurableEnvironment.class); - String[] array = {}; - when(env.getActiveProfiles()).thenReturn(array); - String[] labels = { "" }; - when(configStore.getLabels()).thenReturn(labels); - when(clients.listSettings(Mockito.any(), Mockito.any())).thenThrow(new NullPointerException("")); - when(appProperties.getPrekillTime()).thenReturn(-60); - when(appProperties.getStartDate()).thenReturn(new Date()); - - azureConfigPropertySourceLocator = new AzureConfigPropertySourceLocator(properties, appProperties, clients, - tokenCredentialProvider); - - boolean threwException = false; - try { - azureConfigPropertySourceLocator.locate(env); - } catch (Exception e) { - threwException = true; - } - assertTrue(threwException); - verify(appProperties, times(1)).getPrekillTime(); - } - -} diff --git a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/stores/ClientStoreTest.java b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/stores/ClientStoreTest.java new file mode 100644 index 000000000..fdf5f1c79 --- /dev/null +++ b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/stores/ClientStoreTest.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for + * license information. + */ +package com.microsoft.azure.spring.cloud.config.stores; + +import static com.microsoft.azure.spring.cloud.config.TestConstants.TEST_CONN_STRING; +import static com.microsoft.azure.spring.cloud.config.TestConstants.TEST_ENDPOINT; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import com.azure.core.credential.TokenCredential; +import com.azure.core.http.HttpHeaders; +import com.azure.core.http.HttpMethod; +import com.azure.core.http.HttpRequest; +import com.azure.core.http.policy.RetryPolicy; +import com.azure.core.http.rest.PagedFlux; +import com.azure.core.http.rest.PagedResponse; +import com.azure.core.http.rest.PagedResponseBase; +import com.azure.data.appconfiguration.ConfigurationAsyncClient; +import com.azure.data.appconfiguration.ConfigurationClientBuilder; +import com.azure.data.appconfiguration.models.ConfigurationSetting; +import com.azure.data.appconfiguration.models.SettingSelector; +import com.microsoft.azure.spring.cloud.config.AppConfigurationCredentialProvider; +import com.microsoft.azure.spring.cloud.config.AppConfigurationProviderProperties; +import com.microsoft.azure.spring.cloud.config.pipline.policies.BaseAppConfigurationPolicy; +import com.microsoft.azure.spring.cloud.config.resource.Connection; +import com.microsoft.azure.spring.cloud.config.resource.ConnectionPool; + +import reactor.core.publisher.Mono; + +public class ClientStoreTest { + + private ClientStore clientStore; + + static TokenCredential tokenCredential; + + @Mock + private ConfigurationClientBuilder builderMock; + + @Mock + private ConfigurationAsyncClient clientMock; + + @Mock + private TokenCredential credentialMock; + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + private List> pagedResponses; + + private AppConfigurationProviderProperties appProperties; + + private ConnectionPool pool; + + @Before + public void init() { + appProperties = new AppConfigurationProviderProperties(); + appProperties.setMaxRetries(0); + pool = new ConnectionPool(); + } + + @Test + public void connectWithConnectionString() throws IOException { + pool.put(TEST_ENDPOINT, TEST_CONN_STRING); + + SettingSelector selector = new SettingSelector(); + + clientStore = new ClientStore(appProperties, pool, null); + ClientStore test = Mockito.spy(clientStore); + Mockito.doReturn(builderMock).when(test).getBuilder(); + + when(builderMock.addPolicy(Mockito.any(BaseAppConfigurationPolicy.class))).thenReturn(builderMock); + when(builderMock.retryPolicy(Mockito.any(RetryPolicy.class))).thenReturn(builderMock); + + when(builderMock.endpoint(Mockito.eq(TEST_ENDPOINT))).thenReturn(builderMock); + when(builderMock.buildAsyncClient()).thenReturn(clientMock); + + when(clientMock.listConfigurationSettings(Mockito.any(SettingSelector.class))) + .thenReturn(getConfigurationPagedFlux(1)); + + assertEquals(test.listSettings(selector, TEST_ENDPOINT).size(), 1); + } + + @Test + public void testPrivider() throws IOException { + pool.put(TEST_ENDPOINT, new Connection(TEST_ENDPOINT, "")); + + SettingSelector selector = new SettingSelector(); + AppConfigurationCredentialProvider provider = new AppConfigurationCredentialProvider() { + + @Override + public TokenCredential getAppConfigCredential(String uri) { + assertEquals(TEST_ENDPOINT, uri); + return credentialMock; + } + }; + + clientStore = new ClientStore(appProperties, pool, provider); + ClientStore test = Mockito.spy(clientStore); + Mockito.doReturn(builderMock).when(test).getBuilder(); + + when(builderMock.addPolicy(Mockito.any(BaseAppConfigurationPolicy.class))).thenReturn(builderMock); + when(builderMock.retryPolicy(Mockito.any(RetryPolicy.class))).thenReturn(builderMock); + + when(builderMock.endpoint(Mockito.eq(TEST_ENDPOINT))).thenReturn(builderMock); + when(builderMock.buildAsyncClient()).thenReturn(clientMock); + + when(clientMock.listConfigurationSettings(Mockito.any(SettingSelector.class))) + .thenReturn(getConfigurationPagedFlux(1)); + + assertEquals(test.listSettings(selector, TEST_ENDPOINT).size(), 1); + } + + @Test(expected = IllegalArgumentException.class) + public void multipleArgumentsClientIdProvider() throws IOException { + pool.put(TEST_ENDPOINT, new Connection(TEST_ENDPOINT, "testclientid")); + + SettingSelector selector = new SettingSelector(); + AppConfigurationCredentialProvider provider = new AppConfigurationCredentialProvider() { + + @Override + public TokenCredential getAppConfigCredential(String uri) { + assertEquals(TEST_ENDPOINT, uri); + return credentialMock; + } + }; + + clientStore = new ClientStore(appProperties, pool, provider); + ClientStore test = Mockito.spy(clientStore); + Mockito.doReturn(builderMock).when(test).getBuilder(); + + when(builderMock.addPolicy(Mockito.any(BaseAppConfigurationPolicy.class))).thenReturn(builderMock); + when(builderMock.retryPolicy(Mockito.any(RetryPolicy.class))).thenReturn(builderMock); + + assertEquals(test.listSettings(selector, TEST_ENDPOINT).size(), 1); + } + + @Test(expected = IllegalArgumentException.class) + public void multipleArgumentsConnectionStringProvider() throws IOException { + pool.put(TEST_ENDPOINT, new Connection(TEST_CONN_STRING)); + + SettingSelector selector = new SettingSelector(); + AppConfigurationCredentialProvider provider = new AppConfigurationCredentialProvider() { + + @Override + public TokenCredential getAppConfigCredential(String uri) { + assertEquals(TEST_ENDPOINT, uri); + return credentialMock; + } + }; + + clientStore = new ClientStore(appProperties, pool, provider); + ClientStore test = Mockito.spy(clientStore); + Mockito.doReturn(builderMock).when(test).getBuilder(); + + when(builderMock.addPolicy(Mockito.any(BaseAppConfigurationPolicy.class))).thenReturn(builderMock); + when(builderMock.retryPolicy(Mockito.any(RetryPolicy.class))).thenReturn(builderMock); + + assertEquals(test.listSettings(selector, TEST_ENDPOINT).size(), 1); + } + + @Test + public void watchedKeyNamesWildcardTest() { + clientStore = new ClientStore(appProperties, pool, null); + ConfigStore store = new ConfigStore(); + HashMap> storeContextsMap = new HashMap>(); + + store.setWatchedKey("*"); + store.setEndpoint(TEST_ENDPOINT); + ArrayList contexts = new ArrayList(); + contexts.add("/application/"); + + storeContextsMap.put(TEST_ENDPOINT, contexts); + + assertEquals("/application/*", clientStore.watchedKeyNames(store, storeContextsMap)); + } + + private PagedFlux getConfigurationPagedFlux(int noOfPages) throws MalformedURLException { + HttpHeaders httpHeaders = new HttpHeaders().put("header1", "value1") + .put("header2", "value2"); + HttpRequest httpRequest = new HttpRequest(HttpMethod.GET, new URL("http://localhost")); + + String deserializedHeaders = "header1,value1,header2,value2"; + + pagedResponses = IntStream.range(0, noOfPages) + .boxed() + .map(i -> createPagedResponse(httpRequest, httpHeaders, deserializedHeaders, i, noOfPages)) + .collect(Collectors.toList()); + + return new PagedFlux( + () -> pagedResponses.isEmpty() ? Mono.empty() : Mono.just(pagedResponses.get(0)), + continuationToken -> getNextPage(continuationToken, pagedResponses)); + } + + private PagedResponseBase createPagedResponse(HttpRequest httpRequest, + HttpHeaders httpHeaders, String deserializedHeaders, int i, int noOfPages) { + return new PagedResponseBase<>(httpRequest, 200, + httpHeaders, + getItems(i), + i < noOfPages - 1 ? String.valueOf(i + 1) : null, + deserializedHeaders); + } + + private Mono> getNextPage(String continuationToken, + List> pagedResponses) { + if (continuationToken == null || continuationToken.isEmpty()) { + return Mono.empty(); + } + return Mono.just(pagedResponses.get(Integer.valueOf(continuationToken))); + } + + private List getItems(int i) { + ArrayList lst = new ArrayList(); + lst.add(new ConfigurationSetting()); + return lst; + } + +} diff --git a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/stores/ConfigStoreTest.java b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/stores/ConfigStoreTest.java new file mode 100644 index 000000000..b02566321 --- /dev/null +++ b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/stores/ConfigStoreTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for + * license information. + */ +package com.microsoft.azure.spring.cloud.config.stores; + +import static org.junit.Assert.fail; + +import org.junit.Test; + +public class ConfigStoreTest { + + @Test(expected = IllegalArgumentException.class) + public void invalidLabel() { + ConfigStore configStore = new ConfigStore(); + configStore.setLabel("*"); + configStore.validateAndInit(); + fail(); + } + + @Test(expected = IllegalStateException.class) + public void invalidEndpoint() { + ConfigStore configStore = new ConfigStore(); + configStore.setConnectionString("Endpoint=a^a;Id=fake-conn-id;Secret=ZmFrZS1jb25uLXNlY3JldA=="); + configStore.validateAndInit(); + fail(); + } + +} diff --git a/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/stores/KeyVaultClientTest.java b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/stores/KeyVaultClientTest.java new file mode 100644 index 000000000..87d6b05cd --- /dev/null +++ b/spring-cloud-azure-appconfiguration-config/src/test/java/com/microsoft/azure/spring/cloud/config/stores/KeyVaultClientTest.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for + * license information. + */ +package com.microsoft.azure.spring.cloud.config.stores; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import com.azure.core.credential.TokenCredential; +import com.azure.security.keyvault.secrets.SecretAsyncClient; +import com.azure.security.keyvault.secrets.SecretClientBuilder; +import com.azure.security.keyvault.secrets.models.KeyVaultSecret; +import com.microsoft.azure.spring.cloud.config.AppConfigurationProperties; +import com.microsoft.azure.spring.cloud.config.KeyVaultCredentialProvider; +import com.microsoft.azure.spring.cloud.config.resource.AppConfigManagedIdentityProperties; + +import reactor.core.publisher.Mono; + +public class KeyVaultClientTest { + + private KeyVaultClient clientStore; + + static TokenCredential tokenCredential; + + @Mock + private SecretClientBuilder builderMock; + + @Mock + private SecretAsyncClient clientMock; + + @Mock + private TokenCredential credentialMock; + + @Mock + private Mono monoSecret; + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + private AppConfigurationProperties azureProperties; + + @Test(expected = IllegalArgumentException.class) + public void multipleArguments() throws IOException, URISyntaxException { + azureProperties = new AppConfigurationProperties(); + AppConfigManagedIdentityProperties msiProps = new AppConfigManagedIdentityProperties(); + msiProps.setClientId("testclientid"); + azureProperties.setManagedIdentity(msiProps); + + String keyVaultUri = "https://keyvault.vault.azure.net/secrets/mySecret"; + + KeyVaultCredentialProvider provider = new KeyVaultCredentialProvider() { + + @Override + public TokenCredential getKeyVaultCredential(String uri) { + assertEquals("https://keyvault.vault.azure.net", uri); + return credentialMock; + } + }; + + clientStore = new KeyVaultClient(azureProperties, new URI(keyVaultUri), provider); + + KeyVaultClient test = Mockito.spy(clientStore); + Mockito.doReturn(builderMock).when(test).getBuilder(); + + test.build(); + fail(); + } + + @Test + public void configProviderAuth() throws IOException, URISyntaxException { + azureProperties = new AppConfigurationProperties(); + AppConfigManagedIdentityProperties msiProps = null; + azureProperties.setManagedIdentity(msiProps); + + String keyVaultUri = "https://keyvault.vault.azure.net/secrets/mySecret"; + + KeyVaultCredentialProvider provider = new KeyVaultCredentialProvider() { + + @Override + public TokenCredential getKeyVaultCredential(String uri) { + assertEquals("https://keyvault.vault.azure.net", uri); + return credentialMock; + } + }; + + clientStore = new KeyVaultClient(azureProperties, new URI(keyVaultUri), provider); + + KeyVaultClient test = Mockito.spy(clientStore); + Mockito.doReturn(builderMock).when(test).getBuilder(); + + when(builderMock.vaultUrl(Mockito.any())).thenReturn(builderMock); + when(builderMock.buildAsyncClient()).thenReturn(clientMock);; + + test.build(); + + when(clientMock.getSecret(Mockito.any(), Mockito.any())) + .thenReturn(monoSecret); + when(monoSecret.block(Mockito.any())).thenReturn(new KeyVaultSecret("", "")); + + assertNotNull(test.getSecret(new URI(keyVaultUri), 10)); + assertEquals(test.getSecret(new URI(keyVaultUri), 10).getName(), ""); + } + + @Test + public void configClientIdAuth() throws IOException, URISyntaxException { + azureProperties = new AppConfigurationProperties(); + AppConfigManagedIdentityProperties msiProps = new AppConfigManagedIdentityProperties(); + msiProps.setClientId("testClientId"); + AppConfigManagedIdentityProperties test2 = Mockito.spy(msiProps); + azureProperties.setManagedIdentity(test2); + + String keyVaultUri = "https://keyvault.vault.azure.net/secrets/mySecret"; + + clientStore = new KeyVaultClient(azureProperties, new URI(keyVaultUri), null); + + KeyVaultClient test = Mockito.spy(clientStore); + Mockito.doReturn(builderMock).when(test).getBuilder(); + + when(builderMock.vaultUrl(Mockito.any())).thenReturn(builderMock); + when(builderMock.buildAsyncClient()).thenReturn(clientMock);; + + test.build(); + + when(clientMock.getSecret(Mockito.any(), Mockito.any())) + .thenReturn(monoSecret); + when(monoSecret.block(Mockito.any())).thenReturn(new KeyVaultSecret("", "")); + + assertNotNull(test.getSecret(new URI(keyVaultUri), 10)); + assertEquals(test.getSecret(new URI(keyVaultUri), 10).getName(), ""); + + verify(test2, times(2)).getClientId(); + } + +} diff --git a/spring-cloud-azure-appconfiguration-config/src/main/resources/mockito-extensions/org.mockito.plugins.MockMaker b/spring-cloud-azure-appconfiguration-config/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker similarity index 100% rename from spring-cloud-azure-appconfiguration-config/src/main/resources/mockito-extensions/org.mockito.plugins.MockMaker rename to spring-cloud-azure-appconfiguration-config/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/spring-cloud-azure-feature-management-web/pom.xml b/spring-cloud-azure-feature-management-web/pom.xml index 559f91046..1500833d7 100644 --- a/spring-cloud-azure-feature-management-web/pom.xml +++ b/spring-cloud-azure-feature-management-web/pom.xml @@ -16,18 +16,6 @@ org.springframework.boot - spring-boot-starter - - - com.fasterxml.jackson.core - jackson-annotations - - - com.fasterxml.jackson.core - jackson-databind - - - org.springframework.boot spring-boot-starter-test test diff --git a/spring-cloud-azure-feature-management-web/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureHandler.java b/spring-cloud-azure-feature-management-web/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureHandler.java index 92c4d823b..6e522a2a3 100644 --- a/spring-cloud-azure-feature-management-web/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureHandler.java +++ b/spring-cloud-azure-feature-management-web/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureHandler.java @@ -34,9 +34,11 @@ public class FeatureHandler extends HandlerInterceptorAdapter { private IDisabledFeaturesHandler disabledFeaturesHandler; - public FeatureHandler(FeatureManager featureManager, FeatureManagerSnapshot featureManagerSnapshot) { + public FeatureHandler(FeatureManager featureManager, FeatureManagerSnapshot featureManagerSnapshot, + IDisabledFeaturesHandler disabledFeaturesHandler) { this.featureManager = featureManager; this.featureManagerSnapshot = featureManagerSnapshot; + this.disabledFeaturesHandler = disabledFeaturesHandler; } /** @@ -78,6 +80,13 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons } if (!isEnabled && disabledFeaturesHandler != null) { response = disabledFeaturesHandler.handleDisabledFeatures(request, response); + } else if (!isEnabled) { + try { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + } catch (IOException e) { + LOGGER.error("Error thrown while returning 404 on false feature.", e); + return false; + } } return isEnabled; } diff --git a/spring-cloud-azure-feature-management-web/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureManagementWebConfiguration.java b/spring-cloud-azure-feature-management-web/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureManagementWebConfiguration.java index 7b340a688..74cd4d82e 100644 --- a/spring-cloud-azure-feature-management-web/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureManagementWebConfiguration.java +++ b/spring-cloud-azure-feature-management-web/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureManagementWebConfiguration.java @@ -5,6 +5,7 @@ */ package com.microsoft.azure.spring.cloud.feature.manager; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -23,8 +24,9 @@ public FeatureManagerSnapshot featureManagerSnapshot(FeatureManager featureManag } @Bean - public FeatureHandler featureHandler(FeatureManager featureManager, FeatureManagerSnapshot snapshot) { - return new FeatureHandler(featureManager, snapshot); + public FeatureHandler featureHandler(FeatureManager featureManager, FeatureManagerSnapshot snapshot, + @Autowired(required = false) IDisabledFeaturesHandler disabledFeaturesHandler) { + return new FeatureHandler(featureManager, snapshot, disabledFeaturesHandler); } @Bean diff --git a/spring-cloud-azure-feature-management-web/src/test/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureHandlerTest.java b/spring-cloud-azure-feature-management-web/src/test/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureHandlerTest.java index 676f1025a..1c0ea4f14 100644 --- a/spring-cloud-azure-feature-management-web/src/test/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureHandlerTest.java +++ b/spring-cloud-azure-feature-management-web/src/test/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureHandlerTest.java @@ -7,8 +7,12 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.io.IOException; import java.lang.reflect.Method; import javax.servlet.http.HttpServletRequest; @@ -23,7 +27,6 @@ import org.springframework.web.method.HandlerMethod; import reactor.core.publisher.Mono; - /** * Unit test for simple App. */ @@ -50,6 +53,9 @@ public class FeatureHandlerTest { @Mock HandlerMethod handlerMethod; + + @Mock + FeatureHandler featureHandler2; @Test public void preHandleNotHandler() { @@ -65,7 +71,7 @@ public void preHandleNoFeatureOn() throws NoSuchMethodException, SecurityExcepti } @Test - public void preHandlFeatureOn() throws NoSuchMethodException, SecurityException { + public void preHandleFeatureOn() throws NoSuchMethodException, SecurityException { Method method = TestClass.class.getMethod("featureOnAnnotation"); when(handlerMethod.getMethod()).thenReturn(method); when(featureManager.isEnabledAsync(Mockito.matches("test"))).thenReturn(Mono.just(true)); @@ -74,7 +80,7 @@ public void preHandlFeatureOn() throws NoSuchMethodException, SecurityException } @Test - public void preHandlFeatureOnSnapshot() throws NoSuchMethodException, SecurityException { + public void preHandleFeatureOnSnapshot() throws NoSuchMethodException, SecurityException { Method method = TestClass.class.getMethod("featureOnAnnotationSnapshot"); when(handlerMethod.getMethod()).thenReturn(method); when(featureManagerSnapshot.isEnabledAsync(Mockito.matches("test"))).thenReturn(Mono.just(true)); @@ -83,7 +89,7 @@ public void preHandlFeatureOnSnapshot() throws NoSuchMethodException, SecurityEx } @Test - public void preHandlFeatureOnNotEnabled() throws NoSuchMethodException, SecurityException { + public void preHandleFeatureOnNotEnabled() throws NoSuchMethodException, SecurityException { Method method = TestClass.class.getMethod("featureOnAnnotation"); when(handlerMethod.getMethod()).thenReturn(method); when(featureManager.isEnabledAsync(Mockito.matches("test"))).thenReturn(Mono.just(false)); @@ -92,13 +98,37 @@ public void preHandlFeatureOnNotEnabled() throws NoSuchMethodException, Security } @Test - public void preHandlFeatureOnRedirect() throws NoSuchMethodException, SecurityException { + public void preHandleFeatureOnRedirect() throws NoSuchMethodException, SecurityException { Method method = TestClass.class.getMethod("featureOnAnnotaitonRedirected"); when(handlerMethod.getMethod()).thenReturn(method); when(featureManager.isEnabledAsync(Mockito.matches("test"))).thenReturn(Mono.just(false)); assertFalse(featureHandler.preHandle(request, response, handlerMethod)); } + + @Test + public void preHandleNoDisabledFeatures() throws NoSuchMethodException, SecurityException, IOException { + featureHandler2 = new FeatureHandler(featureManager, featureManagerSnapshot, null); + Method method = TestClass.class.getMethod("featureOnAnnotaitonRedirected"); + when(handlerMethod.getMethod()).thenReturn(method); + when(featureManager.isEnabledAsync(Mockito.matches("test"))).thenReturn(Mono.just(false)); + + assertFalse(featureHandler2.preHandle(request, response, handlerMethod)); + verify(response, times(1)).sendError(Mockito.eq(HttpServletResponse.SC_NOT_FOUND)); + } + + @Test + public void preHandleNoDisabledFeaturesError() throws NoSuchMethodException, SecurityException, IOException { + featureHandler2 = new FeatureHandler(featureManager, featureManagerSnapshot, null); + Method method = TestClass.class.getMethod("featureOnAnnotaitonRedirected"); + when(handlerMethod.getMethod()).thenReturn(method); + when(featureManager.isEnabledAsync(Mockito.matches("test"))).thenReturn(Mono.just(false)); + + doThrow(new IOException()).when(response).sendError(Mockito.eq(HttpServletResponse.SC_NOT_FOUND)); + + assertFalse(featureHandler2.preHandle(request, response, handlerMethod)); + verify(response, times(1)).sendError(Mockito.eq(HttpServletResponse.SC_NOT_FOUND)); + } protected class TestClass { diff --git a/spring-cloud-azure-feature-management-web/src/test/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureManagerSnapshotTest.java b/spring-cloud-azure-feature-management-web/src/test/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureManagerSnapshotTest.java index f723c0ec4..4f8862097 100644 --- a/spring-cloud-azure-feature-management-web/src/test/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureManagerSnapshotTest.java +++ b/spring-cloud-azure-feature-management-web/src/test/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureManagerSnapshotTest.java @@ -49,5 +49,17 @@ public void setAttribute() throws InterruptedException, ExecutionException { assertTrue(featureManagerSnapshot.isEnabledAsync("setAttribute").block()); verify(featureManager, times(1)).isEnabledAsync("setAttribute"); } + + @Test + public void setSavedValue() throws InterruptedException, ExecutionException { + when(featureManager.isEnabledAsync(Mockito.matches("setAttribute"))).thenReturn(Mono.just(true)); + + assertTrue(featureManagerSnapshot.isEnabledAsync("setAttribute").block()); + verify(featureManager, times(1)).isEnabledAsync("setAttribute"); + + // The second time should return the same value, but not increase the non-snapshot count. + assertTrue(featureManagerSnapshot.isEnabledAsync("setAttribute").block()); + verify(featureManager, times(1)).isEnabledAsync("setAttribute"); + } } diff --git a/spring-cloud-azure-feature-management/README.md b/spring-cloud-azure-feature-management/README.md index fea9c7261..e9d299ce5 100644 --- a/spring-cloud-azure-feature-management/README.md +++ b/spring-cloud-azure-feature-management/README.md @@ -1,38 +1,44 @@ # Feature Management + Feature flags provide a way for Spring Boot applications to turn features on or off dynamically. Developers can use feature flags in simple use cases like conditional statement to more advanced scenarios like conditionally adding routes. Feature Flags are not dependent of any spring-cloud-azure dependencies, but may be used in conjunction with spring-cloud-azure-appconfiguration-config. Here are some of the benefits of using this library: + * A common convention for feature management * Low barrier-to-entry * Supports application.yml file feature flag setup * Feature Flag lifetime management * Configuration values can change in real-time, feature flags can be consistent across the entire request -### Feature Flags +## Feature Flags + Feature flags are composed of two parts, a name and a list of feature-filters that are used to turn the feature on. -### Feature Filters +## Feature Filters + Feature filters define a scenario for when a feature should be enabled. When a feature is evaluated for whether it is on or off, its list of feature-filters are traversed until one of the filters decides the feature should be enabled. At this point the feature is considered enabled and traversal through the feature filters stops. If no feature filter indicates that the feature should be enabled, then it will be considered disabled. As an example, a Microsoft Edge browser feature filter could be designed. This feature filter would activate any features it is attached to as long as an HTTP request is coming from Microsoft Edge. ## Registration + The Spring Configuration system is used to determine the state of feature flags. Any system can be used to have them read in, such as application.yml, spring-cloud-azure-appconfiguration-config and more. -### Feature Flag Declaration +## Feature Flag Declaration + The feature management library supports application.yml or bootstrap.yml as a feature flag source. Below we have an example of the format used to set up feature flags in a application.yml file. -``` +```yaml feature-management: feature-set: features: - FeatureT: false - FeatureU: - EnabledFor: + feature-t: false + feature-u: + enabled-for: - name: Random - FeatureV: - EnabledFor: + feature-v: + enabled-for: - name: TimeWindow parameters: @@ -40,25 +46,27 @@ feature-management: end: "Mon, 01 July 2019 00:00:00 GMT" ``` -The `feature-management` section of the YAML document is used by convention to load feature flags. In the section above, we see that we have provided three different features. Features define their filters using the `EnabledFor` property. We can see that feature `FeatureT` is set to false with no filters set. `FeatureT` will allways return false, this can also be done for true. `FeatureU` which has only one feature filter `Random` which does not require any configuration so it only has the name property. `FeatureV` it specifies a feature filter named `TimeWindow`. This is an example of a configurable feature filter. We can see in the example that the filter has a parameter's property. This is used to configure the filter. In this case, the start and end times for the feature to be active are configured. +The `feature-management` section of the YAML document is used by convention to load feature flags. In the section above, we see that we have provided three different features. Features define their filters using the `enabled-for` property. We can see that feature `feature-t` is set to false with no filters set. `feature-t` will always return false, this can also be done for true. `feature-u` which has only one feature filter `Random` which does not require any configuration so it only has the name property. `feature-v` it specifies a feature filter named `TimeWindow`. This is an example of a configurable feature filter. We can see in the example that the filter has a parameter's property. This is used to configure the filter. In this case, the start and end times for the feature to be active are configured. ### Supported properties -Name | Description | Required | Default +Name | Description | Required | Default ---|---|---|--- spring.cloud.azure.feature.management.fail-fast | Whether throw RuntimeException or not when exception occurs | No | true ## Consumption + The simplest use case for feature flags is to do a conditional check for whether a feature is enabled to take different paths in code. The use cases grow when additional using spring-cloud-azure-feature-flag-web to manage web based features. ### Feature Check -The basic form of feature management is checking if a feature is enabled and then performing actions based on the result. This is done through the autowiring `FeatureManager` and calling it's `isEnabled` method. -``` +The basic form of feature management is checking if a feature is enabled and then performing actions based on the result. This is done through the autowiring `FeatureManager` and calling it's `isEnabledAsync` method. + +```java @Autowired FeatureManager featureManager; -if(featureManager.isEnabled("FeatureT")) { +if(featureManager.isEnabledAsync("feature-t").block()) { // Do Something } ``` @@ -66,23 +74,25 @@ if(featureManager.isEnabled("FeatureT")) { `FeatureManager` can also be accessed by `@Component` classes. ### Controllers -When using the Feature Management Web library you can require that a given feature is enabled in order to execute. This can be done by using the `@FeatureOn` annotation. -``` +When using the Feature Management Web library you can require that a given feature is enabled in order to execute. This can be done by using the `@FeatureOn` annotation. + +```java @GetMapping("/featureT") -@FeatureGate(feature = "FeatureT") +@FeatureGate(feature = "feature-t") @ResponseBody public String featureT() { ... } ``` -The `featureT` endpoint can only be accessed if "FeatureT" is enabled. +The `featureT` endpoint can only be accessed if "feature-t" is enabled. ### Disabled Action Handling + When a controller is blocked because the feature it specifies is disabled, `IDisabledFeaturesHandler` will be invoked. By default, a HTTP 404 is returned. This can be overridden using implementing `IDisabledFeaturesHandler`. -``` +```java @Component public class DisabledFeaturesHandler implements IDisabledFeaturesHandler{ @@ -97,11 +107,12 @@ public class DisabledFeaturesHandler implements IDisabledFeaturesHandler{ ``` ### Routing + Certain routes may expose application capabilites that are gated by features. These routes can redirected if a feature is disabled to another endpoint. -``` +```java @GetMapping("/featureT") -@FeatureGate(feature = "FeatureT" fallback= "/oldEndpoint") +@FeatureGate(feature = "feature-t" fallback= "/oldEndpoint") @ResponseBody public String featureT() { ... @@ -115,25 +126,28 @@ public String oldEndpoint() { ``` ## Implementing a Feature Filter -Creating a feature filter provides a way to enable features bassed on criteria that you define. To implement a feature filter, the `FeatureFilter` interface must be implemented. `FeatureFilter` has a single method `evaluate`. When a feature specifies that it can be enabled with a feature filter, the `evaluate` method is called. If `evaluate` returns `true` it means the feature should be enabled. If `false` it will continue evaluating the Feature's filters until one returns true. If all return `false` then the feature is off. + +Creating a feature filter provides a way to enable features based on criteria that you define. To implement a feature filter, the `FeatureFilter` interface must be implemented. `FeatureFilter` has a single method `evaluate`. When a feature specifies that it can be enabled with a feature filter, the `evaluate` method is called. If `evaluate` returns `true` it means the feature should be enabled. If `false` it will continue evaluating the Feature's filters until one returns true. If all return `false` then the feature is off. Feature filters are found by being defined as `@Component` where there name matches the expected filter defined in the configuration. -``` +```java @Component("Random") -public class Random implements FeatureFilter{ +public class Random implements FeatureFilter { @Override public boolean evaluate(FeatureFilterEvaluationContext context) { double chance = Double.valueOf((String) context.getParameters().get("chance")); - return Math.random() > chance/100; + return Math.random() > chance/100; } } ``` ### Parameterized Feature Filters + Some feature filters require parameters to decide whether a feature should be turned on or not. For example a browser feature filter may turn on a feature for a certain set of browsers. It may be desired that Edge and Chrome browsers enable a feature, while FireFox does not. To do this a feature filter can be designed to expect parameters. These parameters would be specified in the feature configuration, and in code would be accessible via the `FeatureFilterEvaluationContext` parameter of `evaluate`. `FeatureFilterEvaluationContext` has a property `parameters` which is a `HashMap`. ## Request Based Features/Snapshot + There are scenarios which require the state of a feature to remain consistent during the lifetime of a request. The values returned from the standard `FeatureManager` may change if the configuration source which it is pulling from is updated during the request. This can be prevented by using `FeatureManagerSnapshot` and `@FeatureOn( snapshot = true )`. `FeatureManagerSnapshot` can be retrieved in the same manner as `FeatureManager`. `FeatureManagerSnapshot` calls `FeatureManager`, but it caches the first evaluated state of a feature during a request and will return the same state of a feature during its lifetime. diff --git a/spring-cloud-azure-feature-management/pom.xml b/spring-cloud-azure-feature-management/pom.xml index 2e9736402..994b1a3b3 100644 --- a/spring-cloud-azure-feature-management/pom.xml +++ b/spring-cloud-azure-feature-management/pom.xml @@ -33,7 +33,6 @@ io.projectreactor.netty reactor-netty - 0.9.0.RELEASE
diff --git a/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureManagementConfiguration.java b/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureManagementConfiguration.java index cb2ce961c..8259b07b4 100644 --- a/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureManagementConfiguration.java +++ b/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureManagementConfiguration.java @@ -12,7 +12,7 @@ @Configuration @EnableConfigurationProperties({ FeatureManagementConfigProperties.class }) public class FeatureManagementConfiguration { - + @Bean public FeatureManager featureManager(FeatureManagementConfigProperties properties) { return new FeatureManager(properties); diff --git a/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureManager.java b/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureManager.java index e189801af..fb5b64489 100644 --- a/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureManager.java +++ b/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureManager.java @@ -6,8 +6,10 @@ package com.microsoft.azure.spring.cloud.feature.manager; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,6 +21,7 @@ import org.springframework.util.ReflectionUtils; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.microsoft.azure.spring.cloud.feature.manager.entities.Feature; import com.microsoft.azure.spring.cloud.feature.manager.entities.FeatureFilterEvaluationContext; @@ -50,6 +53,7 @@ public FeatureManager(FeatureManagementConfigProperties properties) { this.properties = properties; featureManagement = new HashMap(); onOff = new HashMap(); + mapper.setPropertyNamingStrategy(PropertyNamingStrategy.KEBAB_CASE); } /** @@ -60,12 +64,13 @@ public FeatureManager(FeatureManagementConfigProperties properties) { * * @param feature Feature being checked. * @return state of the feature + * @throws FilterNotFoundException */ - public Mono isEnabledAsync(String feature) { + public Mono isEnabledAsync(String feature) throws FilterNotFoundException { return Mono.just(checkFeatures(feature)); } - private boolean checkFeatures(String feature) { + private boolean checkFeatures(String feature) throws FilterNotFoundException { boolean enabled = false; if (featureManagement == null || onOff == null) { return false; @@ -80,7 +85,7 @@ private boolean checkFeatures(String feature) { return false; } - for (FeatureFilterEvaluationContext filter : featureItem.getEnabledFor()) { + for (FeatureFilterEvaluationContext filter : featureItem.getEnabledFor().values()) { if (filter != null && filter.getName() != null) { try { FeatureFilter featureFilter = (FeatureFilter) context.getBean(filter.getName()); @@ -89,7 +94,7 @@ private boolean checkFeatures(String feature) { LOGGER.error("Was unable to find Filter " + filter.getName() + ". Does the class exist and set as an @Component?"); if (properties.isFailFast()) { - String message = "Fail fast is set and a Filter was unable to be found."; + String message = "Fail fast is set and a Filter was unable to be found"; ReflectionUtils.rethrowRuntimeException(new FilterNotFoundException(message, e, filter)); } } @@ -134,21 +139,28 @@ private void addToFeatures(Map features, Str } } - @SuppressWarnings("unchecked") @Override public void putAll(Map m) { if (m == null) { return; } - - if (m.size() == 1 && m.get("featureManagement") != null) { - m = (Map) m.get("featureManagement"); - } - + for (String key : m.keySet()) { addToFeatures(m, key, ""); } } + + /** + * Returns the names of all features flags + * @return a set of all feature names + */ + public Set getAllFeatureNames() { + Set allFeatures = new HashSet(); + + allFeatures.addAll(onOff.keySet()); + allFeatures.addAll(featureManagement.keySet()); + return allFeatures; + } /** * @return the featureManagement @@ -163,4 +175,6 @@ HashMap getFeatureManagement() { HashMap getOnOff() { return onOff; } + + } diff --git a/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FilterNotFoundException.java b/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FilterNotFoundException.java index 863a47a9a..7c979c28b 100644 --- a/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FilterNotFoundException.java +++ b/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FilterNotFoundException.java @@ -13,11 +13,13 @@ * failfast is enabled, which is true by default. * */ -public class FilterNotFoundException extends Exception { +public class FilterNotFoundException extends RuntimeException { private static final long serialVersionUID = 1L; private final FeatureFilterEvaluationContext filter; + + private final String message; /** * Creates a new instance of the FilterNotFoundException @@ -28,15 +30,16 @@ public class FilterNotFoundException extends Exception { */ public FilterNotFoundException(String message, Throwable cause, FeatureFilterEvaluationContext filter) { super(message, cause); + this.message = message; this.filter = filter; } @Override public String getMessage() { if (filter == null) { - return getCause().getMessage(); + return getCause().getMessage() + "."; } - return getCause().getMessage() + ", " + filter.toString(); + return this.message + ": " + filter.getName(); } diff --git a/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FilterParameters.java b/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FilterParameters.java index 095aa5923..fb9c57b1a 100644 --- a/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FilterParameters.java +++ b/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/FilterParameters.java @@ -7,10 +7,10 @@ public class FilterParameters { - public static final String PERCENTAGE_FILTER_SETTING = "PercentageFilterSetting"; + public static final String PERCENTAGE_FILTER_SETTING = "percentage-filter-setting"; - public static final String TIME_WINDOW_FILTER_SETTING_START = "TimeWindowFilterSettingStart"; + public static final String TIME_WINDOW_FILTER_SETTING_START = "time-window-filter-setting-start"; - public static final String TIME_WINDOW_FILTER_SETTING_END = "TimeWindowFilterSettingEnd"; + public static final String TIME_WINDOW_FILTER_SETTING_END = "time-window-filter-setting-end"; } diff --git a/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/entities/Feature.java b/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/entities/Feature.java index 4fbb3ea4d..b6723275e 100644 --- a/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/entities/Feature.java +++ b/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/entities/Feature.java @@ -5,9 +5,7 @@ */ package com.microsoft.azure.spring.cloud.feature.manager.entities; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @@ -17,12 +15,9 @@ public class Feature { @JsonProperty("key") private String key; - - @JsonProperty("enabledFor") - private List enabledFor; - @JsonProperty("EnabledFor") - private HashMap> filterMapper; + @JsonProperty("enabled-for") + private HashMap enabledFor; /** * @return the key @@ -41,29 +36,15 @@ public void setKey(String key) { /** * @return the enabledFor */ - public List getEnabledFor() { + public HashMap getEnabledFor() { return enabledFor; } /** * @param enabledFor the enabledFor to set */ - public void setEnabledFor(List enabledFor) { + public void setEnabledFor(HashMap enabledFor) { this.enabledFor = enabledFor; } - - public void setFilterMapper(HashMap filterMapper) { - if (filterMapper == null) { - return; - } - - if (enabledFor == null) { - enabledFor = new ArrayList(); - } - - for (Integer key: filterMapper.keySet()) { - enabledFor.add(filterMapper.get(key)); - } - } } diff --git a/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/entities/FeatureFilterEvaluationContext.java b/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/entities/FeatureFilterEvaluationContext.java index a87fd7beb..2e7ed39ba 100644 --- a/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/entities/FeatureFilterEvaluationContext.java +++ b/spring-cloud-azure-feature-management/src/main/java/com/microsoft/azure/spring/cloud/feature/manager/entities/FeatureFilterEvaluationContext.java @@ -13,10 +13,10 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class FeatureFilterEvaluationContext { - @JsonProperty("Name") + @JsonProperty("name") private String name; - @JsonProperty("Parameters") + @JsonProperty("parameters") private LinkedHashMap parameters; /** diff --git a/spring-cloud-azure-feature-management/src/test/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureManagerTest.java b/spring-cloud-azure-feature-management/src/test/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureManagerTest.java index 0fb16fd6c..da3570081 100644 --- a/spring-cloud-azure-feature-management/src/test/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureManagerTest.java +++ b/spring-cloud-azure-feature-management/src/test/java/com/microsoft/azure/spring/cloud/feature/manager/FeatureManagerTest.java @@ -9,21 +9,24 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Mockito.when; -import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.concurrent.ExecutionException; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; @@ -50,9 +53,16 @@ public class FeatureManagerTest { @Mock private ApplicationContext context; + @Mock + private FeatureManagementConfigProperties properties; + + @Rule + public ExpectedException expectedEx = ExpectedException.none(); + @Before public void setup() { MockitoAnnotations.initMocks(this); + when(properties.isFailFast()).thenReturn(true); } /** @@ -63,17 +73,17 @@ public void setup() { public void loadFeatureManagerWithLinkedHashSet() { Feature f = new Feature(); f.setKey(FEATURE_KEY); - + LinkedHashMap testMap = new LinkedHashMap(); LinkedHashMap testFeature = new LinkedHashMap(); LinkedHashMap enabledFor = new LinkedHashMap(); LinkedHashMap ffec = new LinkedHashMap(); LinkedHashMap parameters = new LinkedHashMap(); - ffec.put("Name", FILTER_NAME); + ffec.put("name", FILTER_NAME); parameters.put(PARAM_1_NAME, PARAM_1_VALUE); - ffec.put("Parameters", parameters); + ffec.put("parameters", parameters); enabledFor.put("0", ffec); - testFeature.put("EnabledFor", enabledFor); + testFeature.put("enabled-for", enabledFor); testMap.put(f.getKey(), testFeature); featureManager.putAll(testMap); @@ -91,12 +101,12 @@ public void loadFeatureManagerWithLinkedHashSet() { } @Test - public void isEnabledFeatureNotFound() throws InterruptedException, ExecutionException { + public void isEnabledFeatureNotFound() throws InterruptedException, ExecutionException, FilterNotFoundException { assertFalse(featureManager.isEnabledAsync("Non Existed Feature").block()); } @Test - public void isEnabledFeatureOff() throws InterruptedException, ExecutionException { + public void isEnabledFeatureOff() throws InterruptedException, ExecutionException, FilterNotFoundException { HashMap features = new HashMap(); features.put("Off", false); featureManager.putAll(features); @@ -105,11 +115,12 @@ public void isEnabledFeatureOff() throws InterruptedException, ExecutionExceptio } @Test - public void isEnabledFeatureHasNoFilters() throws InterruptedException, ExecutionException { + public void isEnabledFeatureHasNoFilters() + throws InterruptedException, ExecutionException, FilterNotFoundException { HashMap features = new HashMap(); Feature noFilters = new Feature(); noFilters.setKey("NoFilters"); - noFilters.setEnabledFor(new ArrayList()); + noFilters.setEnabledFor(new HashMap()); features.put("NoFilters", noFilters); featureManager.putAll(features); @@ -117,14 +128,15 @@ public void isEnabledFeatureHasNoFilters() throws InterruptedException, Executio } @Test - public void isEnabledON() throws InterruptedException, ExecutionException { + public void isEnabledON() throws InterruptedException, ExecutionException, FilterNotFoundException { HashMap features = new HashMap(); Feature onFeature = new Feature(); onFeature.setKey("On"); - ArrayList filters = new ArrayList(); + HashMap filters = + new HashMap(); FeatureFilterEvaluationContext alwaysOn = new FeatureFilterEvaluationContext(); alwaysOn.setName("AlwaysOn"); - filters.add(alwaysOn); + filters.put(0, alwaysOn); onFeature.setEnabledFor(filters); features.put("On", onFeature); featureManager.putAll(features); @@ -133,9 +145,36 @@ public void isEnabledON() throws InterruptedException, ExecutionException { assertTrue(featureManager.isEnabledAsync("On").block()); } + + @Test + public void isEnabledPeriodSplit() throws InterruptedException, ExecutionException, FilterNotFoundException { + LinkedHashMap features = new LinkedHashMap(); + LinkedHashMap featuresOn = new LinkedHashMap(); + + featuresOn.put("A", true); + features.put("Beta", featuresOn); + + featureManager.putAll(features); + + assertTrue(featureManager.isEnabledAsync("Beta.A").block()); + } + + @Test + public void isEnabledInvalid() throws InterruptedException, ExecutionException, FilterNotFoundException { + LinkedHashMap features = new LinkedHashMap(); + LinkedHashMap featuresOn = new LinkedHashMap(); + + featuresOn.put("A", 5); + features.put("Beta", featuresOn); + + featureManager.putAll(features); + + assertFalse(featureManager.isEnabledAsync("Beta.A").block()); + assertEquals(0, featureManager.size()); + } @Test - public void isEnabledOnBoolean() throws InterruptedException, ExecutionException { + public void isEnabledOnBoolean() throws InterruptedException, ExecutionException, FilterNotFoundException { HashMap features = new HashMap(); features.put("On", true); featureManager.putAll(features); @@ -144,7 +183,8 @@ public void isEnabledOnBoolean() throws InterruptedException, ExecutionException } @Test - public void featureManagerNotEnabledCorrectly() throws InterruptedException, ExecutionException { + public void featureManagerNotEnabledCorrectly() + throws InterruptedException, ExecutionException, FilterNotFoundException { FeatureManager featureManager = new FeatureManager(null); assertFalse(featureManager.isEnabledAsync("").block()); } @@ -165,7 +205,7 @@ public void bootstrapConfiguration() { enabledFor.setParameters(parameters); filterMapper.put(0, enabledFor); - featureV.setFilterMapper(filterMapper); + featureV.setEnabledFor(filterMapper); features.put("FeatureV", featureV); featureManager.putAll(features); @@ -179,6 +219,29 @@ public void bootstrapConfiguration() { assertEquals(ffec.getName(), "Random"); assertEquals(ffec.getParameters().size(), 1); assertEquals(ffec.getParameters().get("chance"), "50"); + assertEquals(2, featureManager.getAllFeatureNames().size()); + } + + @Test + public void noFilter() throws FilterNotFoundException { + expectedEx.expect(FilterNotFoundException.class); + expectedEx.expectMessage("Fail fast is set and a Filter was unable to be found: AlwaysOff"); + HashMap features = new HashMap(); + Feature onFeature = new Feature(); + onFeature.setKey("Off"); + HashMap filters = + new HashMap(); + FeatureFilterEvaluationContext alwaysOn = new FeatureFilterEvaluationContext(); + alwaysOn.setName("AlwaysOff"); + filters.put(0, alwaysOn); + onFeature.setEnabledFor(filters); + features.put("Off", onFeature); + featureManager.putAll(features); + + when(context.getBean(Mockito.matches("AlwaysOff"))).thenThrow(new NoSuchBeanDefinitionException("")); + + featureManager.isEnabledAsync("Off").block(); + fail(); } @Component diff --git a/spring-cloud-azure-feature-management/src/test/java/com/microsoft/azure/spring/cloud/feature/manager/feature/filters/PercentageFilterTest.java b/spring-cloud-azure-feature-management/src/test/java/com/microsoft/azure/spring/cloud/feature/manager/feature/filters/PercentageFilterTest.java new file mode 100644 index 000000000..3b6c3c49e --- /dev/null +++ b/spring-cloud-azure-feature-management/src/test/java/com/microsoft/azure/spring/cloud/feature/manager/feature/filters/PercentageFilterTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for + * license information. + */ +package com.microsoft.azure.spring.cloud.feature.manager.feature.filters; + +import static com.microsoft.azure.spring.cloud.feature.manager.FilterParameters.PERCENTAGE_FILTER_SETTING; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.LinkedHashMap; + +import org.junit.Test; + +import com.microsoft.azure.spring.cloud.feature.manager.entities.FeatureFilterEvaluationContext; + +public class PercentageFilterTest { + + @Test + public void zeroPercentage() { + PercentageFilter filter = new PercentageFilter(); + FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext(); + LinkedHashMap parameters = new LinkedHashMap(); + parameters.put(PERCENTAGE_FILTER_SETTING, "0"); + context.setParameters(parameters); + assertFalse(filter.evaluate(context)); + } + + @Test + public void hundredPercentage() { + PercentageFilter filter = new PercentageFilter(); + FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext(); + LinkedHashMap parameters = new LinkedHashMap(); + parameters.put(PERCENTAGE_FILTER_SETTING, "100"); + context.setParameters(parameters); + assertTrue(filter.evaluate(context)); + } + + @Test + public void errorPercentage() { + PercentageFilter filter = new PercentageFilter(); + FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext(); + LinkedHashMap parameters = new LinkedHashMap(); + parameters.put(PERCENTAGE_FILTER_SETTING, "-1"); + context.setParameters(parameters); + assertFalse(filter.evaluate(context)); + } + + @Test + public void nullPercentage() { + PercentageFilter filter = new PercentageFilter(); + FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext(); + LinkedHashMap parameters = new LinkedHashMap(); + context.setParameters(parameters); + assertFalse(filter.evaluate(context)); + } + +} diff --git a/spring-cloud-azure-feature-management/src/test/java/com/microsoft/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilterTest.java b/spring-cloud-azure-feature-management/src/test/java/com/microsoft/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilterTest.java new file mode 100644 index 000000000..74bda1c61 --- /dev/null +++ b/spring-cloud-azure-feature-management/src/test/java/com/microsoft/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilterTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for + * license information. + */ +package com.microsoft.azure.spring.cloud.feature.manager.feature.filters; + +import static com.microsoft.azure.spring.cloud.feature.manager.FilterParameters.TIME_WINDOW_FILTER_SETTING_END; +import static com.microsoft.azure.spring.cloud.feature.manager.FilterParameters.TIME_WINDOW_FILTER_SETTING_START; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.LinkedHashMap; + +import org.junit.Test; + +import com.microsoft.azure.spring.cloud.feature.manager.entities.FeatureFilterEvaluationContext; + +public class TimeWindowFilterTest { + + @Test + public void middleTest() { + TimeWindowFilter filter = new TimeWindowFilter(); + FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext(); + LinkedHashMap parameters = new LinkedHashMap(); + parameters.put(TIME_WINDOW_FILTER_SETTING_START, + ZonedDateTime.now().minusDays(1).format(DateTimeFormatter.RFC_1123_DATE_TIME)); + parameters.put(TIME_WINDOW_FILTER_SETTING_END, + ZonedDateTime.now().plusDays(1).format(DateTimeFormatter.RFC_1123_DATE_TIME)); + context.setParameters(parameters); + assertTrue(filter.evaluate(context)); + } + + @Test + public void beforeTest() { + TimeWindowFilter filter = new TimeWindowFilter(); + FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext(); + LinkedHashMap parameters = new LinkedHashMap(); + parameters.put(TIME_WINDOW_FILTER_SETTING_START, + ZonedDateTime.now().plusDays(1).format(DateTimeFormatter.RFC_1123_DATE_TIME)); + parameters.put(TIME_WINDOW_FILTER_SETTING_END, + ZonedDateTime.now().plusDays(2).format(DateTimeFormatter.RFC_1123_DATE_TIME)); + context.setParameters(parameters); + assertFalse(filter.evaluate(context)); + } + + @Test + public void afterTest() { + TimeWindowFilter filter = new TimeWindowFilter(); + FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext(); + LinkedHashMap parameters = new LinkedHashMap(); + parameters.put(TIME_WINDOW_FILTER_SETTING_START, + ZonedDateTime.now().minusDays(1).format(DateTimeFormatter.RFC_1123_DATE_TIME)); + parameters.put(TIME_WINDOW_FILTER_SETTING_END, + ZonedDateTime.now().minusDays(2).format(DateTimeFormatter.RFC_1123_DATE_TIME)); + context.setParameters(parameters); + assertFalse(filter.evaluate(context)); + } + + @Test + public void noStartTest() { + TimeWindowFilter filter = new TimeWindowFilter(); + FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext(); + LinkedHashMap parameters = new LinkedHashMap(); + parameters.put(TIME_WINDOW_FILTER_SETTING_END, + ZonedDateTime.now().plusDays(1).format(DateTimeFormatter.RFC_1123_DATE_TIME)); + context.setParameters(parameters); + assertTrue(filter.evaluate(context)); + } + + @Test + public void noEndTest() { + TimeWindowFilter filter = new TimeWindowFilter(); + FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext(); + LinkedHashMap parameters = new LinkedHashMap(); + parameters.put(TIME_WINDOW_FILTER_SETTING_START, + ZonedDateTime.now().minusDays(1).format(DateTimeFormatter.RFC_1123_DATE_TIME)); + context.setParameters(parameters); + assertTrue(filter.evaluate(context)); + } + + @Test + public void noInputsTest() { + TimeWindowFilter filter = new TimeWindowFilter(); + FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext(); + LinkedHashMap parameters = new LinkedHashMap(); + context.setParameters(parameters); + assertFalse(filter.evaluate(context)); + } + +} diff --git a/spring-cloud-azure-samples/azure-appconfiguration-sample/pom.xml b/spring-cloud-azure-samples/azure-appconfiguration-sample/pom.xml index 967d29434..46b27fd23 100644 --- a/spring-cloud-azure-samples/azure-appconfiguration-sample/pom.xml +++ b/spring-cloud-azure-samples/azure-appconfiguration-sample/pom.xml @@ -14,7 +14,7 @@ com.microsoft.azure - spring-cloud-starter-azure-appconfiguration-config + spring-cloud-azure-appconfiguration-config org.springframework.boot diff --git a/spring-cloud-azure-samples/feature-management-sample/src/main/java/com/example/ConsoleApplication.java b/spring-cloud-azure-samples/feature-management-sample/src/main/java/com/example/ConsoleApplication.java index 7af0d533d..494369dbc 100644 --- a/spring-cloud-azure-samples/feature-management-sample/src/main/java/com/example/ConsoleApplication.java +++ b/spring-cloud-azure-samples/feature-management-sample/src/main/java/com/example/ConsoleApplication.java @@ -19,7 +19,7 @@ @EnableAutoConfiguration public class ConsoleApplication implements CommandLineRunner { - private static Logger logger = LoggerFactory + private static final Logger LOGGER = LoggerFactory .getLogger(ConsoleApplication.class); @Autowired @@ -31,12 +31,12 @@ public static void main(String[] args) { @Override public void run(String... args) throws Exception { - logger.info("EXECUTING : command line runner"); + LOGGER.info("EXECUTING : command line runner"); - if (featureManager.isEnabledAsync("Beta").block()) { - System.out.println("Running Beta"); + if (featureManager.isEnabledAsync("beta").block()) { + LOGGER.info("RUNNING : beta"); } else { - System.out.println("Running Application"); + LOGGER.info("RUNNING : application"); } } diff --git a/spring-cloud-azure-samples/feature-management-sample/src/main/resources/application.yml b/spring-cloud-azure-samples/feature-management-sample/src/main/resources/application.yml index 82c47c006..508c0c25d 100644 --- a/spring-cloud-azure-samples/feature-management-sample/src/main/resources/application.yml +++ b/spring-cloud-azure-samples/feature-management-sample/src/main/resources/application.yml @@ -1,13 +1,13 @@ -FeatureManagement: - Beta: true - FeatureV: - EnabledFor: +feature-management: + beta: true + feature-v: + enabled-for: - - Name: percentageFilter - Parameters: - PercentageFilterSetting: "50" + name: percentageFilter + parameters: + percentage-filter-setting: "50" - - Name: timeWindowFilter - Parameters: - TimeWindowFilterSettingStart: "Fri, 6 Dec 2019 00:59:30 GMT" - TimeWindowFilterSettingEnd: "Sat, 2 May 2020 22:59:30 GMT" \ No newline at end of file + name: timeWindowFilter + parameters: + time-window-filter-setting-start: "Fri, 6 Dec 2019 00:59:30 GMT" + time-window-filter-setting-end: "Sat, 2 May 2020 22:59:30 GMT" \ No newline at end of file diff --git a/spring-cloud-azure-samples/feature-management-web-sample/src/main/java/com/example/HelloController.java b/spring-cloud-azure-samples/feature-management-web-sample/src/main/java/com/example/HelloController.java index 4169cdbfb..d5a2289bd 100644 --- a/spring-cloud-azure-samples/feature-management-web-sample/src/main/java/com/example/HelloController.java +++ b/spring-cloud-azure-samples/feature-management-web-sample/src/main/java/com/example/HelloController.java @@ -30,15 +30,15 @@ public class HelloController { @GetMapping("/privacy") public String getRequestBased(Model model) { - model.addAttribute("Beta", featureManager.isEnabledAsync("Beta").block()); - model.addAttribute("isDarkThemeS1", featureManagerSnapshot.isEnabledAsync("DarkTheme").block()); - model.addAttribute("isDarkThemeS2", featureManagerSnapshot.isEnabledAsync("DarkTheme").block()); - model.addAttribute("isDarkThemeS3", featureManagerSnapshot.isEnabledAsync("DarkTheme").block()); + model.addAttribute("Beta", featureManager.isEnabledAsync("beta").block()); + model.addAttribute("isDarkThemeS1", featureManagerSnapshot.isEnabledAsync("dark-theme").block()); + model.addAttribute("isDarkThemeS2", featureManagerSnapshot.isEnabledAsync("dark-theme").block()); + model.addAttribute("isDarkThemeS3", featureManagerSnapshot.isEnabledAsync("dark-theme").block()); return "privacy"; } @GetMapping(value = {"/Beta", "/BetaA"}) - @FeatureGate(feature = "BetaAB", fallback = "/BetaB") + @FeatureGate(feature = "beta-ab", fallback = "/BetaB") public String getRedirect(Model model) { return "BetaA"; } @@ -50,7 +50,7 @@ public String getRedirected(Model model) { @GetMapping(value = {"", "/", "/welcome"}) public String mainWithParam(Model model) { - model.addAttribute("Beta", featureManager.isEnabledAsync("Beta").block()); + model.addAttribute("Beta", featureManager.isEnabledAsync("beta").block()); return "welcome"; } } diff --git a/spring-cloud-azure-samples/feature-management-web-sample/src/main/resources/application.yml b/spring-cloud-azure-samples/feature-management-web-sample/src/main/resources/application.yml index 988c2f48a..bbb839129 100644 --- a/spring-cloud-azure-samples/feature-management-web-sample/src/main/resources/application.yml +++ b/spring-cloud-azure-samples/feature-management-web-sample/src/main/resources/application.yml @@ -9,21 +9,21 @@ spring: delay: 5s config: message: Hi -FeatureManagement: - Beta: true - DarkTheme: - EnabledFor: +feature-management:: + beta: true + dark-theme: + enabled-for: - - Name: Random - Parameters: + name: Random + parameters: chance: "50" - BetaAB: - EnabledFor: + beta-ab: + enabled-for: - - Name: Random - Parameters: + name: Random + parameters: chance: "50" - - Name: ClientFilter - Parameters: + name: ClientFilter + parameters: clientIp: 10.0.0.1 \ No newline at end of file diff --git a/spring-cloud-azure-samples/feature-management-web-sample/src/main/resources/bootstrap.yaml b/spring-cloud-azure-samples/feature-management-web-sample/src/main/resources/bootstrap.yaml deleted file mode 100644 index 86f8b07bf..000000000 --- a/spring-cloud-azure-samples/feature-management-web-sample/src/main/resources/bootstrap.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Use default application name and no profile configured -# Keys starting with /application/ will be matched -spring: - cloud: - azure: - appconfiguration: - stores: - - connection-string: ${CONFIG_STORE_CONNECTION_STRING} diff --git a/spring-cloud-azure-starters/spring-cloud-starter-azure-appconfiguration-config/README.md b/spring-cloud-azure-starters/spring-cloud-starter-azure-appconfiguration-config/README.md index cec2dea2a..5f9d52e36 100644 --- a/spring-cloud-azure-starters/spring-cloud-starter-azure-appconfiguration-config/README.md +++ b/spring-cloud-azure-starters/spring-cloud-starter-azure-appconfiguration-config/README.md @@ -1,4 +1,4 @@ -# Spring Cloud Azure Config +# Azure App Configuration for Spring Cloud This project allows Spring Application to load properties from Azure Configuration Store. @@ -8,22 +8,41 @@ Please use this [sample](../../spring-cloud-azure-samples/azure-appconfiguration ### Dependency Management +There are two libraries that can be used spring-cloud-azure-appconfiguration-config and spring-cloud-azure-appconfiguration-config-web. There are two differences between them the first being the web version takes on spring-web as a dependency, and the web version will attempt a refresh when the application is active when the cache expires. For more information on refresh see the [Configuration Refresh](#Configuration-Refresh) section. + #### Maven Coordinates ```xml com.microsoft.azure - spring-cloud-starter-azure-appconfiguration-config - {starter-version} + spring-cloud-azure-appconfiguration-config + {version} +``` + +or +```xml + + com.microsoft.azure + spring-cloud-azure-appconfiguration-config-web + {version} + ``` #### Gradle Coordinates ```gradle dependencies { - compile group: 'com.microsoft.azure', name: 'spring-cloud-starter-azure-appconfiguration-config', version: '{starter-version}' + compile group: 'com.microsoft.azure', name: 'spring-cloud-azure-appconfiguration-config', version: '{starter-version}' +} +``` + +or + +```gradle +dependencies { + compile group: 'com.microsoft.azure', name: 'spring-cloud-azure-appconfiguration-config-web', version: '{starter-version}' } ``` @@ -36,7 +55,6 @@ spring.cloud.azure.appconfiguration.enabled | Whether enable spring-cloud-azure- spring.cloud.azure.appconfiguration.default-context | Default context path to load properties from | No | application spring.cloud.azure.appconfiguration.name | Alternative to Spring application name, if not configured, fallback to default Spring application name | No | ${spring.application.name} spring.cloud.azure.appconfiguration.profile-separator | Profile separator for the key name, e.g., /foo-app_dev/db.connection.key, must follow format `^[a-zA-Z0-9_@]+$` | No | `_` -spring.cloud.azure.appconfiguration.fail-fast | Whether throw RuntimeException or not when exception occurs | No | true spring.cloud.azure.appconfiguration.cache-expiration | Amount of time, of type [Duration](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-conversion-duration), configurations are stored before a check can occur. | No | 30s spring.cloud.azure.appconfiguration.managed-identity.client-id | Client id of the user assigned managed identity, only required when choosing to use user assigned managed identity on Azure | No | null @@ -44,10 +62,11 @@ spring.cloud.azure.appconfiguration.managed-identity.client-id | Client id of th Name | Description | Required | Default ---|---|---|--- -spring.cloud.azure.appconfiguration.stores[0].endpoint | Endpoint of the configuration store, required when `connection-string` is empty. If `connection-string` is empty and application is deployed on Azure VM or App Service with managed identity enabled, will try to load `connection-string` from Azure Resource Management. | Conditional | null +spring.cloud.azure.appconfiguration.stores[0].endpoint | When the endpoint of an App Configuration store is specified, a managed identity or a token credential provided using `AppConfigCredentialProvider` will be used to connect to the App Configuration service. An `IllegalArgumentException` will be thrown if the endpoint and connection-string are specified at the same time. | Conditional | null spring.cloud.azure.appconfiguration.stores[0].prefix | The prefix of the key name in the configuration store, e.g., /my-prefix/application/key.name | No | null -spring.cloud.azure.appconfiguration.stores[0].connection-string | Required when `name` is empty, otherwise, can be loaded automatically on Azure Virtual Machine or App Service | Conditional | null +spring.cloud.azure.appconfiguration.stores[0].connection-string | When the connection-string of an App Configuration store is specified, HMAC authentication will be used to connect to the App Configuration service. An `IllegalArgumentException` will be thrown if the endpoint and connection-string are specified at the same time. | Conditional | null spring.cloud.azure.appconfiguration.stores[0].label | Comma separated list of label values, by default will query empty labeled value. If you want to specify *empty*(null) label explicitly, use `%00`, e.g., spring.cloud.azure.appconfiguration.stores[0].label=%00,v0 | No | null +spring.cloud.azure.appconfiguration.stores[0].fail-fast | Whether throw `RuntimeException` or not when fail to read App Configuration during application start-up. If an exception does occur during startup when set to false the store is skipped. | No | true spring.cloud.azure.appconfiguration.stores[0].watched-key | The single watched key(or by default *) used to indicate configuration change. | No | * ## Advanced usage @@ -76,6 +95,14 @@ spring.cloud.azure.appconfiguration.stores[0].label=[my-label1], [my-label2] Multiple labels can be separated with comma, if duplicate keys exists for multiple labels, the last label has highest priority. +### Spring Profiles + +Spring Profiles are supported by setting labels on your configurations that match your profile. Then set your label on your config store: + +```properties +spring.cloud.azure.appconfiguration.stores[0].label=${spring.profiles.active} +``` + ### Configuration Refresh Configuration Refresh feature allows the application to load the latest property value from configuration store automatically, without restarting the application. @@ -94,14 +121,25 @@ By default, all the keys in a configuration store will be watched. To prevent co spring.cloud.azure.appconfiguration.stores[0].watched-key=[my-watched-key] ``` -For web applications a refresh will be attempted whenever a ServletRequestHandledEvent occurs after the cache expiration time. Otherwise, calling refreshConfiguration on `AzureCloudConfigRefresh` will result in a refresh if the cache has expired. +When using the web library, applications will attempt a refresh whenever a servlet request occurs after the cache expiration time. + +In the console library calling refreshConfiguration on `AzureCloudConfigRefresh` will result in a refresh if the cache has expired. The web library can also use this method along with servlet request method. ### Failfast -Failfast feature decides whether throw RuntimeException or not when exception happens. By default, failfast is enabled, it can be disabled with below configuration: +Failfast feature decides whether throw RuntimeException or not when exception happens. If an exception does occur when false the store is skipped. Any store skipped on startup will be automatically skipped on Refresh. By default, failfast is enabled, it can be disabled with below configuration: ```properties -spring.cloud.azure.appconfiguration.fail-fast=false +spring.cloud.azure.appconfiguration.stores[0].fail-fast=false +``` + +### Placeholders in App Configuration + +The values in App Configuration are filtered through the existing Environment when they are used. Placeholders can be used just like in `application.properties`, but with the added benefit of support for key vault references. Example with kafka: + +```properties +/application/app.name=MyApp +/application/app.description=${app.name} is configured with Azure App Configuration ``` ### Use Managed Identity to access App Configuration @@ -120,20 +158,29 @@ Follow the below steps to enable accessing App Configuration with managed identi The configuration store endpoint must be configured when `connection-string` is empty. When using a User Assigned Id the value `spring.cloud.azure.appconfiguration.managed-identity.client-id=[client-id]` must be set. +#### bootstrap.application + +```application +spring.cloud.azure.appconfiguration.stores[0].endpoint=[config-store-endpoint] + +#If Using User Assigned Identity +spring.cloud.azure.appconfiguration.managed-identity.client-id=[client-id] +``` + ### Token Credential Provider -Another method of authentication is using AppConfigCredentialProvider and/or KeyVaultCredentialProvider. By implementing either of these classes and providing and generating a @Bean of them will enable authentication through any method defined by the [Java Azure SDK][azure_identity_sdk]. +Another method of authentication is using AppConfigCredentialProvider and/or KeyVaultCredentialProvider. By implementing either of these classes and providing and generating a @Bean of them will enable authentication through any method defined by the [Java Azure SDK][azure_identity_sdk]. The uri value is the endpoint/dns name of the connection service, so if needed different credentials can be used per config store/key vault. ```java public class MyCredentials implements AppConfigCredentialProvider, KeyVaultCredentialProvider { @Override - public TokenCredential credentialForAppConfig(String uri) { + public TokenCredential getAppConfigCredential(String uri) { return buildCredential(); } @Override - public TokenCredential credentialForKeyVault(String uri) { + public TokenCredential getKeyVaultCredential(String uri) { return buildCredential(); } @@ -144,17 +191,8 @@ public class MyCredentials implements AppConfigCredentialProvider, KeyVaultCrede } ``` -### bootstrap.application - -```application -spring.cloud.azure.appconfiguration.stores[0].endpoint=[config-store-endpoint] - -#If Using option 3 -spring.cloud.azure.appconfiguration.managed-identity.client-id=[client-id] -``` - [azure]: https://azure.microsoft.com [azure_active_directory]: https://azure.microsoft.com/services/active-directory/ [azure_identity_sdk]: https://github.com/Azure/azure-sdk-for-java/tree/master/sdk/identity/azure-identity -[azure_rbac]: https://docs.microsoft.com/azure/role-based-access-control/role-assignments-portal \ No newline at end of file +[azure_rbac]: https://docs.microsoft.com/azure/role-based-access-control/role-assignments-portal diff --git a/spring-cloud-azure-starters/spring-cloud-starter-azure-appconfiguration-config/pom.xml b/spring-cloud-azure-starters/spring-cloud-starter-azure-appconfiguration-config/pom.xml index 861032a50..41f7d7064 100644 --- a/spring-cloud-azure-starters/spring-cloud-starter-azure-appconfiguration-config/pom.xml +++ b/spring-cloud-azure-starters/spring-cloud-starter-azure-appconfiguration-config/pom.xml @@ -14,10 +14,6 @@ ${basedir}/../.. - - com.microsoft.azure - spring-cloud-azure-autoconfigure - com.microsoft.azure spring-cloud-azure-appconfiguration-config