diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index 9159c8a6afede..535c40a8b9378 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -445,5 +445,6 @@ unreleased_com.azure:azure-core-test;1.20.0-beta.1 beta_com.azure:azure-communication-common;1.3.0-beta.1 beta_com.azure:azure-core-http-netty;1.14.0-beta.1 beta_com.azure:azure-core;1.42.0-beta.1 +beta_com.azure:azure-data-appconfiguration;1.5.0-beta.1 diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/CHANGELOG.md b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/CHANGELOG.md index c33acfa86d4c1..c6ae3e2274edf 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/CHANGELOG.md @@ -1,14 +1,11 @@ # Release History -## 5.6.0-beta.1 (Unreleased) +## 5.6.0-beta.1 (2023-09-11) -### Features Added - -### Breaking Changes + ### Features Added -### Bugs Fixed - -### Other Changes + * Snapshot support using, `spring.cloud.azure.appconfiguration.stores[0].selects[0].snapshot-name`. + * Support for trimming prefixes from keys, default value is the key-filter when key-filter is used. `spring.cloud.azure.appconfiguration.stores[0].trim-key-prefix` ## 5.5.0 (2023-08-28) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md b/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md index b884a8a4f71e6..0d1658e04c17a 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md @@ -1,14 +1,11 @@ # Release History -## 5.6.0-beta.1 (Unreleased) +## 5.6.0-beta.1 (2023-09-11) -### Features Added - -### Breaking Changes + ### Features Added -### Bugs Fixed - -### Other Changes + * Snapshot support using, `spring.cloud.azure.appconfiguration.stores[0].selects[0].snapshot-name`. + * Support for trimming prefixes from keys, default value is the key-filter when key-filter is used. `spring.cloud.azure.appconfiguration.stores[0].trim-key-prefix` ## 5.5.0 (2023-08-28) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index ec008b37e476d..34ea77e3d8eb3 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -48,7 +48,7 @@ com.azure azure-data-appconfiguration - 1.4.8 + 1.5.0-beta.1 com.azure diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySource.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySource.java index 96a9f5fd5d801..87cc8bddc0733 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySource.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySource.java @@ -2,9 +2,12 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation; +import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_CONTENT_TYPE; + import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -17,6 +20,7 @@ import org.springframework.util.StringUtils; import com.azure.data.appconfiguration.models.ConfigurationSetting; +import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting; import com.azure.data.appconfiguration.models.SecretReferenceConfigurationSetting; import com.azure.data.appconfiguration.models.SettingSelector; import com.azure.security.keyvault.secrets.models.KeyVaultSecret; @@ -26,27 +30,28 @@ * Azure App Configuration PropertySource unique per Store Label(Profile) combo. * *

- * i.e. If connecting to 2 stores and have 2 labels set 4 - * AppConfigurationPropertySources need to be created. + * i.e. If connecting to 2 stores and have 2 labels set 4 AppConfigurationPropertySources need to be created. *

