diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md b/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md
index aec8fbaa3a98..c9fe94793c7a 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md
@@ -4,6 +4,8 @@
### Features Added
+- Added support for Snapshot References in App Configuration stores. Snapshot reference settings are now automatically resolved, loading the referenced snapshot's configuration settings as properties.
+
### Breaking Changes
### Bugs Fixed
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 59941a313076..0a33e68af2d4 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
@@ -23,6 +23,7 @@
import com.azure.data.appconfiguration.models.SettingSelector;
import com.azure.security.keyvault.secrets.models.KeyVaultSecret;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_CONTENT_TYPE;
+import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.WatchedConfigurationSettings;
/**
* Azure App Configuration PropertySource unique per Store Label(Profile) combo.
@@ -44,9 +45,15 @@ class AppConfigurationApplicationSettingPropertySource extends AppConfigurationP
private final List tagsFilter;
+ protected List featureFlagsList = new ArrayList<>();
+
+ private static final String SNAPSHOT_REF_CONTENT_TYPE = "application/json; profile=\"https://azconfig.io/mime-profiles/snapshot-ref\"; charset=utf-8";
+
+ protected final FeatureFlagClient featureFlagClient;
+
AppConfigurationApplicationSettingPropertySource(String name, AppConfigurationReplicaClient replicaClient,
AppConfigurationKeyVaultClientFactory keyVaultClientFactory, String keyFilter, String[] labelFilters,
- List tagsFilter) {
+ List tagsFilter, FeatureFlagClient featureFlagClient) {
// The context alone does not uniquely define a PropertySource, append storeName
// and label to uniquely define a PropertySource
super(name + getLabelName(labelFilters), replicaClient);
@@ -54,6 +61,7 @@ class AppConfigurationApplicationSettingPropertySource extends AppConfigurationP
this.keyFilter = keyFilter;
this.labelFilters = labelFilters;
this.tagsFilter = tagsFilter;
+ this.featureFlagClient = featureFlagClient;
}
/**
@@ -65,7 +73,8 @@ class AppConfigurationApplicationSettingPropertySource extends AppConfigurationP
* @throws InvalidConfigurationPropertyValueException thrown if fails to parse Json content type
*/
@Override
- public void initProperties(List keyPrefixTrimValues, Context context) throws InvalidConfigurationPropertyValueException {
+ public void initProperties(List keyPrefixTrimValues, Context context)
+ throws InvalidConfigurationPropertyValueException {
replicaClient.getTracingInfo().resetAiConfigurationTracing();
@@ -82,14 +91,21 @@ public void initProperties(List keyPrefixTrimValues, Context context) th
}
// * for wildcard match
- processConfigurationSettings(replicaClient.listSettings(settingSelector, context), settingSelector.getKeyFilter(),
- keyPrefixTrimValues);
+ processConfigurationSettings(replicaClient.listSettings(settingSelector, context),
+ settingSelector.getKeyFilter(),
+ keyPrefixTrimValues, context);
}
}
protected void processConfigurationSettings(List settings, String keyFilter,
- List keyPrefixTrimValues)
+ List keyPrefixTrimValues, Context context)
throws InvalidConfigurationPropertyValueException {
+ // Reset per-label state so flags from a previous label aren't re-processed.
+ featureFlagsList.clear();
+
+ // First resolve snapshot references
+ settings = resolveSnapshotReferences(settings, context);
+
for (ConfigurationSetting setting : settings) {
replicaClient.getTracingInfo().updateAiConfigurationTracing(setting.getContentType());
if (keyPrefixTrimValues == null && StringUtils.hasText(keyFilter)) {
@@ -110,6 +126,30 @@ protected void processConfigurationSettings(List settings,
properties.put(key, setting.getValue());
}
}
+
+ WatchedConfigurationSettings featureFlags = new WatchedConfigurationSettings(null, featureFlagsList);
+ featureFlagClient.processFeatureFlags(featureFlags, replicaClient.getEndpoint());
+ }
+
+ private List resolveSnapshotReferences(List settings, Context context) {
+ List resolvedSettings = new ArrayList<>();
+ for (ConfigurationSetting setting : settings) {
+ if (SNAPSHOT_REF_CONTENT_TYPE.equals(setting.getContentType())) {
+ // Handle snapshot reference
+ replicaClient.getTracingInfo().setUsesSnapshotReference();
+ List snapshotSettings = replicaClient.listSettingSnapshot(setting.getValue(),
+ context);
+ resolvedSettings.addAll(snapshotSettings);
+ } else if (setting instanceof FeatureFlagConfigurationSetting) {
+ // We need to strip feature flags as we only support feature flags from snapshots, and if they are in a
+ // snapshot reference we won't be able to resolve them.
+ LOGGER.warn("Feature Flag {} with key {} is being ignored as it is not from a snapshot reference.",
+ setting.getLabel(), setting.getKey());
+ } else {
+ resolvedSettings.add(setting);
+ }
+ }
+ return resolvedSettings;
}
/**
@@ -119,7 +159,7 @@ protected void processConfigurationSettings(List settings,
* @param secretReference {"uri": "<your-vault-url>/secret/<secret>/<version>"}
* @throws InvalidConfigurationPropertyValueException
*/
- private void handleKeyVaultReference(String key, SecretReferenceConfigurationSetting secretReference)
+ protected void handleKeyVaultReference(String key, SecretReferenceConfigurationSetting secretReference)
throws InvalidConfigurationPropertyValueException {
// Parsing Key Vault Reference for URI
try {
@@ -138,10 +178,11 @@ private void handleKeyVaultReference(String key, SecretReferenceConfigurationSet
void handleFeatureFlag(String key, FeatureFlagConfigurationSetting setting, List trimStrings)
throws InvalidConfigurationPropertyValueException {
- // Feature Flags aren't loaded as configuration, but are loaded as feature flags when loading a snapshot.
+ // Feature Flags are only part of this if they come from a snapshot
+ featureFlagsList.add(setting);
}
- private void handleJson(ConfigurationSetting setting, List keyPrefixTrimValues)
+ protected void handleJson(ConfigurationSetting setting, List keyPrefixTrimValues)
throws InvalidConfigurationPropertyValueException {
Map jsonSettings = JsonConfigurationParser.parseJsonSetting(setting);
for (Entry jsonSetting : jsonSettings.entrySet()) {
@@ -150,7 +191,7 @@ private void handleJson(ConfigurationSetting setting, List keyPrefixTrim
}
}
- private String trimKey(String key, List trimStrings) {
+ protected String trimKey(String key, List trimStrings) {
key = key.trim();
if (trimStrings != null) {
for (String trim : trimStrings) {
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationConstants.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationConstants.java
index 0b8869814b4d..89304c594c8d 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationConstants.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationConstants.java
@@ -93,4 +93,9 @@ public class AppConfigurationConstants {
* Constant for tracing AI Chat Completion configuration usage.
*/
public static final String AI_CHAT_COMPLETION_FEATURE = "AICC";
+
+ /**
+ * Constant for tracing snapshot reference usage.
+ */
+ public static final String SNAPSHOT_REFERENCE_TAG = "SnapshotRef";
}
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
index a22a6dee1e20..2465fd8dfe72 100644
--- 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
@@ -2,14 +2,16 @@
// Licensed under the MIT License.
package com.azure.spring.cloud.appconfiguration.config.implementation;
-import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
+import org.springframework.util.StringUtils;
import com.azure.core.util.Context;
import com.azure.data.appconfiguration.models.ConfigurationSetting;
import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting;
+import com.azure.data.appconfiguration.models.SecretReferenceConfigurationSetting;
+import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_CONTENT_TYPE;
import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.WatchedConfigurationSettings;
/**
@@ -23,18 +25,13 @@ final class AppConfigurationSnapshotPropertySource extends AppConfigurationAppli
private final String snapshotName;
- private final FeatureFlagClient featureFlagClient;
-
- private List featureFlagsList = new ArrayList<>();
-
AppConfigurationSnapshotPropertySource(String name, AppConfigurationReplicaClient replicaClient,
AppConfigurationKeyVaultClientFactory keyVaultClientFactory, String snapshotName,
FeatureFlagClient featureFlagClient) {
// The context alone does not uniquely define a PropertySource, append storeName
// and label to uniquely define a PropertySource
- super(name, replicaClient, keyVaultClientFactory, null, null, null);
+ super(name, replicaClient, keyVaultClientFactory, null, null, null, featureFlagClient);
this.snapshotName = snapshotName;
- this.featureFlagClient = featureFlagClient;
}
/**
@@ -43,12 +40,29 @@ final class AppConfigurationSnapshotPropertySource extends AppConfigurationAppli
*
*
* @param trim prefix to trim
- * @param isRefresh true if a refresh triggered the loading of the Snapshot.
+ * @param context request context propagated to the App Configuration client.
* @throws InvalidConfigurationPropertyValueException thrown if fails to parse Json content type
*/
+ @Override
public void initProperties(List trim, Context context) throws InvalidConfigurationPropertyValueException {
replicaClient.getTracingInfo().resetAiConfigurationTracing();
- processConfigurationSettings(replicaClient.listSettingSnapshot(snapshotName, context), null, trim);
+ List settings = replicaClient.listSettingSnapshot(snapshotName, context);
+
+ for (ConfigurationSetting setting : settings) {
+ String key = trimKey(setting.getKey(), trim);
+
+ if (setting instanceof SecretReferenceConfigurationSetting) {
+ handleKeyVaultReference(key, (SecretReferenceConfigurationSetting) setting);
+ } else if (setting instanceof FeatureFlagConfigurationSetting
+ && FEATURE_FLAG_CONTENT_TYPE.equals(setting.getContentType())) {
+ handleFeatureFlag(key, (FeatureFlagConfigurationSetting) setting, trim);
+ } else if (StringUtils.hasText(setting.getContentType())
+ && JsonConfigurationParser.isJsonContentType(setting.getContentType())) {
+ handleJson(setting, trim);
+ } else {
+ properties.put(key, setting.getValue());
+ }
+ }
WatchedConfigurationSettings featureFlags = new WatchedConfigurationSettings(null, featureFlagsList);
featureFlagClient.processFeatureFlags(featureFlags, replicaClient.getEndpoint());
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java
index 88ee215b12d0..da144d9443ca 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java
@@ -374,7 +374,7 @@ private List createSettings(AppConfigurationRepl
propertySource = new AppConfigurationApplicationSettingPropertySource(
selectedKeys.getKeyFilter() + resource.getEndpoint() + "/", client, keyVaultClientFactory,
selectedKeys.getKeyFilter(), selectedKeys.getLabelFilter(profiles),
- selectedKeys.getTagsFilter());
+ selectedKeys.getTagsFilter(), featureFlagClient);
}
propertySource.initProperties(resource.getTrimKeyPrefix(), requestContext);
sourceList.add(propertySource);
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfo.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfo.java
index e4eb53addcf8..94533bf73ea6 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfo.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfo.java
@@ -12,6 +12,7 @@
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.KEY_VAULT_CONFIGURED_TRACING;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.LOAD_BALANCING_FEATURE;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.PUSH_REFRESH;
+import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.SNAPSHOT_REFERENCE_TAG;
import com.azure.spring.cloud.appconfiguration.config.implementation.HostType;
import com.azure.spring.cloud.appconfiguration.config.implementation.JsonConfigurationParser;
import com.azure.spring.cloud.appconfiguration.config.implementation.RequestTracingConstants;
@@ -35,6 +36,8 @@ public class TracingInfo {
private boolean usesAiccConfiguration = false;
+ private boolean usesSnapshotReference = false;
+
private boolean isFailoverRequest = false;
public TracingInfo(boolean isKeyVaultConfigured, int replicaCount, Configuration configuration) {
@@ -58,6 +61,13 @@ public void setFailoverRequest() {
this.isFailoverRequest = true;
}
+ /**
+ * Marks snapshot references as used.
+ */
+ public void setUsesSnapshotReference() {
+ this.usesSnapshotReference = true;
+ }
+
/**
* Resets AI configuration tracing flags.
*/
@@ -199,6 +209,12 @@ private String createFeaturesString() {
}
sb.append(AI_CHAT_COMPLETION_FEATURE);
}
+ if (usesSnapshotReference) {
+ if (sb.length() > 0) {
+ sb.append(DELIMITER);
+ }
+ sb.append(SNAPSHOT_REFERENCE_TAG);
+ }
return sb.toString();
}
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 cc1ed8b1620e..19778edfda1d 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
@@ -96,6 +96,9 @@ public class AppConfigurationApplicationSettingPropertySourceTest {
@Mock
private Context contextMock;
+ @Mock
+ private FeatureFlagClient featureFlagClientMock;
+
private MockitoSession session;
@BeforeAll
@@ -122,7 +125,7 @@ public void init() {
String[] labelFilter = { "\0" };
propertySource = new AppConfigurationApplicationSettingPropertySource(TEST_STORE_NAME, clientMock,
- keyVaultClientFactoryMock, KEY_FILTER, labelFilter, null);
+ keyVaultClientFactoryMock, KEY_FILTER, labelFilter, null, featureFlagClientMock);
}
@AfterEach
@@ -209,7 +212,7 @@ public void initPropertiesWithTagsFilterTest() throws IOException {
List tagsFilter = Arrays.asList("env=prod", "team=backend");
AppConfigurationApplicationSettingPropertySource taggedPropertySource
= new AppConfigurationApplicationSettingPropertySource(TEST_STORE_NAME, clientMock,
- keyVaultClientFactoryMock, KEY_FILTER, labelFilter, tagsFilter);
+ keyVaultClientFactoryMock, KEY_FILTER, labelFilter, tagsFilter, featureFlagClientMock);
when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(testItems);
@@ -231,7 +234,7 @@ public void initPropertiesWithNullTagsFilterTest() throws IOException {
String[] labelFilter = { "\0" };
AppConfigurationApplicationSettingPropertySource untaggedPropertySource
= new AppConfigurationApplicationSettingPropertySource(TEST_STORE_NAME, clientMock,
- keyVaultClientFactoryMock, KEY_FILTER, labelFilter, null);
+ keyVaultClientFactoryMock, KEY_FILTER, labelFilter, null, featureFlagClientMock);
when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(testItems);
@@ -253,7 +256,7 @@ public void initPropertiesWithEmptyTagsFilterTest() throws IOException {
List tagsFilter = new ArrayList<>();
AppConfigurationApplicationSettingPropertySource emptyTagPropertySource
= new AppConfigurationApplicationSettingPropertySource(TEST_STORE_NAME, clientMock,
- keyVaultClientFactoryMock, KEY_FILTER, labelFilter, tagsFilter);
+ keyVaultClientFactoryMock, KEY_FILTER, labelFilter, tagsFilter, featureFlagClientMock);
when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(testItems);
@@ -275,7 +278,7 @@ public void initPropertiesWithTagsFilterMultipleLabelsTest() throws IOException
List tagsFilter = Arrays.asList("env=staging");
AppConfigurationApplicationSettingPropertySource multiLabelPropertySource
= new AppConfigurationApplicationSettingPropertySource(TEST_STORE_NAME, clientMock,
- keyVaultClientFactoryMock, KEY_FILTER, labelFilter, tagsFilter);
+ keyVaultClientFactoryMock, KEY_FILTER, labelFilter, tagsFilter, featureFlagClientMock);
when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(testItems);
@@ -291,4 +294,123 @@ public void initPropertiesWithTagsFilterMultipleLabelsTest() throws IOException
assertThat(capturedSelector.getTagsFilter()).containsExactly("env=staging");
}
}
+
+ @Test
+ public void snapshotReferenceIsResolved() throws IOException {
+ // Create a snapshot reference setting
+ String snapshotRefContentType = "application/json; profile=\"https://azconfig.io/mime-profiles/snapshot-ref\"; charset=utf-8";
+ ConfigurationSetting snapshotRef = new ConfigurationSetting()
+ .setKey("snapshot-ref-key")
+ .setValue("my-snapshot")
+ .setContentType(snapshotRefContentType);
+
+ List settingsWithRef = new ArrayList<>();
+ settingsWithRef.add(snapshotRef);
+
+ // The snapshot contains regular settings
+ List snapshotSettings = new ArrayList<>();
+ snapshotSettings.add(createItem(KEY_FILTER, TEST_KEY_1, TEST_VALUE_1, TEST_LABEL_1, EMPTY_CONTENT_TYPE));
+ snapshotSettings.add(createItem(KEY_FILTER, TEST_KEY_2, TEST_VALUE_2, TEST_LABEL_2, EMPTY_CONTENT_TYPE));
+
+ when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(settingsWithRef);
+ when(clientMock.listSettingSnapshot(Mockito.eq("my-snapshot"), Mockito.any(Context.class)))
+ .thenReturn(snapshotSettings);
+
+ propertySource.initProperties(null, contextMock);
+
+ String[] keyNames = propertySource.getPropertyNames();
+ assertThat(keyNames).hasSize(2);
+ assertThat(propertySource.getProperty(TEST_KEY_1)).isEqualTo(TEST_VALUE_1);
+ assertThat(propertySource.getProperty(TEST_KEY_2)).isEqualTo(TEST_VALUE_2);
+ }
+
+ @Test
+ public void snapshotReferenceWithMixedSettings() throws IOException {
+ // Mix of snapshot references and regular settings
+ String snapshotRefContentType = "application/json; profile=\"https://azconfig.io/mime-profiles/snapshot-ref\"; charset=utf-8";
+ ConfigurationSetting snapshotRef = new ConfigurationSetting()
+ .setKey("snapshot-ref-key")
+ .setValue("my-snapshot")
+ .setContentType(snapshotRefContentType);
+ ConfigurationSetting regularSetting = createItem(KEY_FILTER, TEST_KEY_3, TEST_VALUE_3, TEST_LABEL_3,
+ EMPTY_CONTENT_TYPE);
+
+ List mixedSettings = new ArrayList<>();
+ mixedSettings.add(snapshotRef);
+ mixedSettings.add(regularSetting);
+
+ List snapshotSettings = new ArrayList<>();
+ snapshotSettings.add(createItem(KEY_FILTER, TEST_KEY_1, TEST_VALUE_1, TEST_LABEL_1, EMPTY_CONTENT_TYPE));
+
+ when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(mixedSettings);
+ when(clientMock.listSettingSnapshot(Mockito.eq("my-snapshot"), Mockito.any(Context.class)))
+ .thenReturn(snapshotSettings);
+
+ propertySource.initProperties(null, contextMock);
+
+ String[] keyNames = propertySource.getPropertyNames();
+ assertThat(keyNames).hasSize(2);
+ assertThat(propertySource.getProperty(TEST_KEY_1)).isEqualTo(TEST_VALUE_1);
+ assertThat(propertySource.getProperty(TEST_KEY_3)).isEqualTo(TEST_VALUE_3);
+ }
+
+ @Test
+ public void featureFlagsAreStrippedFromNonSnapshotSettings() throws IOException {
+ // Feature flags outside snapshots should be ignored/stripped
+ List settingsWithFeatureFlag = new ArrayList<>();
+ settingsWithFeatureFlag.add(ITEM_1);
+ settingsWithFeatureFlag.add(FEATURE_FLAG);
+
+ when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(settingsWithFeatureFlag);
+
+ propertySource.initProperties(null, contextMock);
+
+ // Only the regular setting should be loaded as a property
+ String[] keyNames = propertySource.getPropertyNames();
+ assertThat(keyNames).hasSize(1);
+ assertThat(propertySource.getProperty(TEST_KEY_1)).isEqualTo(TEST_VALUE_1);
+ }
+
+ @Test
+ public void featureFlagsFromSnapshotAreCollected() throws IOException {
+ // Feature flags from snapshot references should be collected
+ String snapshotRefContentType = "application/json; profile=\"https://azconfig.io/mime-profiles/snapshot-ref\"; charset=utf-8";
+ ConfigurationSetting snapshotRef = new ConfigurationSetting()
+ .setKey("snapshot-ref-key")
+ .setValue("my-snapshot")
+ .setContentType(snapshotRefContentType);
+
+ List settings = new ArrayList<>();
+ settings.add(snapshotRef);
+
+ FeatureFlagConfigurationSetting featureFlag = createItemFeatureFlag("Beta", "/0");
+ List snapshotSettings = new ArrayList<>();
+ snapshotSettings.add(createItem(KEY_FILTER, TEST_KEY_1, TEST_VALUE_1, TEST_LABEL_1, EMPTY_CONTENT_TYPE));
+ snapshotSettings.add(featureFlag);
+
+ when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(settings);
+ when(clientMock.listSettingSnapshot(Mockito.eq("my-snapshot"), Mockito.any(Context.class)))
+ .thenReturn(snapshotSettings);
+
+ propertySource.initProperties(null, contextMock);
+
+ // Feature flag should be in the featureFlagsList
+ assertThat(propertySource.featureFlagsList).hasSize(1);
+ assertThat(propertySource.featureFlagsList.get(0)).isInstanceOf(FeatureFlagConfigurationSetting.class);
+
+ // Regular setting should be loaded
+ assertThat(propertySource.getProperty(TEST_KEY_1)).isEqualTo(TEST_VALUE_1);
+
+ // featureFlagClient should have been called
+ Mockito.verify(featureFlagClientMock).processFeatureFlags(Mockito.any(), Mockito.any());
+ }
+
+ @Test
+ public void featureFlagClientIsCalledOnInit() throws IOException {
+ when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(testItems);
+
+ propertySource.initProperties(null, contextMock);
+
+ Mockito.verify(featureFlagClientMock).processFeatureFlags(Mockito.any(), Mockito.any());
+ }
}
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 6eaf41d6276d..28768ff3187f 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
@@ -76,6 +76,9 @@ public class AppConfigurationPropertySourceKeyVaultTest {
@Mock
private Context contextMock;
+
+ @Mock
+ private FeatureFlagClient featureFlagClientMock;
private MockitoSession session;
@@ -93,7 +96,7 @@ public void init() {
String[] labelFilter = { "\0" };
propertySource = new AppConfigurationApplicationSettingPropertySource(TEST_STORE_NAME, replicaClientMock,
- keyVaultClientFactoryMock, KEY_FILTER, labelFilter, null);
+ keyVaultClientFactoryMock, KEY_FILTER, labelFilter, null, featureFlagClientMock);
}
@AfterEach
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfoTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfoTest.java
index 1a3d85c6afe7..a1f2257f3164 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfoTest.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfoTest.java
@@ -72,6 +72,15 @@ public void loadBalancingTracingTest() {
assertTrue(value.contains("Features=LB"));
}
+ @Test
+ public void snapshotReferenceTracingTest() {
+ Configuration configuration = getConfiguration("false");
+ TracingInfo tracingInfo = new TracingInfo(false, 0, configuration);
+ tracingInfo.setUsesSnapshotReference();
+ String value = tracingInfo.getValue(false, false, null);
+ assertTrue(value.contains("Features=SnapshotRef"));
+ }
+
@Test
public void aiConfigurationTracingTest() {
Configuration configuration = getConfiguration("false");
@@ -144,8 +153,9 @@ public void multipleFeaturesTracingTest() {
TracingInfo tracingInfo = new TracingInfo(false, 0, configuration);
tracingInfo.setUsesLoadBalancing();
tracingInfo.updateAiConfigurationTracing("application/json; profile=\"https://azconfig.io/mime-profiles/ai\"");
+ tracingInfo.setUsesSnapshotReference();
String value = tracingInfo.getValue(false, false, null);
- assertTrue(value.contains("Features=LB+AI"));
+ assertTrue(value.contains("Features=LB+AI+SnapshotRef"));
}
@Test
@@ -183,6 +193,7 @@ public void fullCorrelationContextTest() {
Configuration configuration = getConfiguration("false");
TracingInfo tracingInfo = new TracingInfo(true, 2, configuration);
tracingInfo.setUsesLoadBalancing();
+ tracingInfo.setUsesSnapshotReference();
tracingInfo.setFailoverRequest();
FeatureFlagTracing ffTracing = new FeatureFlagTracing();
@@ -198,7 +209,7 @@ public void fullCorrelationContextTest() {
assertTrue(value.contains("Filter=TRGT"));
assertTrue(value.contains("MaxVariants=3"));
assertTrue(value.contains("FFFeatures=Telemetry"));
- assertTrue(value.contains("Features=LB"));
+ assertTrue(value.contains("Features=LB+SnapshotRef"));
assertTrue(value.contains("UsesKeyVault"));
assertTrue(value.contains("PushRefresh"));
assertTrue(value.contains("Failover"));