*/ -final class AppConfigurationApplicationSettingPropertySource extends AppConfigurationPropertySource { +class AppConfigurationApplicationSettingPropertySource extends AppConfigurationPropertySource { private static final Logger LOGGER = LoggerFactory - .getLogger(AppConfigurationApplicationSettingPropertySource.class); + .getLogger(AppConfigurationApplicationSettingPropertySource.class); private final AppConfigurationKeyVaultClientFactory keyVaultClientFactory; - private final int maxRetryTime; + private final String keyFilter; + + private final String[] labelFilters; - AppConfigurationApplicationSettingPropertySource(String originEndpoint, AppConfigurationReplicaClient replicaClient, - AppConfigurationKeyVaultClientFactory keyVaultClientFactory, String keyFilter, String[] labelFilter, - int maxRetryTime) { + AppConfigurationApplicationSettingPropertySource(String name, AppConfigurationReplicaClient replicaClient, + AppConfigurationKeyVaultClientFactory keyVaultClientFactory, String keyFilter, String[] labelFilters) { // The context alone does not uniquely define a PropertySource, append storeName // and label to uniquely define a PropertySource - super(originEndpoint, replicaClient, keyFilter, labelFilter); + super(name + getLabelName(labelFilters), replicaClient); this.keyVaultClientFactory = keyVaultClientFactory; - this.maxRetryTime = maxRetryTime; + this.keyFilter = keyFilter; + this.labelFilters = labelFilters; } /** @@ -54,74 +59,102 @@ final class AppConfigurationApplicationSettingPropertySource extends AppConfigur * Gets settings from Azure/Cache to set as configurations. Updates the cache. *

* + * @param keyPrefixTrimValues prefixs to trim from key values * @throws JsonProcessingException thrown if fails to parse Json content type */ - public void initProperties() throws JsonProcessingException { - List labels = Arrays.asList(labelFilter); + public void initProperties(List keyPrefixTrimValues) throws JsonProcessingException { + + List labels = Arrays.asList(labelFilters); + // Reverse labels so they have the right priority order. Collections.reverse(labels); for (String label : labels) { - SettingSelector settingSelector = new SettingSelector().setKeyFilter(keyFilter + "*") - .setLabelFilter(label); + SettingSelector settingSelector = new SettingSelector().setKeyFilter(keyFilter + "*").setLabelFilter(label); // * for wildcard match - List settings = replicaClient.listSettings(settingSelector); - - for (ConfigurationSetting setting : settings) { - String key = setting.getKey().trim().substring(keyFilter.length()) - .replace('/', '.'); - if (setting instanceof SecretReferenceConfigurationSetting) { - String entry = getKeyVaultEntry((SecretReferenceConfigurationSetting) setting); - - // Null in the case of failFast is false, will just skip entry. - if (entry != null) { - properties.put(key, entry); - } - } else if (StringUtils.hasText(setting.getContentType()) - && JsonConfigurationParser.isJsonContentType(setting.getContentType())) { - Map jsonSettings = JsonConfigurationParser.parseJsonSetting(setting); - for (Entry jsonSetting : jsonSettings.entrySet()) { - key = jsonSetting.getKey().trim().substring(keyFilter.length()); - properties.put(key, jsonSetting.getValue()); - } - } else { - properties.put(key, setting.getValue()); - } - } + processConfigurationSettings(replicaClient.listSettings(settingSelector), settingSelector.getKeyFilter(), + keyPrefixTrimValues); } } + protected void processConfigurationSettings(List settings, String keyFilter, + List keyPrefixTrimValues) + throws JsonProcessingException { + for (ConfigurationSetting setting : settings) { + if (keyPrefixTrimValues == null && StringUtils.hasText(keyFilter)) { + keyPrefixTrimValues = new ArrayList<>(); + keyPrefixTrimValues.add(keyFilter.substring(0, keyFilter.length() - 1)); + } + String key = trimKey(setting.getKey(), keyPrefixTrimValues); + + if (setting instanceof SecretReferenceConfigurationSetting) { + handleKeyVaultReference(key, (SecretReferenceConfigurationSetting) setting); + } else if (setting instanceof FeatureFlagConfigurationSetting + && FEATURE_FLAG_CONTENT_TYPE.equals(setting.getContentType())) { + handleFeatureFlag(key, (FeatureFlagConfigurationSetting) setting, keyPrefixTrimValues); + } else if (StringUtils.hasText(setting.getContentType()) + && JsonConfigurationParser.isJsonContentType(setting.getContentType())) { + handleJson(setting, keyPrefixTrimValues); + } else { + properties.put(key, setting.getValue()); + } + } + } + /** - * Given a Setting's Key Vault Reference stored in the Settings value, it will - * get its entry in Key Vault. + * Given a Setting's Key Vault Reference stored in the Settings value, it will get its entry in Key Vault. * - * @param secretReference {"uri": - * "<your-vault-url>/secret/<secret>/<version>"} + * @param key Application Setting name + * @param secretReference {"uri": "<your-vault-url>/secret/<secret>/<version>"} * @return Key Vault Secret Value */ - private String getKeyVaultEntry(SecretReferenceConfigurationSetting secretReference) { - String secretValue = null; + protected void handleKeyVaultReference(String key, SecretReferenceConfigurationSetting secretReference) { try { - URI uri = null; KeyVaultSecret secret = null; // Parsing Key Vault Reference for URI try { - uri = new URI(secretReference.getSecretId()); - secret = keyVaultClientFactory.getClient("https://" + uri.getHost()).getSecret(uri, maxRetryTime); + URI uri = new URI(secretReference.getSecretId()); + secret = keyVaultClientFactory.getClient("https://" + uri.getHost()).getSecret(uri); } catch (URISyntaxException e) { LOGGER.error("Error Processing Key Vault Entry URI."); ReflectionUtils.rethrowRuntimeException(e); } - + if (secret == null) { throw new IOException("No Key Vault Secret found for Reference."); } - secretValue = secret.getValue(); + properties.put(key, secret.getValue()); } catch (RuntimeException | IOException e) { LOGGER.error("Error Retrieving Key Vault Entry"); ReflectionUtils.rethrowRuntimeException(e); } - return secretValue; + } + + void handleFeatureFlag(String key, FeatureFlagConfigurationSetting setting, List trimStrings) + throws JsonProcessingException { + handleJson(setting, trimStrings); + } + + void handleJson(ConfigurationSetting setting, List keyPrefixTrimValues) + throws JsonProcessingException { + Map jsonSettings = JsonConfigurationParser.parseJsonSetting(setting); + for (Entry jsonSetting : jsonSettings.entrySet()) { + String key = trimKey(jsonSetting.getKey(), keyPrefixTrimValues); + properties.put(key, jsonSetting.getValue()); + } + } + + + protected String trimKey(String key, List trimStrings) { + key = key.trim(); + if (trimStrings != null) { + for (String trim : trimStrings) { + if (key.startsWith(trim)) { + return key.replaceFirst("^" + trim, "").replace('/', '.'); + } + } + } + return key.replace("/", "."); } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationFeatureManagementPropertySource.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationFeatureManagementPropertySource.java index c1fcd5315d6c9..f310c604ef3b1 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationFeatureManagementPropertySource.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationFeatureManagementPropertySource.java @@ -19,7 +19,6 @@ import static java.util.Collections.emptyList; import static java.util.stream.Collectors.toMap; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -48,24 +47,20 @@ * i.e. If connecting to 2 stores and have 2 labels set 4 AppConfigurationPropertySources need to be created. *

*/ -final class AppConfigurationFeatureManagementPropertySource extends AppConfigurationPropertySource { +class AppConfigurationFeatureManagementPropertySource extends AppConfigurationPropertySource { private static final ObjectMapper CASE_INSENSITIVE_MAPPER = JsonMapper.builder() .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build(); - private final List featureConfigurationSettings; + private final String keyFilter; + + private final String[] labelFilter; AppConfigurationFeatureManagementPropertySource(String originEndpoint, AppConfigurationReplicaClient replicaClient, String keyFilter, String[] labelFilter) { - super("FM_" + originEndpoint, replicaClient, keyFilter, labelFilter); - featureConfigurationSettings = new ArrayList<>(); - } - - private static List convertToListOrEmptyList(Map parameters, String key) { - List listObjects = CASE_INSENSITIVE_MAPPER.convertValue(parameters.get(key), - new TypeReference>() { - }); - return listObjects == null ? emptyList() : listObjects; + super("FM_" + originEndpoint + "/" + getLabelName(labelFilter), replicaClient); + this.keyFilter = keyFilter; + this.labelFilter = labelFilter; } /** @@ -80,7 +75,8 @@ private static List convertToListOrEmptyList(Map paramet *

* */ - public void initProperties() { + @Override + public void initProperties(List trim) { SettingSelector settingSelector = new SettingSelector(); String keyFilter = SELECT_ALL_FEATURE_FLAGS; @@ -98,30 +94,33 @@ public void initProperties() { settingSelector.setLabelFilter(label); List features = replicaClient.listSettings(settingSelector); - TracingInfo tracing = replicaClient.getTracingInfo(); // Reading In Features for (ConfigurationSetting setting : features) { if (setting instanceof FeatureFlagConfigurationSetting && FEATURE_FLAG_CONTENT_TYPE.equals(setting.getContentType())) { - featureConfigurationSettings.add(setting); - FeatureFlagConfigurationSetting featureFlag = (FeatureFlagConfigurationSetting) setting; - - String configName = FEATURE_MANAGEMENT_KEY - + setting.getKey().trim().substring(FEATURE_FLAG_PREFIX.length()); - - updateTelemetry(featureFlag, tracing); - - properties.put(configName, createFeature(featureFlag)); + processFeatureFlag(null, (FeatureFlagConfigurationSetting) setting, null); } } } } - + List getFeatureFlagSettings() { return featureConfigurationSettings; } + protected void processFeatureFlag(String key, FeatureFlagConfigurationSetting setting, List trimStrings) { + TracingInfo tracing = replicaClient.getTracingInfo(); + featureConfigurationSettings.add(setting); + FeatureFlagConfigurationSetting featureFlag = setting; + + String configName = FEATURE_MANAGEMENT_KEY + setting.getKey().trim().substring(FEATURE_FLAG_PREFIX.length()); + + updateTelemetry(featureFlag, tracing); + + properties.put(configName, createFeature(featureFlag)); + } + /** * Creates a {@code Feature} from a {@code KeyValueItem} * @@ -129,7 +128,7 @@ List getFeatureFlagSettings() { * @return Feature created from KeyValueItem */ @SuppressWarnings("unchecked") - private Object createFeature(FeatureFlagConfigurationSetting item) { + protected static Object createFeature(FeatureFlagConfigurationSetting item) { String key = getFeatureSimpleName(item); String requirementType = DEFAULT_REQUIREMENT_TYPE; try { @@ -182,31 +181,37 @@ private Object createFeature(FeatureFlagConfigurationSetting item) { } return feature; - } - + /** * Looks at each filter used in a Feature Flag to check what types it is using. * * @param featureFlag FeatureFlagConfigurationSetting * @param tracing The TracingInfo for this store. */ - private void updateTelemetry(FeatureFlagConfigurationSetting featureFlag, TracingInfo tracing) { + protected static void updateTelemetry(FeatureFlagConfigurationSetting featureFlag, TracingInfo tracing) { for (FeatureFlagFilter filter : featureFlag.getClientFilters()) { tracing.getFeatureFlagTracing().updateFeatureFilterTelemetry(filter.getName()); } } - private String getFeatureSimpleName(ConfigurationSetting setting) { + private static String getFeatureSimpleName(ConfigurationSetting setting) { return setting.getKey().trim().substring(FEATURE_FLAG_PREFIX.length()); } - - private Map mapValuesByIndex(List users) { + + @SuppressWarnings("null") + private static Map mapValuesByIndex(List users) { return IntStream.range(0, users.size()).boxed().collect(toMap(String::valueOf, users::get)); } - private void switchKeyValues(Map parameters, String oldKey, String newKey, Object value) { + private static void switchKeyValues(Map parameters, String oldKey, String newKey, Object value) { parameters.put(newKey, value); parameters.remove(oldKey); } + + private static List convertToListOrEmptyList(Map parameters, String key) { + List listObjects = + CASE_INSENSITIVE_MAPPER.convertValue(parameters.get(key), new TypeReference>() {}); + return listObjects == null ? emptyList() : listObjects; + } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationKeyVaultClientFactory.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationKeyVaultClientFactory.java index c1ebcbd34394e..2cc9fbffe363a 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationKeyVaultClientFactory.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationKeyVaultClientFactory.java @@ -23,16 +23,19 @@ public class AppConfigurationKeyVaultClientFactory { private final boolean credentialsConfigured; private final boolean isConfigured; + + private final int timeout; public AppConfigurationKeyVaultClientFactory(SecretClientCustomizer keyVaultClientProvider, KeyVaultSecretProvider keyVaultSecretProvider, SecretClientBuilderFactory secretClientFactory, - boolean credentialsConfigured) { + boolean credentialsConfigured, int timeout) { this.keyVaultClientProvider = keyVaultClientProvider; this.keyVaultSecretProvider = keyVaultSecretProvider; this.secretClientFactory = secretClientFactory; keyVaultClients = new HashMap<>(); this.credentialsConfigured = credentialsConfigured; isConfigured = keyVaultClientProvider != null || credentialsConfigured; + this.timeout = timeout; } public AppConfigurationSecretClientManager getClient(String host) { @@ -40,7 +43,7 @@ public AppConfigurationSecretClientManager getClient(String host) { // one if (!keyVaultClients.containsKey(host)) { AppConfigurationSecretClientManager client = new AppConfigurationSecretClientManager(host, - keyVaultClientProvider, keyVaultSecretProvider, secretClientFactory, credentialsConfigured); + keyVaultClientProvider, keyVaultSecretProvider, secretClientFactory, credentialsConfigured, timeout); keyVaultClients.put(host, client); } return keyVaultClients.get(host); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySource.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySource.java index f883f304aac82..ca1a4caad94fe 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySource.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySource.java @@ -2,40 +2,39 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; import org.springframework.core.env.EnumerablePropertySource; import com.azure.data.appconfiguration.ConfigurationClient; +import com.azure.data.appconfiguration.models.ConfigurationSetting; +import com.fasterxml.jackson.core.JsonProcessingException; /** * Azure App Configuration PropertySource unique per Store Label(Profile) combo. * *

- * i.e. If connecting to 2 stores and have 2 labels set 4 AppConfigurationPropertySources need to be created. + * i.e. If connecting to 2 stores and have 2 labels set 4 AppConfigurationPropertySources need to be + * created. *

*/ abstract class AppConfigurationPropertySource extends EnumerablePropertySource { - protected final String keyFilter; - - protected final String[] labelFilter; - protected final Map properties = new LinkedHashMap<>(); + protected final List featureConfigurationSettings = new ArrayList<>(); + protected final AppConfigurationReplicaClient replicaClient; - AppConfigurationPropertySource(String originEndpoint, AppConfigurationReplicaClient replicaClient, String keyFilter, - String[] labelFilter) { + AppConfigurationPropertySource(String name, AppConfigurationReplicaClient replicaClient) { // The context alone does not uniquely define a PropertySource, append storeName // and label to uniquely define a PropertySource - super( - keyFilter + originEndpoint + "/" + getLabelName(labelFilter)); + super(name); this.replicaClient = replicaClient; - this.keyFilter = keyFilter; - this.labelFilter = labelFilter; } @Override @@ -49,12 +48,12 @@ public Object getProperty(String name) { return properties.get(name); } - private static String getLabelName(String[] labelFilter) { - StringBuilder labelName = new StringBuilder(); - for (String label : labelFilter) { - - labelName.append((labelName.length() == 0) ? label : "," + label); + protected static String getLabelName(String[] labelFilters) { + if (labelFilters == null) { + return ""; } - return labelName.toString(); + return String.join(",", labelFilters); } + + protected abstract void initProperties(List trim) throws JsonProcessingException; } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceLocator.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceLocator.java index 870cc48fa3221..f17487acce07e 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceLocator.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceLocator.java @@ -19,6 +19,7 @@ import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.env.PropertySource; +import org.springframework.util.StringUtils; import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationKeyValueSelector; @@ -114,9 +115,8 @@ public PropertySource locate(Environment environment) { for (AppConfigurationReplicaClient client : clients) { sourceList = new ArrayList<>(); - if (!STARTUP.get() && reloadFailed - && !AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(client, clientFactory, - configStore.getFeatureFlags(), profiles)) { + if (!STARTUP.get() && reloadFailed && !AppConfigurationRefreshUtil + .checkStoreAfterRefreshFailed(client, clientFactory, configStore.getFeatureFlags(), profiles)) { // This store doesn't have any changes where to refresh store did. Skipping Checking next. continue; } @@ -196,9 +196,7 @@ private List getWatchKeys(AppConfigurationReplicaClient cl if (watchKey != null) { watchKeysSettings.add(watchKey); } else { - watchKeysSettings - .add(new ConfigurationSetting().setKey(trigger.getKey()) - .setLabel(trigger.getLabel())); + watchKeysSettings.add(new ConfigurationSetting().setKey(trigger.getKey()).setLabel(trigger.getLabel())); } } return watchKeysSettings; @@ -210,7 +208,8 @@ private List getFeatureFlagWatchKeys(ConfigStore configSto if (configStore.getFeatureFlags().getEnabled()) { for (AppConfigurationPropertySource propertySource : sources) { if (propertySource instanceof AppConfigurationFeatureManagementPropertySource) { - watchKeysFeatures.addAll(((AppConfigurationFeatureManagementPropertySource) propertySource).getFeatureFlagSettings()); + watchKeysFeatures.addAll( + ((AppConfigurationFeatureManagementPropertySource) propertySource).getFeatureFlagSettings()); } } } @@ -260,20 +259,29 @@ private List create(AppConfigurationReplicaClien if (store.getFeatureFlags().getEnabled()) { for (FeatureFlagKeyValueSelector selectedKeys : store.getFeatureFlags().getSelects()) { AppConfigurationFeatureManagementPropertySource propertySource = new AppConfigurationFeatureManagementPropertySource( - store.getEndpoint(), client, selectedKeys.getKeyFilter(), - selectedKeys.getLabelFilter(profiles)); + store.getEndpoint(), client, + selectedKeys.getKeyFilter(), selectedKeys.getLabelFilter(profiles)); - propertySource.initProperties(); + propertySource.initProperties(null); sourceList.add(propertySource); } } for (AppConfigurationKeyValueSelector selectedKeys : selects) { - AppConfigurationApplicationSettingPropertySource propertySource = new AppConfigurationApplicationSettingPropertySource( - store.getEndpoint(), client, keyVaultClientFactory, selectedKeys.getKeyFilter(), - selectedKeys.getLabelFilter(profiles), appProperties.getMaxRetryTime()); - propertySource.initProperties(); + AppConfigurationPropertySource propertySource = null; + + if (StringUtils.hasText(selectedKeys.getSnapshotName())) { + propertySource = new AppConfigurationSnapshotPropertySource( + selectedKeys.getSnapshotName() + "/" + store.getEndpoint(), client, keyVaultClientFactory, + selectedKeys.getSnapshotName()); + } else { + propertySource = new AppConfigurationApplicationSettingPropertySource( + selectedKeys.getKeyFilter() + store.getEndpoint() + "/", client, keyVaultClientFactory, + selectedKeys.getKeyFilter(), selectedKeys.getLabelFilter(profiles)); + } + propertySource.initProperties(store.getTrimKeyPrefix()); sourceList.add(propertySource); + } return sourceList; diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClient.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClient.java index c6532283f58a7..ae3c84abbcc8e 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClient.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClient.java @@ -2,6 +2,7 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation; +import java.io.UncheckedIOException; import java.time.Instant; import java.util.ArrayList; import java.util.List; @@ -11,7 +12,9 @@ import com.azure.core.exception.HttpResponseException; import com.azure.core.http.rest.PagedIterable; import com.azure.data.appconfiguration.ConfigurationClient; +import com.azure.data.appconfiguration.models.CompositionType; import com.azure.data.appconfiguration.models.ConfigurationSetting; +import com.azure.data.appconfiguration.models.ConfigurationSettingSnapshot; import com.azure.data.appconfiguration.models.SettingSelector; import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.TracingInfo; @@ -97,16 +100,8 @@ ConfigurationSetting getWatchKey(String key, String label) } } throw e; - } catch (Exception e) { // TODO (mametcal) This should be an UnknownHostException, but currently it isn't - // catchable. - if (e.getMessage().startsWith("java.net.UnknownHostException") - || e.getMessage().startsWith("java.net.WebSocketHandshakeException") - || e.getMessage().startsWith("java.net.SocketException") - || e.getMessage().startsWith("java.io.IOException") || e.getMessage() - .startsWith("io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused")) { - throw new AppConfigurationStatusException(e.getMessage(), null, null); - } - throw e; + } catch (UncheckedIOException e) { + throw new AppConfigurationStatusException(e.getMessage(), null, null); } } @@ -133,16 +128,34 @@ List listSettings(SettingSelector settingSelector) } } throw e; - } catch (Exception e) { // TODO (mametcal) This should be an UnknownHostException, but currently it isn't - // catchable. - if (e.getMessage().startsWith("java.net.UnknownHostException") - || e.getMessage().startsWith("java.net.WebSocketHandshakeException") - || e.getMessage().startsWith("java.net.SocketException") - || e.getMessage().startsWith("java.io.IOException") || e.getMessage() - .startsWith("io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused")) { - throw new AppConfigurationStatusException(e.getMessage(), null, null); + } catch (UncheckedIOException e) { + throw new AppConfigurationStatusException(e.getMessage(), null, null); + } + } + + List listSettingSnapshot(String snapshotName) { + List configurationSettings = new ArrayList<>(); + try { + ConfigurationSettingSnapshot snapshot = client.getSnapshot(snapshotName); + if (!CompositionType.KEY.equals(snapshot.getCompositionType())) { + throw new IllegalArgumentException("Snapshot " + snapshotName + " needs to be of type Key."); + } + + PagedIterable settings = client.listConfigurationSettingsForSnapshot(snapshotName); + this.failedAttempts = 0; + settings.forEach(setting -> configurationSettings.add(NormalizeNull.normalizeNullLabel(setting))); + return configurationSettings; + } catch (HttpResponseException e) { + if (e.getResponse() != null) { + int statusCode = e.getResponse().getStatusCode(); + + if (statusCode == 429 || statusCode == 408 || statusCode >= 500) { + throw new AppConfigurationStatusException(e.getMessage(), e.getResponse(), e.getValue()); + } } throw e; + } catch (UncheckedIOException e) { + throw new AppConfigurationStatusException(e.getMessage(), null, null); } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationSnapshotPropertySource.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationSnapshotPropertySource.java new file mode 100644 index 0000000000000..e0d6eae181b2f --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationSnapshotPropertySource.java @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.appconfiguration.config.implementation; + +import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_PREFIX; +import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_MANAGEMENT_KEY; + +import java.util.List; + +import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting; +import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.TracingInfo; +import com.fasterxml.jackson.core.JsonProcessingException; + +/** + * Azure App Configuration PropertySource unique per Store Label(Profile) combo. + * + *

+ * i.e. If connecting to 2 stores and have 2 labels set 4 AppConfigurationPropertySources need to be created. + *

+ */ +final class AppConfigurationSnapshotPropertySource extends AppConfigurationApplicationSettingPropertySource { + + private final String snapshotName; + + AppConfigurationSnapshotPropertySource(String name, AppConfigurationReplicaClient replicaClient, + AppConfigurationKeyVaultClientFactory keyVaultClientFactory, String snapshotName) { + // The context alone does not uniquely define a PropertySource, append storeName + // and label to uniquely define a PropertySource + // super(snapshotName + originEndpoint + "/", replicaClient, maxRetryTime); + super(name, replicaClient, keyVaultClientFactory, null, null); + this.snapshotName = snapshotName; + } + + /** + *

+ * Gets settings from Azure/Cache to set as configurations. Updates the cache. + *

+ * + * @param trim prefix to trim + * @throws JsonProcessingException thrown if fails to parse Json content type + */ + public void initProperties(List trim) throws JsonProcessingException { + processConfigurationSettings(replicaClient.listSettingSnapshot(snapshotName), null, trim); + } + + @Override + void handleFeatureFlag(String key, FeatureFlagConfigurationSetting setting, List trimStrings) + throws JsonProcessingException { + // Feature Flags are only part of this if they come from a snapshot + processFeatureFlag(key, setting, trimStrings); + } + + protected void processFeatureFlag(String key, FeatureFlagConfigurationSetting setting, List trimStrings) { + TracingInfo tracing = replicaClient.getTracingInfo(); + featureConfigurationSettings.add(setting); + FeatureFlagConfigurationSetting featureFlag = setting; + + String configName = FEATURE_MANAGEMENT_KEY + setting.getKey().trim().substring(FEATURE_FLAG_PREFIX.length()); + + AppConfigurationFeatureManagementPropertySource.updateTelemetry(featureFlag, tracing); + + properties.put(configName, AppConfigurationFeatureManagementPropertySource.createFeature(featureFlag)); + } +} diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/config/AppConfigurationBootstrapConfiguration.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/config/AppConfigurationBootstrapConfiguration.java index 826ee47a3bb84..b503d3080e7d3 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/config/AppConfigurationBootstrapConfiguration.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/config/AppConfigurationBootstrapConfiguration.java @@ -44,7 +44,7 @@ */ @Configuration @PropertySource("classpath:appConfiguration.properties") -@EnableConfigurationProperties({AppConfigurationProperties.class, AppConfigurationProviderProperties.class}) +@EnableConfigurationProperties({ AppConfigurationProperties.class, AppConfigurationProviderProperties.class }) @ConditionalOnClass(AppConfigurationPropertySourceLocator.class) @ConditionalOnProperty(prefix = AppConfigurationProperties.CONFIG_PREFIX, name = "enabled", matchIfMissing = true) public class AppConfigurationBootstrapConfiguration { @@ -55,38 +55,40 @@ public class AppConfigurationBootstrapConfiguration { @Bean AppConfigurationPropertySourceLocator sourceLocator(AppConfigurationProperties properties, AppConfigurationProviderProperties appProperties, AppConfigurationReplicaClientFactory clientFactory, - AppConfigurationKeyVaultClientFactory keyVaultClientFactory) throws IllegalArgumentException { + AppConfigurationKeyVaultClientFactory keyVaultClientFactory) + throws IllegalArgumentException { return new AppConfigurationPropertySourceLocator(appProperties, clientFactory, keyVaultClientFactory, properties.getRefreshInterval(), properties.getStores()); } @Bean - AppConfigurationKeyVaultClientFactory appConfigurationKeyVaultClientFactory(Environment environment) + AppConfigurationKeyVaultClientFactory appConfigurationKeyVaultClientFactory(Environment environment, AppConfigurationProviderProperties appProperties) throws IllegalArgumentException { - AzureGlobalProperties globalSource = - Binder.get(environment).bindOrCreate(AzureGlobalProperties.PREFIX, AzureGlobalProperties.class); - AzureGlobalProperties serviceSource = - Binder.get(environment).bindOrCreate(AzureKeyVaultSecretProperties.PREFIX, AzureGlobalProperties.class); + AzureGlobalProperties globalSource = Binder.get(environment).bindOrCreate(AzureGlobalProperties.PREFIX, + AzureGlobalProperties.class); + AzureGlobalProperties serviceSource = Binder.get(environment).bindOrCreate(AzureKeyVaultSecretProperties.PREFIX, + AzureGlobalProperties.class); - AzureKeyVaultSecretProperties globalProperties = - AzureGlobalPropertiesUtils.loadProperties(globalSource, new AzureKeyVaultSecretProperties()); - AzureKeyVaultSecretProperties clientProperties = - AzureGlobalPropertiesUtils.loadProperties(serviceSource, new AzureKeyVaultSecretProperties()); + AzureKeyVaultSecretProperties globalProperties = AzureGlobalPropertiesUtils.loadProperties( + globalSource, + new AzureKeyVaultSecretProperties()); + AzureKeyVaultSecretProperties clientProperties = AzureGlobalPropertiesUtils.loadProperties(serviceSource, + new AzureKeyVaultSecretProperties()); AzurePropertiesUtils.copyAzureCommonPropertiesIgnoreNull(globalProperties, clientProperties); - SecretClientCustomizer keyVaultClientProvider = - context.getBeanProvider(SecretClientCustomizer.class).getIfAvailable(); - KeyVaultSecretProvider keyVaultSecretProvider = - context.getBeanProvider(KeyVaultSecretProvider.class).getIfAvailable(); + SecretClientCustomizer keyVaultClientProvider = context.getBeanProvider(SecretClientCustomizer.class) + .getIfAvailable(); + KeyVaultSecretProvider keyVaultSecretProvider = context.getBeanProvider(KeyVaultSecretProvider.class) + .getIfAvailable(); SecretClientBuilderFactory secretClientBuilderFactory = new SecretClientBuilderFactory(clientProperties); boolean credentialConfigured = isCredentialConfigured(clientProperties); return new AppConfigurationKeyVaultClientFactory(keyVaultClientProvider, keyVaultSecretProvider, - secretClientBuilderFactory, credentialConfigured); + secretClientBuilderFactory, credentialConfigured, appProperties.getMaxRetryTime()); } /** @@ -117,15 +119,16 @@ AppConfigurationReplicaClientFactory buildClientFactory(AppConfigurationReplicaC AppConfigurationReplicaClientsBuilder replicaClientBuilder(Environment environment, AppConfigurationProviderProperties appProperties, AppConfigurationKeyVaultClientFactory keyVaultClientFactory, ObjectProvider> customizers) { - AzureGlobalProperties globalSource = - Binder.get(environment).bindOrCreate(AzureGlobalProperties.PREFIX, AzureGlobalProperties.class); - AzureGlobalProperties serviceSource = - Binder.get(environment).bindOrCreate(AzureAppConfigurationProperties.PREFIX, AzureGlobalProperties.class); + AzureGlobalProperties globalSource = Binder.get(environment).bindOrCreate(AzureGlobalProperties.PREFIX, + AzureGlobalProperties.class); + AzureGlobalProperties serviceSource = Binder.get(environment).bindOrCreate( + AzureAppConfigurationProperties.PREFIX, + AzureGlobalProperties.class); - AzureGlobalProperties globalProperties = - AzureGlobalPropertiesUtils.loadProperties(globalSource, new AzureGlobalProperties()); - AzureAppConfigurationProperties clientProperties = - AzureGlobalPropertiesUtils.loadProperties(serviceSource, new AzureAppConfigurationProperties()); + AzureGlobalProperties globalProperties = AzureGlobalPropertiesUtils.loadProperties(globalSource, + new AzureGlobalProperties()); + AzureAppConfigurationProperties clientProperties = AzureGlobalPropertiesUtils.loadProperties(serviceSource, + new AzureAppConfigurationProperties()); AzurePropertiesUtils.copyAzureCommonPropertiesIgnoreNull(globalProperties, clientProperties); @@ -139,7 +142,9 @@ AppConfigurationReplicaClientsBuilder replicaClientBuilder(Environment environme AppConfigurationReplicaClientsBuilder clientBuilder = new AppConfigurationReplicaClientsBuilder( appProperties.getMaxRetries(), clientFactory, credentialConfigured); - clientBuilder.setClientProvider(context.getBeanProvider(ConfigurationClientCustomizer.class).getIfAvailable()); + clientBuilder + .setClientProvider(context.getBeanProvider(ConfigurationClientCustomizer.class) + .getIfAvailable()); clientBuilder.setIsKeyVaultConfigured(keyVaultClientFactory.isConfigured()); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationKeyValueSelector.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationKeyValueSelector.java index 2192121008d77..b8b9f1ad86847 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationKeyValueSelector.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationKeyValueSelector.java @@ -24,7 +24,7 @@ public final class AppConfigurationKeyValueSelector { * Label for requesting all configurations with (No Label) */ private static final String[] EMPTY_LABEL_ARRAY = { EMPTY_LABEL }; - + private static final String APPLICATION_SETTING_DEFAULT_KEY_FILTER = "/application/"; /** @@ -37,6 +37,7 @@ public final class AppConfigurationKeyValueSelector { private String labelFilter; + private String snapshotName = ""; /** * @return the keyFilter */ @@ -61,14 +62,14 @@ public String[] getLabelFilter(List profiles) { if (labelFilter == null && profiles.size() > 0) { Collections.reverse(profiles); return profiles.toArray(new String[profiles.size()]); + } else if (StringUtils.hasText(snapshotName)) { + return new String[0]; } else if (!StringUtils.hasText(labelFilter)) { return EMPTY_LABEL_ARRAY; } // The use of trim makes label= dev,prod and label= dev, prod equal. - List labels = Arrays.stream(labelFilter.split(LABEL_SEPARATOR)) - .map(this::mapLabel) - .distinct() + List labels = Arrays.stream(labelFilter.split(LABEL_SEPARATOR)).map(this::mapLabel).distinct() .collect(Collectors.toList()); if (labelFilter.endsWith(",")) { @@ -89,6 +90,20 @@ public AppConfigurationKeyValueSelector setLabelFilter(String labelFilter) { return this; } + /** + * @return the snapshot + */ + public String getSnapshotName() { + return snapshotName; + } + + /** + * @param snapshot the snapshot to set + */ + public void setSnapshotName(String snapshotName) { + this.snapshotName = snapshotName; + } + /** * Validates key-filter and label-filter are valid. */ @@ -98,6 +113,10 @@ public void validateAndInit() { if (labelFilter != null) { Assert.isTrue(!labelFilter.contains("*"), "LabelFilter must not contain asterisk(*)"); } + Assert.isTrue(!(StringUtils.hasText(keyFilter) && StringUtils.hasText(snapshotName)), + "Snapshots can't use key filters"); + Assert.isTrue(!(StringUtils.hasText(labelFilter) && StringUtils.hasText(snapshotName)), + "Snapshots can't use label filters"); } private String mapLabel(String label) { diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/ConfigStore.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/ConfigStore.java index 0e50436064307..17ac625ba1558 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/ConfigStore.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/ConfigStore.java @@ -40,6 +40,8 @@ public final class ConfigStore { private AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); + private List trimKeyPrefix; + /** * @return the endpoint */ @@ -173,6 +175,21 @@ public boolean containsEndpoint(String endpoint) { return endpoints.stream().anyMatch(storeEndpoint -> storeEndpoint.startsWith(endpoint)); } + /** + * @return the trimKeyPrefix + */ + public List getTrimKeyPrefix() { + return trimKeyPrefix; + } + + /** + * @param trimKeyPrefix the values to be trimmed from key names before being set to + * `@ConfigurationProperties` + */ + public void setTrimKeyPrefix(List trimKeyPrefix) { + this.trimKeyPrefix = trimKeyPrefix; + } + /** * @throws IllegalStateException Connection String URL endpoint is invalid */ @@ -190,9 +207,9 @@ public void validateAndInit() { String endpoint = (AppConfigurationReplicaClientsBuilder.getEndpointFromConnectionString(connectionString)); try { // new URI is used to validate the endpoint as a valid URI - new URI(endpoint).toURL(); + new URI(endpoint); this.endpoint = endpoint; - } catch (MalformedURLException | URISyntaxException | IllegalArgumentException e) { + } catch (URISyntaxException e) { throw new IllegalStateException("Endpoint in connection string is not a valid URI.", e); } } else if (connectionStrings.size() > 0) { diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/stores/AppConfigurationSecretClientManager.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/stores/AppConfigurationSecretClientManager.java index 606d997d1fcd8..2c086d3c5dab3 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/stores/AppConfigurationSecretClientManager.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/stores/AppConfigurationSecretClientManager.java @@ -29,8 +29,10 @@ public final class AppConfigurationSecretClientManager { private final KeyVaultSecretProvider keyVaultSecretProvider; private final SecretClientBuilderFactory secretClientFactory; - + private final boolean credentialConfigured; + + private final int timeout; /** * Creates a Client for connecting to Key Vault @@ -39,14 +41,17 @@ public final class AppConfigurationSecretClientManager { * @param keyVaultSecretProvider optional provider for providing Secrets instead of connecting to Key Vault * @param secretClientFactory Factory for building clients to Key Vault * @param credentialConfigured Is a credential configured with Global Configurations or Service Configurations + * @param timeout How long the connection to key vault is kept open without a response. */ public AppConfigurationSecretClientManager(String endpoint, SecretClientCustomizer keyVaultClientProvider, - KeyVaultSecretProvider keyVaultSecretProvider, SecretClientBuilderFactory secretClientFactory, boolean credentialConfigured) { + KeyVaultSecretProvider keyVaultSecretProvider, SecretClientBuilderFactory secretClientFactory, + boolean credentialConfigured, int timeout) { this.endpoint = endpoint; this.keyVaultClientProvider = keyVaultClientProvider; this.keyVaultSecretProvider = keyVaultSecretProvider; this.secretClientFactory = secretClientFactory; this.credentialConfigured = credentialConfigured; + this.timeout = timeout; } AppConfigurationSecretClientManager build() { @@ -74,7 +79,7 @@ AppConfigurationSecretClientManager build() { * @param timeout How long it waits for a response from Key Vault * @return Secret values that matches the secretIdentifier */ - public KeyVaultSecret getSecret(URI secretIdentifier, int timeout) { + public KeyVaultSecret getSecret(URI secretIdentifier) { if (secretClient == null) { build(); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceTest.java index b59a2c0ff7e04..975d694443e2e 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceTest.java @@ -91,7 +91,7 @@ public void init() { String[] labelFilter = { "\0" }; propertySource = new AppConfigurationApplicationSettingPropertySource(TEST_STORE_NAME, clientMock, - keyVaultClientFactoryMock, KEY_FILTER, labelFilter, 60); + keyVaultClientFactoryMock, KEY_FILTER, labelFilter); } @AfterEach @@ -105,7 +105,7 @@ public void testPropCanBeInitAndQueried() throws IOException { when(clientMock.listSettings(Mockito.any())).thenReturn(configurationListMock) .thenReturn(configurationListMock); - propertySource.initProperties(); + propertySource.initProperties(null); String[] keyNames = propertySource.getPropertyNames(); String[] expectedKeyNames = testItems.stream() @@ -129,7 +129,7 @@ public void testPropertyNameSlashConvertedToDots() throws IOException { when(clientMock.listSettings(Mockito.any())).thenReturn(configurationListMock) .thenReturn(configurationListMock); - propertySource.initProperties(); + propertySource.initProperties(null); String expectedKeyName = TEST_SLASH_KEY.replace('/', '.'); String[] actualKeyNames = propertySource.getPropertyNames(); @@ -148,7 +148,7 @@ public void initNullValidContentTypeTest() throws IOException { .thenReturn(Collections.emptyIterator()); when(clientMock.listSettings(Mockito.any())).thenReturn(configurationListMock); - propertySource.initProperties(); + propertySource.initProperties(null); String[] keyNames = propertySource.getPropertyNames(); String[] expectedKeyNames = items.stream() diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationFeatureManagementPropertySourceTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationFeatureManagementPropertySourceTest.java index 8ae9ed99071ad..755e5a8c0725b 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationFeatureManagementPropertySourceTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationFeatureManagementPropertySourceTest.java @@ -121,7 +121,7 @@ public void init() { public void cleanup() throws Exception { MockitoAnnotations.openMocks(this).close(); } - + @Test public void overrideTest() { String[] labels = {"test"}; @@ -133,7 +133,7 @@ public void overrideTest() { when(clientMock.getTracingInfo()).thenReturn(new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); featureFlagStore.setEnabled(true); - propertySourceOverride.initProperties(); + propertySourceOverride.initProperties(null); Map filters = new HashMap<>(); FeatureFlagFilter ffec = new FeatureFlagFilter("TestFilter"); @@ -142,7 +142,7 @@ public void overrideTest() { gamma.setKey("Gamma"); filters = new HashMap<>(); ffec = new FeatureFlagFilter("TestFilter"); - LinkedHashMap parameters = new LinkedHashMap<>(); + Map parameters = new LinkedHashMap<>(); parameters.put("key", "value"); ffec.setParameters(parameters); filters.put(0, ffec); @@ -151,7 +151,7 @@ public void overrideTest() { assertEquals(gamma.getKey(), ((Feature) propertySourceOverride.getProperty(FEATURE_MANAGEMENT_KEY + "Gamma")).getKey()); } - + @Test public void testFeatureFlagCanBeInitedAndQueried() { when(featureListMock.iterator()).thenReturn(FEATURE_ITEMS.iterator()); @@ -160,9 +160,9 @@ public void testFeatureFlagCanBeInitedAndQueried() { when(clientMock.getTracingInfo()).thenReturn(new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); featureFlagStore.setEnabled(true); - propertySource.initProperties(); + propertySource.initProperties(null); - Map filters = new HashMap<>(); + HashMap filters = new HashMap<>(); FeatureFlagFilter ffec = new FeatureFlagFilter("TestFilter"); filters.put(0, ffec); Feature gamma = new Feature(); @@ -185,7 +185,7 @@ public void testFeatureFlagThrowError() { when(clientMock.listSettings(Mockito.any())).thenReturn(featureListMock); when(clientMock.getTracingInfo()).thenReturn(new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); try { - propertySource.initProperties(); + propertySource.initProperties(null); } catch (Exception e) { assertEquals("Found Feature Flag /foo/test_key_1 with invalid Content Type of ", e.getMessage()); } @@ -200,7 +200,7 @@ public void initNullInvalidContentTypeFeatureFlagTest() { when(clientMock.listSettings(Mockito.any())) .thenReturn(featureListMock).thenReturn(featureListMock); - propertySource.initProperties(); + propertySource.initProperties(null); String[] keyNames = propertySource.getPropertyNames(); String[] expectedKeyNames = {}; @@ -216,7 +216,7 @@ public void testFeatureFlagTargeting() { when(clientMock.getTracingInfo()).thenReturn(new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); featureFlagStore.setEnabled(true); - propertySource.initProperties(); + propertySource.initProperties(null); FeatureSet featureSetExpected = new FeatureSet(); Feature feature = new Feature(); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java index e7ec5fb3e3380..f37d941a78505 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java @@ -97,7 +97,7 @@ public void init() { String[] labelFilter = { "\0" }; propertySource = new AppConfigurationApplicationSettingPropertySource(TEST_STORE_NAME, replicaClientMock, - keyVaultClientFactory, KEY_FILTER, labelFilter, 60); + keyVaultClientFactory, KEY_FILTER, labelFilter); TEST_ITEMS.add(ITEM_1); TEST_ITEMS.add(ITEM_2); @@ -121,10 +121,10 @@ public void testKeyVaultTest() { KeyVaultSecret secret = new KeyVaultSecret("mySecret", "mySecretValue"); when(keyVaultClientFactory.getClient(Mockito.eq("https://test.key.vault.com"))).thenReturn(clientManagerMock); - when(clientManagerMock.getSecret(Mockito.any(URI.class), Mockito.anyInt())).thenReturn(secret); + when(clientManagerMock.getSecret(Mockito.any(URI.class))).thenReturn(secret); try { - propertySource.initProperties(); + propertySource.initProperties(null); } catch (IOException e) { fail("Failed Reading in Feature Flags"); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java index 247c2db84a7f8..f554ab41f7bb5 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java @@ -5,8 +5,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.io.UncheckedIOException; +import java.net.UnknownHostException; import java.time.Instant; import java.util.ArrayList; import java.util.List; @@ -22,7 +27,9 @@ import com.azure.core.http.rest.PagedIterable; import com.azure.core.util.Configuration; import com.azure.data.appconfiguration.ConfigurationClient; +import com.azure.data.appconfiguration.models.CompositionType; import com.azure.data.appconfiguration.models.ConfigurationSetting; +import com.azure.data.appconfiguration.models.ConfigurationSettingSnapshot; import com.azure.data.appconfiguration.models.SettingSelector; import com.azure.identity.CredentialUnavailableException; import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.TracingInfo; @@ -73,6 +80,9 @@ public void getWatchKeyTest() { when(responseMock.getStatusCode()).thenReturn(499); assertThrows(HttpResponseException.class, () -> client.getWatchKey("watch", "\0")); + + when(clientMock.getConfigurationSetting(Mockito.any(), Mockito.any())).thenThrow(new UncheckedIOException(new UnknownHostException())); + assertThrows(AppConfigurationStatusException.class, () -> client.getWatchKey("watch", "\0")); } @Test @@ -102,6 +112,15 @@ public void listSettingsTest() { assertThrows(HttpResponseException.class, () -> client.listSettings(new SettingSelector())); } + @Test + public void listSettingsUnknownHostTest() { + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock, + new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); + + when(clientMock.listConfigurationSettings(Mockito.any())).thenThrow(new UncheckedIOException(new UnknownHostException())); + assertThrows(AppConfigurationStatusException.class, () -> client.listSettings(new SettingSelector())); + } + @Test public void listSettingsNoCredentialTest() { AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock, @@ -158,4 +177,65 @@ public void backoffTest() { assertEquals(0, client.getFailedAttempts()); } + @Test + public void listSettingSnapshotTest() { + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock, + new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); + + List configurations = new ArrayList<>(); + ConfigurationSettingSnapshot snapshot = new ConfigurationSettingSnapshot(null); + snapshot.setCompositionType(CompositionType.KEY); + + when(clientMock.getSnapshot(Mockito.any())).thenReturn(snapshot); + when(clientMock.listConfigurationSettingsForSnapshot(Mockito.any())).thenReturn(settingsMock); + when(settingsMock.iterator()).thenReturn(configurations.iterator()); + + assertEquals(configurations, client.listSettingSnapshot("SnapshotName")); + + when(clientMock.listConfigurationSettingsForSnapshot(Mockito.any())).thenThrow(exceptionMock); + when(exceptionMock.getResponse()).thenReturn(responseMock); + when(responseMock.getStatusCode()).thenReturn(429); + assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName")); + + when(responseMock.getStatusCode()).thenReturn(408); + assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName")); + + when(responseMock.getStatusCode()).thenReturn(500); + assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName")); + + when(responseMock.getStatusCode()).thenReturn(499); + assertThrows(HttpResponseException.class, () -> client.listSettingSnapshot("SnapshotName")); + + when(clientMock.getSnapshot(Mockito.any())).thenThrow(new UncheckedIOException(new UnknownHostException())); + assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName")); + } + + @Test + public void listSettingSnapshotInvalidCompositionTypeTest() { + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock, + new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); + + ConfigurationSettingSnapshot snapshot = new ConfigurationSettingSnapshot(null); + snapshot.setCompositionType(CompositionType.KEY_LABEL); + + when(clientMock.getSnapshot(Mockito.any())).thenReturn(snapshot); + + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> client.listSettingSnapshot("SnapshotName")); + assertEquals("Snapshot SnapshotName needs to be of type Key.", e.getMessage()); + } + + @Test + public void updateSyncTokenTest() { + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock, + new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); + String fakeToken = "fake_sync_token"; + + client.updateSyncToken(fakeToken); + verify(clientMock, times(1)).updateSyncToken(Mockito.eq(fakeToken)); + reset(clientMock); + + client.updateSyncToken(null); + verify(clientMock, times(0)).updateSyncToken(Mockito.eq(fakeToken)); + } + } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationSnapshotPropertySourceTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationSnapshotPropertySourceTest.java new file mode 100644 index 0000000000000..5c810b11b8c0a --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationSnapshotPropertySourceTest.java @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.appconfiguration.config.implementation; + +import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_CONTENT_TYPE; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.FEATURE_LABEL; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.FEATURE_VALUE; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_CONN_STRING; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_KEY_1; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_KEY_2; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_KEY_3; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_LABEL_1; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_LABEL_2; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_LABEL_3; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_SLASH_KEY; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_SLASH_VALUE; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_STORE_NAME; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_VALUE_1; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_VALUE_2; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_VALUE_3; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestUtils.createItem; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestUtils.createItemFeatureFlag; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import com.azure.core.util.Configuration; +import com.azure.data.appconfiguration.models.ConfigurationSetting; +import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting; +import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.TracingInfo; +import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProperties; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; + +public class AppConfigurationSnapshotPropertySourceTest { + + private static final String EMPTY_CONTENT_TYPE = ""; + + private static final AppConfigurationProperties TEST_PROPS = new AppConfigurationProperties(); + + private static final String KEY_FILTER = "/foo/"; + + private static final List TRIM = new ArrayList<>(); + + private static final String SNAPSHOT_NAME = "snapshot_test"; + + private static final ConfigurationSetting ITEM_1 = + createItem(KEY_FILTER, TEST_KEY_1, TEST_VALUE_1, TEST_LABEL_1, EMPTY_CONTENT_TYPE); + + private static final ConfigurationSetting ITEM_2 = + createItem(KEY_FILTER, TEST_KEY_2, TEST_VALUE_2, TEST_LABEL_2, EMPTY_CONTENT_TYPE); + + private static final ConfigurationSetting ITEM_3 = + createItem(KEY_FILTER, TEST_KEY_3, TEST_VALUE_3, TEST_LABEL_3, EMPTY_CONTENT_TYPE); + + private static final ConfigurationSetting ITEM_4 = + createItem("/bar/", "test_key_4", "test_value_4", "test_label_4", EMPTY_CONTENT_TYPE); + + private static final FeatureFlagConfigurationSetting FEATURE_ITEM = createItemFeatureFlag(".appconfig.featureflag/", + "Alpha", FEATURE_VALUE, FEATURE_LABEL, FEATURE_FLAG_CONTENT_TYPE); + + private static final ConfigurationSetting ITEM_NULL = + createItem(KEY_FILTER, TEST_KEY_3, TEST_VALUE_3, TEST_LABEL_3, null); + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private List testItems = new ArrayList<>(); + + private AppConfigurationSnapshotPropertySource propertySource; + + @Mock + private AppConfigurationReplicaClient clientMock; + + @Mock + private AppConfigurationKeyVaultClientFactory keyVaultClientFactoryMock; + + @Mock + private List configurationListMock; + + @BeforeAll + public static void setup() { + TestUtils.addStore(TEST_PROPS, TEST_STORE_NAME, TEST_CONN_STRING, KEY_FILTER); + } + + @BeforeEach + public void init() { + MAPPER.setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE); + + MockitoAnnotations.openMocks(this); + + testItems = new ArrayList<>(); + testItems.add(ITEM_1); + testItems.add(ITEM_2); + testItems.add(ITEM_3); + testItems.add(ITEM_4); + testItems.add(FEATURE_ITEM); + + TRIM.add(KEY_FILTER); + + propertySource = new AppConfigurationSnapshotPropertySource(TEST_STORE_NAME, clientMock, + keyVaultClientFactoryMock, SNAPSHOT_NAME); + } + + @AfterEach + public void cleanup() throws Exception { + MockitoAnnotations.openMocks(this).close(); + } + + @Test + public void testPropCanBeInitAndQueried() throws IOException { + when(configurationListMock.iterator()).thenReturn(testItems.iterator()); + when(clientMock.listSettingSnapshot(Mockito.any())).thenReturn(configurationListMock) + .thenReturn(configurationListMock); + when(clientMock.getTracingInfo()) + .thenReturn(new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); + + propertySource.initProperties(TRIM); + + String[] keyNames = propertySource.getPropertyNames(); + String[] expectedKeyNames = testItems.stream().map(t -> { + if (t.getKey().startsWith(".appconfig.featureflag/")) { + return t.getKey().replace(".appconfig.featureflag/", "feature-management."); + } + return t.getKey().replaceFirst("^" + KEY_FILTER, "").replace("/", "."); + + }).toArray(String[]::new); + + assertThat(keyNames).containsExactlyInAnyOrder(expectedKeyNames); + + assertThat(propertySource.getProperty(TEST_KEY_1)).isEqualTo(TEST_VALUE_1); + assertThat(propertySource.getProperty(TEST_KEY_2)).isEqualTo(TEST_VALUE_2); + assertThat(propertySource.getProperty(TEST_KEY_3)).isEqualTo(TEST_VALUE_3); + assertThat(propertySource.getProperty(".bar.test_key_4")).isEqualTo("test_value_4"); + } + + @Test + public void testPropertyNameSlashConvertedToDots() throws IOException { + ConfigurationSetting slashedProp = + createItem(KEY_FILTER, TEST_SLASH_KEY, TEST_SLASH_VALUE, null, EMPTY_CONTENT_TYPE); + List settings = new ArrayList<>(); + settings.add(slashedProp); + when(configurationListMock.iterator()).thenReturn(settings.iterator()).thenReturn(Collections.emptyIterator()); + when(clientMock.listSettingSnapshot(Mockito.any())).thenReturn(configurationListMock) + .thenReturn(configurationListMock); + + propertySource.initProperties(TRIM); + + String expectedKeyName = TEST_SLASH_KEY.replace('/', '.'); + String[] actualKeyNames = propertySource.getPropertyNames(); + + assertThat(actualKeyNames.length).isEqualTo(1); + assertThat(actualKeyNames[0]).isEqualTo(expectedKeyName); + assertThat(propertySource.getProperty(TEST_SLASH_KEY)).isNull(); + assertThat(propertySource.getProperty(expectedKeyName)).isEqualTo(TEST_SLASH_VALUE); + } + + @Test + public void initNullValidContentTypeTest() throws IOException { + List items = new ArrayList<>(); + items.add(ITEM_NULL); + when(configurationListMock.iterator()).thenReturn(items.iterator()).thenReturn(Collections.emptyIterator()); + when(clientMock.listSettingSnapshot(Mockito.any())).thenReturn(configurationListMock); + + propertySource.initProperties(TRIM); + + String[] keyNames = propertySource.getPropertyNames(); + String[] expectedKeyNames = + items.stream().map(t -> t.getKey().substring(KEY_FILTER.length())).toArray(String[]::new); + + assertThat(keyNames).containsExactlyInAnyOrder(expectedKeyNames); + } +} diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/stores/KeyVaultClientTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/stores/KeyVaultClientTest.java index 05fd95dc3e2b4..98f2c3ec9bc82 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/stores/KeyVaultClientTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/stores/KeyVaultClientTest.java @@ -59,7 +59,7 @@ public void configProviderAuth() throws URISyntaxException { String keyVaultUri = "https://keyvault.vault.azure.net"; clientStore = new AppConfigurationSecretClientManager(keyVaultUri, null, null, secretClientBuilderFactoryMock, - false); + false, 60); AppConfigurationSecretClientManager test = Mockito.spy(clientStore); when(secretClientBuilderFactoryMock.build()).thenReturn(builderMock); @@ -73,8 +73,8 @@ public void configProviderAuth() throws URISyntaxException { .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(), ""); + assertNotNull(test.getSecret(new URI(keyVaultUri))); + assertEquals(test.getSecret(new URI(keyVaultUri)).getName(), ""); } @Test @@ -82,7 +82,7 @@ public void systemAssignedCredentials() throws URISyntaxException { String keyVaultUri = "https://keyvault.vault.azure.net/secrets/mySecret"; clientStore = new AppConfigurationSecretClientManager(keyVaultUri, null, null, secretClientBuilderFactoryMock, - false); + false, 60); AppConfigurationSecretClientManager test = Mockito.spy(clientStore); when(secretClientBuilderFactoryMock.build()).thenReturn(builderMock); @@ -96,8 +96,8 @@ public void systemAssignedCredentials() throws URISyntaxException { .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(), ""); + assertNotNull(test.getSecret(new URI(keyVaultUri))); + assertEquals(test.getSecret(new URI(keyVaultUri)).getName(), ""); } @Test @@ -105,15 +105,15 @@ public void secretResolverTest() throws URISyntaxException { String keyVaultUri = "https://keyvault.vault.azure.net/secrets/mySecret"; clientStore = new AppConfigurationSecretClientManager(keyVaultUri, null, new TestSecretResolver(), - secretClientBuilderFactoryMock, false); + secretClientBuilderFactoryMock, false, 60); AppConfigurationSecretClientManager test = Mockito.spy(clientStore); when(secretClientBuilderFactoryMock.build()).thenReturn(builderMock); when(builderMock.vaultUrl(Mockito.any())).thenReturn(builderMock); - assertEquals("Test-Value", test.getSecret(new URI(keyVaultUri + "/testSecret"), 10).getValue()); - assertEquals("Default-Secret", test.getSecret(new URI(keyVaultUri + "/testSecret2"), 10).getValue()); + assertEquals("Test-Value", test.getSecret(new URI(keyVaultUri + "/testSecret")).getValue()); + assertEquals("Default-Secret", test.getSecret(new URI(keyVaultUri + "/testSecret2")).getValue()); } class TestSecretResolver implements KeyVaultSecretProvider { diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/CHANGELOG.md b/sdk/spring/spring-cloud-azure-feature-management-web/CHANGELOG.md index 6594559a9261a..a0a6d6ca349a3 100644 --- a/sdk/spring/spring-cloud-azure-feature-management-web/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-feature-management-web/CHANGELOG.md @@ -1,14 +1,11 @@ # Release History -## 5.6.0-beta.1 (Unreleased) +## 5.6.0-beta.1 (2023-09-11) -### Features Added + ### Features Added -### Breaking Changes - -### Bugs Fixed - -### Other Changes + * Snapshot support using, `spring.cloud.azure.appconfiguration.stores[0].selects[0].snapshot-name`. + * Support for trimming prefixes from keys, default value is the key-filter when key-filter is used. `spring.cloud.azure.appconfiguration.stores[0].trim-key-prefix` ## 5.5.0 (2023-08-28) diff --git a/sdk/spring/spring-cloud-azure-feature-management/CHANGELOG.md b/sdk/spring/spring-cloud-azure-feature-management/CHANGELOG.md index 70784437b504f..25800b26b7797 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-feature-management/CHANGELOG.md @@ -1,14 +1,11 @@ # Release History -## 5.6.0-beta.1 (Unreleased) +## 5.6.0-beta.1 (2023-09-11) -### Features Added - -### Breaking Changes - -### Bugs Fixed + ### Features Added -### Other Changes + * Snapshot support using, `spring.cloud.azure.appconfiguration.stores[0].selects[0].snapshot-name`. + * Support for trimming prefixes from keys, default value is the key-filter when key-filter is used. `spring.cloud.azure.appconfiguration.stores[0].trim-key-prefix` ## 5.5.0 (2023-08-28) diff --git a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/CHANGELOG.md b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/CHANGELOG.md index e0efa8e931934..0ede9498c2de5 100644 --- a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/CHANGELOG.md @@ -1,14 +1,11 @@ # Release History -## 5.6.0-beta.1 (Unreleased) +## 5.6.0-beta.1 (2023-09-11) -### Features Added - -### Breaking Changes - -### Bugs Fixed + ### Features Added -### Other Changes + * Snapshot support using, `spring.cloud.azure.appconfiguration.stores[0].selects[0].snapshot-name`. + * Support for trimming prefixes from keys, default value is the key-filter when key-filter is used. `spring.cloud.azure.appconfiguration.stores[0].trim-key-prefix` ## 5.5.0 (2023-08-28) diff --git a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md index fa4116a968cdb..e176589b280dd 100644 --- a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md +++ b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md @@ -62,6 +62,8 @@ spring.cloud.azure.appconfiguration.stores[0].enabled | Whether the store will b spring.cloud.azure.appconfiguration.stores[0].fail-fast | Whether to throw a `RuntimeException` or not when failing to read from 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].selects[0].key-filter | The key pattern used to indicate which configuration(s) will be loaded. | No | /application/* spring.cloud.azure.appconfiguration.stores[0].selects[0].label-filter | The label used to indicate which configuration(s) will be loaded. | No | `${spring.profiles.active}` or if null `\0` +spring.cloud.azure.appconfiguration.stores[0].selects[0].snapshot-name | The snapshot name used to indicate which configuration(s) will be loaded. | No | null + spring.cloud.azure.appconfiguration.stores[0].trim-key-prefix[0] | The prefix that will be trimmed from the key when the configuration is loaded. | No | null, unless using key-filter, then it is the key-filter Configuration Store Authentication @@ -168,6 +170,75 @@ spring: label-filter: ',${spring.profiles.active}' ``` +#### Snapshots + + App Configuration snapshots allow you to freeze a moment in time of your configuration store. Snapshots are immutable. Snapshots are stored in the same configuration store as the rest of your configuration data. Snapshots are identified by a unique snapshot name. The snapshot name is a string that can contain any combination of alphanumeric characters, hyphens, and underscores. The snapshot name is case sensitive and must be unique within the configuration store. + + To load configuration from a snapshot, use the following configuration: + + ```yaml + spring: + cloud: + azure: + appconfiguration: + stores: + - + connection-string: + selects: + - + snapshot-name: + trim-key-prefix: + - /application/ + ``` + + NOTE: Snapshots have to be of the composition type KEY in order to be loaded, this is to stop configuration name conflicts inside of a snapshot. + + NOTE 2: If keys start with a prefix such as `/application/` a trim value is needed otherwise `/` will be converted to `.` and your key will not be mapped to `@ConfigurationProperties` + + When using snapshots, key-filters and label filters aren't used. The snapshot is loaded as is. You can load multiple snapshots by adding multiple selects, even adding key and label filters to other selects. + + ```yaml + spring: + cloud: + azure: + appconfiguration: + stores: + - + connection-string: + selects: + - + snapshot-name: + - + key-filter: + label-filter: + ``` + + In this case, the snapshot is loaded first then keys from the filter are loaded. If there are duplicate keys, the last key loaded has the highest priority. + + If previously you used these keys in your application outside of a snapshot than they will most likely contain a prefix like `/application/`, when using a key filter the prefix was automatically removed, but it isn't with a snapshot, which means you have to trim your key names. + + ```yaml + spring: + cloud: + azure: + appconfiguration: + stores: + - + connection-string: + selects: + - + snapshot-name: + trim: + - /application/ + ``` + + This will trim the prefix from all keys in the snapshot, and will also trim any other keys selected if they begin with the prefix. This has also been added to the key filter, so you can use it there as well, though it overrides the key-filter name trim. + + + NOTE: If you are only using snapshots, you don't have to monitor the configuration store, as snapshots are immutable. But if you are using snapshots and other configuration data, you can still monitor the configuration store. + + NOTE: If your snapshot includes feature flags they will automatically be loaded even if feature flags are disabled. If feature flags are enabled, the feature flags will be loaded, any feature flags loaded this way take priority of feature flags loaded from snapshots. + #### Configuration Refresh Configuration Refresh feature allows the application to load the latest property value from configuration store automatically, without restarting the application.