diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt
index 2feadc7feeb9..74d93e18e17c 100644
--- a/eng/versioning/external_dependencies.txt
+++ b/eng/versioning/external_dependencies.txt
@@ -411,6 +411,8 @@ springboot3_org.springframework:spring-web;6.2.5
springboot3_org.springframework:spring-webmvc;6.2.5
springboot3_org.testcontainers:junit-jupiter;1.20.6
springboot3_org.testcontainers:azure;1.20.6
+springboot3_jakarta.annotation:jakarta.annotation-api;3.0.0
+springboot3_ch.qos.logback:logback-classic;1.5.18
# Used for Spring version updates
springboot3_org.springframework.boot:spring-boot-dependencies;3.4.4
springboot3_org.springframework.cloud:spring-cloud-dependencies;2024.0.1
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/AppConfigurationWebAutoConfiguration.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/AppConfigurationWebAutoConfiguration.java
index 8e1376a46045..fd08cc653354 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/AppConfigurationWebAutoConfiguration.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/AppConfigurationWebAutoConfiguration.java
@@ -42,15 +42,18 @@ AppConfigurationEventListener configListener(AppConfigurationRefresh appConfigur
"org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties",
"org.springframework.cloud.endpoint.RefreshEndpoint"
})
+ @Deprecated
static class AppConfigurationPushRefreshConfiguration {
@Bean
+ @Deprecated
AppConfigurationRefreshEndpoint appConfigurationRefreshEndpoint(ContextRefresher contextRefresher,
AppConfigurationProperties appConfiguration) {
return new AppConfigurationRefreshEndpoint(contextRefresher, appConfiguration);
}
@Bean
+ @Deprecated
AppConfigurationRefreshEventListener appConfigurationRefreshEventListener(
AppConfigurationRefresh appConfigurationRefresh) {
return new AppConfigurationRefreshEventListener(appConfigurationRefresh);
@@ -63,15 +66,18 @@ AppConfigurationRefreshEventListener appConfigurationRefreshEventListener(
"org.springframework.cloud.bus.BusProperties",
"org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent",
"org.springframework.cloud.endpoint.RefreshEndpoint" })
+ @Deprecated
static class AppConfigurationBusConfiguration {
@Bean
+ @Deprecated
AppConfigurationBusRefreshEndpoint appConfigurationBusRefreshEndpoint(ApplicationContext context,
BusProperties bus, AppConfigurationProperties appConfiguration, Destination.Factory destinationFactory) {
return new AppConfigurationBusRefreshEndpoint(context, bus.getId(), destinationFactory, appConfiguration);
}
@Bean
+ @Deprecated
AppConfigurationBusRefreshEventListener appConfigurationBusRefreshEventListener(
AppConfigurationRefresh appConfigurationRefresh) {
return new AppConfigurationBusRefreshEventListener(appConfigurationRefresh);
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pullrefresh/AppConfigurationEventListener.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pullrefresh/AppConfigurationEventListener.java
index 1b00e0f55600..8771558968af 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pullrefresh/AppConfigurationEventListener.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pullrefresh/AppConfigurationEventListener.java
@@ -9,6 +9,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
+import org.springframework.lang.NonNull;
import org.springframework.web.context.support.ServletRequestHandledEvent;
import com.azure.spring.cloud.appconfiguration.config.AppConfigurationRefresh;
@@ -32,7 +33,7 @@ public AppConfigurationEventListener(AppConfigurationRefresh appConfigurationRef
}
@Override
- public void onApplicationEvent(ServletRequestHandledEvent event) {
+ public void onApplicationEvent(@NonNull ServletRequestHandledEvent event) {
try {
if (!(event.getRequestUrl().equals(ACTUATOR + APPCONFIGURATION_REFRESH)
|| event.getRequestUrl().equals(ACTUATOR + APPCONFIGURATION_REFRESH_BUS))) {
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEventListener.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEventListener.java
index bf846e6418df..296f554f51ec 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEventListener.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEventListener.java
@@ -5,6 +5,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
+import org.springframework.lang.NonNull;
import com.azure.spring.cloud.appconfiguration.config.AppConfigurationRefresh;
@@ -31,7 +32,7 @@ public AppConfigurationBusRefreshEventListener(AppConfigurationRefresh appConfig
* @param event Event Triggering refresh, contains valid config store endpoint.
*/
@Override
- public void onApplicationEvent(AppConfigurationBusRefreshEvent event) {
+ public void onApplicationEvent(@NonNull AppConfigurationBusRefreshEvent event) {
try {
appConfigurationRefresh.expireRefreshInterval(event.getEndpoint(), event.getSyncToken());
} catch (Exception e) {
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushrefresh/AppConfigurationRefreshEndpoint.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushrefresh/AppConfigurationRefreshEndpoint.java
index 139f2a18b433..1a97678e0cd9 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushrefresh/AppConfigurationRefreshEndpoint.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushrefresh/AppConfigurationRefreshEndpoint.java
@@ -10,11 +10,12 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint;
+import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.http.HttpStatus;
+import org.springframework.lang.NonNull;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@@ -29,8 +30,7 @@
/**
* Endpoint for requesting new configurations to be loaded.
*/
-@SuppressWarnings("removal")
-@ControllerEndpoint(id = APPCONFIGURATION_REFRESH)
+@Endpoint(id = APPCONFIGURATION_REFRESH)
public class AppConfigurationRefreshEndpoint implements ApplicationEventPublisherAware {
private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationRefreshEndpoint.class);
@@ -105,7 +105,7 @@ public String refresh(HttpServletRequest request, HttpServletResponse response,
}
@Override
- public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
+ public void setApplicationEventPublisher(@NonNull ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushrefresh/AppConfigurationRefreshEventListener.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushrefresh/AppConfigurationRefreshEventListener.java
index 187946abbf02..cfaf05050bc9 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushrefresh/AppConfigurationRefreshEventListener.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushrefresh/AppConfigurationRefreshEventListener.java
@@ -5,6 +5,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
+import org.springframework.lang.NonNull;
import com.azure.spring.cloud.appconfiguration.config.AppConfigurationRefresh;
@@ -32,7 +33,7 @@ public AppConfigurationRefreshEventListener(AppConfigurationRefresh appConfigura
* @param event Event Triggering refresh, contains valid config store endpoint.
*/
@Override
- public void onApplicationEvent(AppConfigurationRefreshEvent event) {
+ public void onApplicationEvent(@NonNull AppConfigurationRefreshEvent event) {
try {
appConfigurationRefresh.expireRefreshInterval(event.getEndpoint(), event.getSyncToken());
} catch (Exception e) {
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/AppConfigurationWebAutoConfigurationTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/AppConfigurationWebAutoConfigurationTest.java
deleted file mode 100644
index 2f861d46ad5d..000000000000
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/AppConfigurationWebAutoConfigurationTest.java
+++ /dev/null
@@ -1,100 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-package com.azure.spring.cloud.appconfiguration.config.web.implementation;
-
-import static com.azure.spring.cloud.appconfiguration.config.web.implementation.TestConstants.CONN_STRING_PROP;
-import static com.azure.spring.cloud.appconfiguration.config.web.implementation.TestConstants.STORE_ENDPOINT_PROP;
-import static com.azure.spring.cloud.appconfiguration.config.web.implementation.TestConstants.TEST_CONN_STRING;
-import static com.azure.spring.cloud.appconfiguration.config.web.implementation.TestConstants.TEST_STORE_NAME;
-import static com.azure.spring.cloud.appconfiguration.config.web.implementation.TestUtils.propPair;
-import static org.assertj.core.api.Assertions.assertThat;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
-import org.springframework.boot.autoconfigure.AutoConfigurations;
-import org.springframework.boot.test.context.FilteredClassLoader;
-import org.springframework.boot.test.context.runner.ApplicationContextRunner;
-import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration;
-import org.springframework.cloud.bus.BusProperties;
-import org.springframework.cloud.bus.event.PathDestinationFactory;
-import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent;
-import org.springframework.cloud.endpoint.RefreshEndpoint;
-
-import com.azure.spring.cloud.appconfiguration.config.AppConfigurationAutoConfiguration;
-import com.azure.spring.cloud.appconfiguration.config.implementation.config.AppConfigurationBootstrapConfiguration;
-import com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalPropertiesAutoConfiguration;
-
-public class AppConfigurationWebAutoConfigurationTest {
-
- private static final ApplicationContextRunner CONTEXT_RUNNER = new ApplicationContextRunner()
- .withPropertyValues(propPair(CONN_STRING_PROP, TEST_CONN_STRING),
- propPair(STORE_ENDPOINT_PROP, TEST_STORE_NAME))
- .withConfiguration(AutoConfigurations.of(AppConfigurationBootstrapConfiguration.class,
- AppConfigurationAutoConfiguration.class, AppConfigurationWebAutoConfiguration.class,
- RefreshAutoConfiguration.class, PathDestinationFactory.class, AzureGlobalPropertiesAutoConfiguration.class))
- .withUserConfiguration(BusProperties.class);
-
- @Test
- public void refreshMissing() {
- CONTEXT_RUNNER
- .withClassLoader(new FilteredClassLoader(WebEndpointProperties.class))
- .run(context -> {
- assertThat(context)
- .doesNotHaveBean("appConfigurationRefreshBusEndpoint");
- assertThat(context)
- .doesNotHaveBean("appConfigurationRefreshEndpoint");
- assertThat(context)
- .hasBean("configListener");
- });
- }
-
- @Test
- public void busRefreshMissing() {
- CONTEXT_RUNNER
- .withClassLoader(new FilteredClassLoader(RefreshRemoteApplicationEvent.class))
- .run(context -> {
- assertThat(context)
- .doesNotHaveBean("appConfigurationBusRefreshEndpoint");
- assertThat(context)
- .hasBean("appConfigurationRefreshEndpoint");
- assertThat(context)
- .hasBean("configListener");
- });
- }
-
- @Test
- public void pullRefreshListenerMissing() {
- CONTEXT_RUNNER.withClassLoader(new FilteredClassLoader(RefreshEndpoint.class))
- .run(context -> assertThat(context)
- .doesNotHaveBean("configListener"));
- }
-
- @Test
- public void pushRefresh() {
- CONTEXT_RUNNER
- .run(context -> {
- assertThat(context)
- .hasBean("appConfigurationRefreshEndpoint");
- });
- }
-
- @Test
- public void busRefresh() {
- CONTEXT_RUNNER
- .run(context -> assertThat(context)
- .hasBean("appConfigurationBusRefreshEndpoint"));
- }
-
- @Test
- public void fullRefresh() {
- CONTEXT_RUNNER
- .run(context -> {
- assertThat(context)
- .hasBean("configListener");
- assertThat(context)
- .hasBean("appConfigurationRefreshEndpoint");
- assertThat(context)
- .hasBean("appConfigurationBusRefreshEndpoint");
- });
- }
-}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml
index 42a477922d33..bf2a8d518612 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml
@@ -29,11 +29,6 @@
3.4.4
true
-
- org.springframework.cloud
- spring-cloud-starter-bootstrap
- 4.2.1
-
org.springframework.cloud
spring-cloud-context
@@ -45,6 +40,11 @@
3.4.4
compile
+
+ jakarta.annotation
+ jakarta.annotation-api
+ 3.0.0
+
com.azure
@@ -177,6 +177,7 @@
+ jakarta.annotation:jakarta.annotation-api:[3.0.0]
com.fasterxml.jackson.core:jackson-annotations:[2.18.3]
com.fasterxml.jackson.core:jackson-databind:[2.18.3]
org.springframework.boot:spring-boot-actuator:[3.4.4]
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationAutoConfiguration.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationWatchAutoConfiguration.java
similarity index 52%
rename from sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationAutoConfiguration.java
rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationWatchAutoConfiguration.java
index dde0e852d65a..9b0d4be6c0a0 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationAutoConfiguration.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationWatchAutoConfiguration.java
@@ -2,12 +2,14 @@
// Licensed under the MIT License.
package com.azure.spring.cloud.appconfiguration.config;
+import org.springframework.boot.BootstrapContext;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.endpoint.RefreshEndpoint;
import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationPullRefresh;
@@ -15,41 +17,31 @@
import com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationReplicaClientFactory;
import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp;
import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProperties;
-import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProviderProperties;
/**
* Setup AppConfigurationRefresh when spring.cloud.azure.appconfiguration.enabled is enabled.
*/
-@Configuration
@EnableAsync
@ConditionalOnProperty(prefix = AppConfigurationProperties.CONFIG_PREFIX, name = "enabled", matchIfMissing = true)
-public class AppConfigurationAutoConfiguration {
+@EnableConfigurationProperties({ AppConfigurationProperties.class })
+@AutoConfiguration
+@ConditionalOnClass(RefreshEndpoint.class)
+public class AppConfigurationWatchAutoConfiguration {
/**
- * Creates an instance of {@link AppConfigurationAutoConfiguration}
+ * Creates an instance of {@link AppConfigurationWatchAutoConfiguration}
*/
- public AppConfigurationAutoConfiguration() {
+ public AppConfigurationWatchAutoConfiguration() {
}
- /**
- * Auto Watch
- */
- @Configuration
- @ConditionalOnClass(RefreshEndpoint.class)
- public static class AppConfigurationWatchAutoConfiguration {
-
- /**
- * Creates an instance of {@link AppConfigurationWatchAutoConfiguration}
- */
- public AppConfigurationWatchAutoConfiguration() {
- }
+ @Bean
+ @ConditionalOnMissingBean
+ AppConfigurationRefresh appConfigurationRefresh(AppConfigurationProperties properties, BootstrapContext context) {
+ AppConfigurationReplicaClientFactory clientFactory = context
+ .get(AppConfigurationReplicaClientFactory.class);
+ ReplicaLookUp replicaLookUp = context.get(ReplicaLookUp.class);
- @Bean
- @ConditionalOnMissingBean
- AppConfigurationRefresh appConfigurationRefresh(AppConfigurationProperties properties,
- AppConfigurationProviderProperties appProperties, AppConfigurationReplicaClientFactory clientFactory, ReplicaLookUp replicaLookUp) {
- return new AppConfigurationPullRefresh(clientFactory, properties.getRefreshInterval(),
- appProperties.getDefaultMinBackoff(), replicaLookUp, new AppConfigurationRefreshUtil());
- }
+ return new AppConfigurationPullRefresh(clientFactory, properties.getRefreshInterval(), replicaLookUp,
+ new AppConfigurationRefreshUtil());
}
}
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 5d410ab33151..2f1f5e0f28a7 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
@@ -18,6 +18,7 @@
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;
@@ -60,7 +61,7 @@ class AppConfigurationApplicationSettingPropertySource extends AppConfigurationP
* @param keyPrefixTrimValues prefixs to trim from key values
* @throws InvalidConfigurationPropertyValueException thrown if fails to parse Json content type
*/
- public void initProperties(List keyPrefixTrimValues, boolean isRefresh) throws InvalidConfigurationPropertyValueException {
+ public void initProperties(List keyPrefixTrimValues, Context context) throws InvalidConfigurationPropertyValueException {
List labels = Arrays.asList(labelFilters);
// Reverse labels so they have the right priority order.
@@ -70,7 +71,7 @@ public void initProperties(List keyPrefixTrimValues, boolean isRefresh)
SettingSelector settingSelector = new SettingSelector().setKeyFilter(keyFilter + "*").setLabelFilter(label);
// * for wildcard match
- processConfigurationSettings(replicaClient.listSettings(settingSelector, isRefresh), settingSelector.getKeyFilter(),
+ processConfigurationSettings(replicaClient.listSettings(settingSelector, context), settingSelector.getKeyFilter(),
keyPrefixTrimValues);
}
}
@@ -107,7 +108,7 @@ protected void processConfigurationSettings(List settings,
* @return Key Vault Secret Value
* @throws InvalidConfigurationPropertyValueException
*/
- protected void handleKeyVaultReference(String key, SecretReferenceConfigurationSetting secretReference)
+ private void handleKeyVaultReference(String key, SecretReferenceConfigurationSetting secretReference)
throws InvalidConfigurationPropertyValueException {
// Parsing Key Vault Reference for URI
try {
@@ -126,10 +127,11 @@ protected void handleKeyVaultReference(String key, SecretReferenceConfigurationS
void handleFeatureFlag(String key, FeatureFlagConfigurationSetting setting, List trimStrings)
throws InvalidConfigurationPropertyValueException {
- handleJson(setting, trimStrings);
+ // Feature Flags aren't loaded as configuration, but are loaded as feature flags when loading a snapshot.
+ return;
}
- void handleJson(ConfigurationSetting setting, List keyPrefixTrimValues)
+ private void handleJson(ConfigurationSetting setting, List keyPrefixTrimValues)
throws InvalidConfigurationPropertyValueException {
Map jsonSettings = JsonConfigurationParser.parseJsonSetting(setting);
for (Entry jsonSetting : jsonSettings.entrySet()) {
@@ -138,7 +140,7 @@ void handleJson(ConfigurationSetting setting, List keyPrefixTrimValues)
}
}
- protected String trimKey(String key, List trimStrings) {
+ private 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 44a8db8df3cc..b480b72eb922 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
@@ -12,16 +12,6 @@ public class AppConfigurationConstants {
*/
public static final String FEATURE_FLAG_CONTENT_TYPE = "application/vnd.microsoft.appconfig.ff+json;charset=utf-8";
- /**
- * App Configurations Key Vault Reference Content Type
- */
- public static final String KEY_VAULT_CONTENT_TYPE = "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8";
-
- /**
- * Feature Management Key Prefix
- */
- public static final String FEATURE_MANAGEMENT_KEY = "feature-management.";
-
/**
* Feature Flag Prefix
*/
@@ -46,17 +36,17 @@ public class AppConfigurationConstants {
* Constant for tracing if Key Vault is configured for use.
*/
public static final String KEY_VAULT_CONFIGURED_TRACING = "UsesKeyVault";
-
+
/**
- * Constant for tracing for Replica Count
+ * Constant for tracing if Push Refresh is enabled for the store.
*/
- public static final String REPLICA_COUNT = "ReplicaCount";
+ public static final String PUSH_REFRESH = "PushRefresh";
/**
* Http Header User Agent
*/
public static final String USER_AGENT_TYPE = "User-Agent";
-
+
/**
* Http Header Correlation Context
*/
@@ -71,30 +61,10 @@ public class AppConfigurationConstants {
public static final String TELEMETRY = "telemetry";
- public static final String USERS = "users";
-
- public static final String USERS_CAPS = "Users";
-
- public static final String AUDIENCE = "Audience";
-
- public static final String GROUPS = "groups";
-
- public static final String GROUPS_CAPS = "Groups";
-
- public static final String TARGETING_FILTER = "targetingFilter";
-
- public static final String DEFAULT_ROLLOUT_PERCENTAGE = "defaultRolloutPercentage";
-
- public static final String DEFAULT_ROLLOUT_PERCENTAGE_CAPS = "DefaultRolloutPercentage";
-
public static final String DEFAULT_REQUIREMENT_TYPE = "Any";
public static final String REQUIREMENT_TYPE_SERVICE = "requirement_type";
- public static final String REQUIREMENT_TYPE = "requirement-type";
-
- public static final String FEATURE_FLAG_ID = "FeatureFlagId";
-
public static final String FEATURE_FLAG_REFERENCE = "FeatureFlagReference";
public static final String E_TAG = "ETag";
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 310081227eaa..186ae9586f14 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
@@ -2,8 +2,13 @@
// Licensed under the MIT License.
package com.azure.spring.cloud.appconfiguration.config.implementation;
+import java.util.List;
+
import org.springframework.core.env.EnumerablePropertySource;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+
/**
* Azure App Configuration PropertySource unique per Store Label(Profile) combo.
*
@@ -19,6 +24,8 @@ class AppConfigurationFeatureManagementPropertySource extends EnumerableProperty
private static final String FEATURE_FLAG_KEY = FEATURE_MANAGEMENT_KEY + ".feature_flags";
+ private static final ObjectMapper MAPPER = JsonMapper.builder().build();
+
AppConfigurationFeatureManagementPropertySource(FeatureFlagClient featureFlagLoader) {
super(FEATURE_MANAGEMENT_KEY, featureFlagLoader);
this.featureFlagLoader = featureFlagLoader;
@@ -26,14 +33,16 @@ class AppConfigurationFeatureManagementPropertySource extends EnumerableProperty
@Override
public String[] getPropertyNames() {
- String[] names = { FEATURE_FLAG_KEY };
- return names;
+ if (featureFlagLoader != null && featureFlagLoader.getFeatureFlags().size() > 0) {
+ return new String[]{ FEATURE_FLAG_KEY };
+ }
+ return new String[0];
}
@Override
public Object getProperty(String name) {
if (FEATURE_FLAG_KEY.equals(name)) {
- return featureFlagLoader.getProperties();
+ return MAPPER.convertValue(featureFlagLoader.getFeatureFlags(), List.class);
}
return null;
}
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 2cc9fbffe363..67f9e17256b0 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
@@ -10,7 +10,7 @@
import com.azure.spring.cloud.appconfiguration.config.implementation.stores.AppConfigurationSecretClientManager;
import com.azure.spring.cloud.service.implementation.keyvault.secrets.SecretClientBuilderFactory;
-public class AppConfigurationKeyVaultClientFactory {
+class AppConfigurationKeyVaultClientFactory {
private final Map keyVaultClients;
@@ -23,27 +23,24 @@ public class AppConfigurationKeyVaultClientFactory {
private final boolean credentialsConfigured;
private final boolean isConfigured;
-
- private final int timeout;
- public AppConfigurationKeyVaultClientFactory(SecretClientCustomizer keyVaultClientProvider,
+ AppConfigurationKeyVaultClientFactory(SecretClientCustomizer keyVaultClientProvider,
KeyVaultSecretProvider keyVaultSecretProvider, SecretClientBuilderFactory secretClientFactory,
- boolean credentialsConfigured, int timeout) {
+ boolean credentialsConfigured) {
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) {
+ AppConfigurationSecretClientManager getClient(String host) {
// Check if we already have a client for this key vault, if not we will make
// one
if (!keyVaultClients.containsKey(host)) {
AppConfigurationSecretClientManager client = new AppConfigurationSecretClientManager(host,
- keyVaultClientProvider, keyVaultSecretProvider, secretClientFactory, credentialsConfigured, timeout);
+ keyVaultClientProvider, keyVaultSecretProvider, secretClientFactory, credentialsConfigured);
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 c31069689625..9515c30258a9 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,7 +2,6 @@
// 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;
@@ -11,8 +10,8 @@
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
import org.springframework.core.env.EnumerablePropertySource;
+import com.azure.core.util.Context;
import com.azure.data.appconfiguration.ConfigurationClient;
-import com.azure.data.appconfiguration.models.ConfigurationSetting;
/**
* Azure App Configuration PropertySource unique per Store Label(Profile) combo.
@@ -26,8 +25,6 @@ abstract class AppConfigurationPropertySource extends EnumerablePropertySource properties = new LinkedHashMap<>();
- protected final List featureConfigurationSettings = new ArrayList<>();
-
protected final AppConfigurationReplicaClient replicaClient;
AppConfigurationPropertySource(String name, AppConfigurationReplicaClient replicaClient) {
@@ -55,5 +52,5 @@ protected static String getLabelName(String[] labelFilters) {
return String.join(",", labelFilters);
}
- protected abstract void initProperties(List trim, boolean isRefresh) throws InvalidConfigurationPropertyValueException;
+ protected abstract void initProperties(List trim, Context context) throws InvalidConfigurationPropertyValueException;
}
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
deleted file mode 100644
index 04de646b0631..000000000000
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceLocator.java
+++ /dev/null
@@ -1,304 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-package com.azure.spring.cloud.appconfiguration.config.implementation;
-
-import static org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration.BOOTSTRAP_PROPERTY_SOURCE_NAME;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
-import org.springframework.core.env.CompositePropertySource;
-import org.springframework.core.env.ConfigurableEnvironment;
-import org.springframework.core.env.Environment;
-import org.springframework.core.env.PropertySource;
-import org.springframework.util.StringUtils;
-
-import com.azure.data.appconfiguration.models.ConfigurationSetting;
-import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp;
-import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags;
-import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationKeyValueSelector;
-import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProviderProperties;
-import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring;
-import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore;
-import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagKeyValueSelector;
-
-/**
- * Locates Azure App Configuration Property Sources.
- */
-public final class AppConfigurationPropertySourceLocator implements PropertySourceLocator {
-
- private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationPropertySourceLocator.class);
-
- private static final String PROPERTY_SOURCE_NAME = "azure-config-store";
-
- private static final String REFRESH_ARGS_PROPERTY_SOURCE = "refreshArgs";
-
- private final List configStores;
-
- private final AppConfigurationProviderProperties appProperties;
-
- private final AppConfigurationReplicaClientFactory clientFactory;
-
- private final AppConfigurationKeyVaultClientFactory keyVaultClientFactory;
-
- private final FeatureFlagClient featureFlagClient;
-
- private final ReplicaLookUp replicaLookUp;
-
- private Duration refreshInterval;
-
- static final AtomicBoolean STARTUP = new AtomicBoolean(true);
-
- /**
- * Loads all Azure App Configuration Property Sources configured.
- *
- * @param properties Configurations for stores to be loaded.
- * @param appProperties Configurations for the library.
- * @param clientFactory factory for creating clients for connecting to Azure App Configuration.
- * @param keyVaultClientFactory factory for creating clients for connecting to Azure Key Vault
- * @param featureFlagLoader service for loadingFeatureFlags.
- */
- public AppConfigurationPropertySourceLocator(AppConfigurationProviderProperties appProperties,
- AppConfigurationReplicaClientFactory clientFactory, AppConfigurationKeyVaultClientFactory keyVaultClientFactory,
- Duration refreshInterval, List configStores, ReplicaLookUp replicaLookUp,
- FeatureFlagClient featureFlagLoader) {
- this.refreshInterval = refreshInterval;
- this.appProperties = appProperties;
- this.configStores = configStores;
- this.clientFactory = clientFactory;
- this.keyVaultClientFactory = keyVaultClientFactory;
- this.replicaLookUp = replicaLookUp;
- this.featureFlagClient = featureFlagLoader;
-
- BackoffTimeCalculator.setDefaults(appProperties.getDefaultMaxBackoff(), appProperties.getDefaultMinBackoff());
- }
-
- @Override
- public PropertySource> locate(Environment environment) {
- if (!(environment instanceof ConfigurableEnvironment)) {
- return null;
- }
- replicaLookUp.updateAutoFailoverEndpoints();
-
- ConfigurableEnvironment env = (ConfigurableEnvironment) environment;
- boolean currentlyLoaded = env.getPropertySources().stream().anyMatch(source -> {
- String storeName = configStores.get(0).getEndpoint();
- if (configStores.get(0).getSelects().size() == 0) {
- return false;
- }
- AppConfigurationKeyValueSelector selectedKey = configStores.get(0).getSelects().get(0);
- return source.getName()
- .startsWith(BOOTSTRAP_PROPERTY_SOURCE_NAME + "-" + selectedKey.getKeyFilter() + storeName + "/");
- });
- if (currentlyLoaded && !env.getPropertySources().contains(REFRESH_ARGS_PROPERTY_SOURCE)) {
- return null;
- }
-
- List profiles = Arrays.asList(env.getActiveProfiles());
-
- CompositePropertySource composite = new CompositePropertySource(PROPERTY_SOURCE_NAME);
- Collections.reverse(configStores); // Last store has the highest precedence
-
- StateHolder newState = new StateHolder();
- newState.setNextForcedRefresh(refreshInterval);
-
- // Feature Management needs to be set in the last config store.
- for (ConfigStore configStore : configStores) {
- boolean loadNewPropertySources = STARTUP.get() || StateHolder.getLoadState(configStore.getEndpoint());
-
- if (configStore.isEnabled() && loadNewPropertySources) {
- // There is only one Feature Set for all AppConfigurationPropertySources
-
- List clients = clientFactory
- .getAvailableClients(configStore.getEndpoint(), true);
-
- boolean generatedPropertySources = false;
-
- List sourceList = new ArrayList<>();
- boolean reloadFailed = false;
-
- for (AppConfigurationReplicaClient client : clients) {
- sourceList = new ArrayList<>();
-
- if (!STARTUP.get() && reloadFailed && !AppConfigurationRefreshUtil
- .checkStoreAfterRefreshFailed(client, clientFactory, configStore.getFeatureFlags())) {
- // This store doesn't have any changes where to refresh store did. Skipping Checking next.
- continue;
- }
-
- // Reverse in order to add Profile specific properties earlier, and last profile comes first
- try {
- List sources = createSettings(client, configStore, profiles);
- List featureFlags = createFeatureFlags(client, configStore, profiles);
- sourceList.addAll(sources);
-
- LOGGER.debug("PropertySource context.");
- setupMonitoring(configStore, client, sources, newState, featureFlags);
-
- generatedPropertySources = true;
- } catch (AppConfigurationStatusException e) {
- reloadFailed = true;
- clientFactory.backoffClientClient(configStore.getEndpoint(), client.getEndpoint());
- } catch (Exception e) {
- newState = failedToGeneratePropertySource(configStore, newState, e);
-
- // Not a retiable error
- break;
- }
- if (generatedPropertySources) {
- break;
- }
- }
-
- if (generatedPropertySources) {
- // Updating list of propertySources
- sourceList.forEach(composite::addPropertySource);
- } else if (!STARTUP.get() || (configStore.isFailFast() && STARTUP.get())) {
- String message = "Failed to generate property sources for " + configStore.getEndpoint();
-
- // Refresh failed for a config store ending attempt
- failedToGeneratePropertySource(configStore, newState, new RuntimeException(message));
- }
-
- if (featureFlagClient.getProperties().size() > 0) {
- // This can be true if feature flags are enabled or if a Snapshot contained feature flags
- AppConfigurationFeatureManagementPropertySource acfmps = new AppConfigurationFeatureManagementPropertySource(
- featureFlagClient);
- composite.addPropertySource(acfmps);
- }
-
- } else if (!configStore.isEnabled() && loadNewPropertySources) {
- LOGGER.info("Not loading configurations from {} as it is not enabled.", configStore.getEndpoint());
- } else {
- LOGGER.warn("Not loading configurations from {} as it failed on startup.", configStore.getEndpoint());
- }
- }
-
- StateHolder.updateState(newState);
- STARTUP.set(false);
-
- return composite;
- }
-
- private void setupMonitoring(ConfigStore configStore, AppConfigurationReplicaClient client,
- List sources, StateHolder newState, List featureFlags) {
- AppConfigurationStoreMonitoring monitoring = configStore.getMonitoring();
-
- if (configStore.getFeatureFlags().getEnabled()) {
- newState.setStateFeatureFlag(configStore.getEndpoint(), featureFlags,
- monitoring.getFeatureFlagRefreshInterval());
- }
-
- if (monitoring.isEnabled()) {
- // Setting new ETag values for Watch
- List watchKeysSettings = monitoring.getTriggers().stream()
- .map(trigger -> client.getWatchKey(trigger.getKey(), trigger.getLabel(), !STARTUP.get())).toList();
-
- newState.setState(configStore.getEndpoint(), watchKeysSettings, monitoring.getRefreshInterval());
- }
- newState.setLoadState(configStore.getEndpoint(), true, configStore.isFailFast());
- }
-
- private StateHolder failedToGeneratePropertySource(ConfigStore configStore, StateHolder newState, Exception e) {
- String message = "Failed to generate property sources for " + configStore.getEndpoint();
- if (!STARTUP.get()) {
- // Need to check for refresh first, or reset will never happen if fail fast is true.
- LOGGER.error("Refreshing failed while reading configuration from Azure App Configuration store "
- + configStore.getEndpoint() + ".");
-
- if (refreshInterval != null) {
- // The next refresh will happen sooner if refresh interval is expired.
- newState.updateNextRefreshTime(refreshInterval, appProperties.getDefaultMinBackoff());
- }
- throw new RuntimeException(message, e);
- } else if (configStore.isFailFast()) {
- LOGGER.error("Fail fast is set and there was an error reading configuration from Azure App "
- + "Configuration store " + configStore.getEndpoint() + ".");
- delayException();
- throw new RuntimeException(message, e);
- } else {
- LOGGER.warn(
- "Unable to load configuration from Azure AppConfiguration store " + configStore.getEndpoint() + ".", e);
- newState.setLoadState(configStore.getEndpoint(), false, configStore.isFailFast());
- }
- return newState;
- }
-
- /**
- * Creates a new set of AppConfigurationPropertySources, 1 per Label.
- *
- * @param client client for connecting to App Configuration
- * @param store Config Store the PropertySource is being generated from
- * @param profiles active profiles to be used as labels. it needs to be in the last one.
- * @return a list of AppConfigurationPropertySources
- * @throws Exception creating a property source failed
- */
- private List createSettings(AppConfigurationReplicaClient client, ConfigStore store,
- List profiles) throws Exception {
- List sourceList = new ArrayList<>();
- List selects = store.getSelects();
-
- for (AppConfigurationKeyValueSelector selectedKeys : selects) {
- AppConfigurationPropertySource propertySource = null;
-
- if (StringUtils.hasText(selectedKeys.getSnapshotName())) {
- propertySource = new AppConfigurationSnapshotPropertySource(
- selectedKeys.getSnapshotName() + "/" + store.getEndpoint(), client, keyVaultClientFactory,
- selectedKeys.getSnapshotName(), featureFlagClient);
- } else {
- propertySource = new AppConfigurationApplicationSettingPropertySource(
- selectedKeys.getKeyFilter() + store.getEndpoint() + "/", client, keyVaultClientFactory,
- selectedKeys.getKeyFilter(), selectedKeys.getLabelFilter(profiles));
- }
- propertySource.initProperties(store.getTrimKeyPrefix(), !STARTUP.get());
- sourceList.add(propertySource);
-
- }
-
- return sourceList;
- }
-
- /**
- * Creates a new set of AppConfigurationPropertySources, 1 per Label.
- *
- * @param client client for connecting to App Configuration
- * @param store Config Store the PropertySource is being generated from
- * @param profiles active profiles to be used as labels. it needs to be in the last one.
- * @return a list of AppConfigurationPropertySources
- * @throws Exception creating a property source failed
- */
- private List createFeatureFlags(AppConfigurationReplicaClient client, ConfigStore store,
- List profiles) throws Exception {
- List featureFlagWatchKeys = new ArrayList<>();
- if (store.getFeatureFlags().getEnabled()) {
- for (FeatureFlagKeyValueSelector selectedKeys : store.getFeatureFlags().getSelects()) {
- List storesFeatureFlags = featureFlagClient.loadFeatureFlags(client,
- selectedKeys.getKeyFilter(), selectedKeys.getLabelFilter(profiles), !STARTUP.get());
- storesFeatureFlags.forEach(featureFlags -> featureFlags.setConfigStore(store));
- featureFlagWatchKeys.addAll(storesFeatureFlags);
- }
- }
- return featureFlagWatchKeys;
- }
-
- private void delayException() {
- Instant currentDate = Instant.now();
- Instant preKillTIme = appProperties.getStartDate().plusSeconds(appProperties.getPrekillTime());
- if (currentDate.isBefore(preKillTIme)) {
- long diffInMillies = Math.abs(preKillTIme.toEpochMilli() - currentDate.toEpochMilli());
- try {
- Thread.sleep(diffInMillies);
- } catch (InterruptedException e) {
- LOGGER.error("Failed to wait before fast fail.");
- }
- }
- }
-}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefresh.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefresh.java
index 44c59c1583a2..c45799b10629 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefresh.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefresh.java
@@ -30,8 +30,8 @@ public class AppConfigurationPullRefresh implements AppConfigurationRefresh {
private final AtomicBoolean running = new AtomicBoolean(false);
private ApplicationEventPublisher publisher;
-
- private final Long defaultMinBackoff;
+
+ private final Long defaultMinBackoff = (long) 30;
private final AppConfigurationReplicaClientFactory clientFactory;
@@ -46,11 +46,9 @@ public class AppConfigurationPullRefresh implements AppConfigurationRefresh {
*
* @param clientFactory Clients stores used to connect to App Configuration. * @param defaultMinBackoff default
* @param refreshInterval time between refresh intervals
- * @param defaultMinBackoff minimum time between backoff retries minimum backoff time
*/
public AppConfigurationPullRefresh(AppConfigurationReplicaClientFactory clientFactory, Duration refreshInterval,
- Long defaultMinBackoff, ReplicaLookUp replicaLookUp, AppConfigurationRefreshUtil refreshUtils) {
- this.defaultMinBackoff = defaultMinBackoff;
+ ReplicaLookUp replicaLookUp, AppConfigurationRefreshUtil refreshUtils) {
this.refreshInterval = refreshInterval;
this.clientFactory = clientFactory;
this.replicaLookUp = replicaLookUp;
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtil.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtil.java
index ca1cc33c3905..a5337b08e43a 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtil.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtil.java
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
package com.azure.spring.cloud.appconfiguration.config.implementation;
+import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.PUSH_REFRESH;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
@@ -9,13 +10,16 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.util.StringUtils;
import com.azure.core.exception.HttpResponseException;
+import com.azure.core.util.Context;
import com.azure.data.appconfiguration.models.ConfigurationSetting;
import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlagState;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags;
import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring.PushNotification;
import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagStore;
public class AppConfigurationRefreshUtil {
@@ -50,6 +54,14 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF
clientFactory.setCurrentConfigStoreClient(originEndpoint, originEndpoint);
AppConfigurationStoreMonitoring monitor = connection.getMonitoring();
+
+ boolean pushRefresh = false;
+ PushNotification notification = monitor.getPushNotification();
+ if ((notification.getPrimaryToken() != null && StringUtils.hasText(notification.getPrimaryToken().getName()))
+ || (notification.getSecondaryToken() != null && StringUtils.hasText(notification.getPrimaryToken().getName()))) {
+ pushRefresh = true;
+ }
+ Context context = new Context("refresh", true).addData(PUSH_REFRESH, pushRefresh);
List clients = clientFactory.getAvailableClients(originEndpoint);
@@ -57,7 +69,7 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF
for (AppConfigurationReplicaClient client : clients) {
try {
refreshWithTime(client, StateHolder.getState(originEndpoint), monitor.getRefreshInterval(),
- eventData, replicaLookUp);
+ eventData, replicaLookUp, context);
if (eventData.getDoRefresh()) {
clientFactory.setCurrentConfigStoreClient(originEndpoint, client.getEndpoint());
return eventData;
@@ -81,7 +93,7 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF
for (AppConfigurationReplicaClient client : clients) {
try {
refreshWithTimeFeatureFlags(client, StateHolder.getStateFeatureFlag(originEndpoint),
- monitor.getFeatureFlagRefreshInterval(), eventData, replicaLookUp);
+ monitor.getFeatureFlagRefreshInterval(), eventData, replicaLookUp, context);
if (eventData.getDoRefresh()) {
clientFactory.setCurrentConfigStoreClient(originEndpoint, client.getEndpoint());
return eventData;
@@ -108,12 +120,6 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF
return eventData;
}
- static boolean checkStoreAfterRefreshFailed(AppConfigurationReplicaClient client,
- AppConfigurationReplicaClientFactory clientFactory, FeatureFlagStore featureStore) {
- return refreshStoreCheck(client, clientFactory.findOriginForEndpoint(client.getEndpoint()))
- || refreshStoreFeatureFlagCheck(featureStore, client);
- }
-
/**
* This is for a refresh fail only.
*
@@ -121,10 +127,10 @@ static boolean checkStoreAfterRefreshFailed(AppConfigurationReplicaClient client
* @param originEndpoint config store origin endpoint
* @return A refresh should be triggered.
*/
- private static boolean refreshStoreCheck(AppConfigurationReplicaClient client, String originEndpoint) {
+ static boolean refreshStoreCheck(AppConfigurationReplicaClient client, String originEndpoint, Context context) {
RefreshEventData eventData = new RefreshEventData();
if (StateHolder.getLoadState(originEndpoint)) {
- refreshWithoutTime(client, StateHolder.getState(originEndpoint).getWatchKeys(), eventData);
+ refreshWithoutTime(client, StateHolder.getState(originEndpoint).getWatchKeys(), eventData, context);
}
return eventData.getDoRefresh();
}
@@ -136,13 +142,13 @@ private static boolean refreshStoreCheck(AppConfigurationReplicaClient client, S
* @param client Client checking for refresh
* @return true if a refresh should be triggered.
*/
- private static boolean refreshStoreFeatureFlagCheck(FeatureFlagStore featureStore,
- AppConfigurationReplicaClient client) {
+ static boolean refreshStoreFeatureFlagCheck(Boolean featureStoreEnabled,
+ AppConfigurationReplicaClient client, Context context) {
RefreshEventData eventData = new RefreshEventData();
String endpoint = client.getEndpoint();
- if (featureStore.getEnabled() && StateHolder.getStateFeatureFlag(endpoint) != null) {
- refreshWithoutTimeFeatureFlags(client, StateHolder.getStateFeatureFlag(endpoint), eventData);
+ if (featureStoreEnabled && StateHolder.getStateFeatureFlag(endpoint) != null) {
+ refreshWithoutTimeFeatureFlags(client, StateHolder.getStateFeatureFlag(endpoint), eventData, context);
} else {
LOGGER.debug("Skipping feature flag refresh check for " + endpoint);
}
@@ -157,10 +163,10 @@ private static boolean refreshStoreFeatureFlagCheck(FeatureFlagStore featureStor
* @param eventData Info for this refresh event.
*/
private static void refreshWithTime(AppConfigurationReplicaClient client, State state, Duration refreshInterval,
- RefreshEventData eventData, ReplicaLookUp replicaLookUp) throws AppConfigurationStatusException {
+ RefreshEventData eventData, ReplicaLookUp replicaLookUp, Context context) throws AppConfigurationStatusException {
if (Instant.now().isAfter(state.getNextRefreshCheck())) {
replicaLookUp.updateAutoFailoverEndpoints();
- refreshWithoutTime(client, state.getWatchKeys(), eventData);
+ refreshWithoutTime(client, state.getWatchKeys(), eventData, context);
StateHolder.getCurrentState().updateStateRefresh(state, refreshInterval);
}
@@ -174,9 +180,9 @@ private static void refreshWithTime(AppConfigurationReplicaClient client, State
* @param eventData Refresh event info
*/
private static void refreshWithoutTime(AppConfigurationReplicaClient client, List watchKeys,
- RefreshEventData eventData) throws AppConfigurationStatusException {
+ RefreshEventData eventData, Context context) throws AppConfigurationStatusException {
for (ConfigurationSetting watchKey : watchKeys) {
- ConfigurationSetting watchedKey = client.getWatchKey(watchKey.getKey(), watchKey.getLabel(), true);
+ ConfigurationSetting watchedKey = client.getWatchKey(watchKey.getKey(), watchKey.getLabel(), context);
// If there is no result, etag will be considered empty.
// A refresh will trigger once the selector returns a value.
@@ -190,15 +196,14 @@ private static void refreshWithoutTime(AppConfigurationReplicaClient client, Lis
}
private static void refreshWithTimeFeatureFlags(AppConfigurationReplicaClient client, FeatureFlagState state,
- Duration refreshInterval, RefreshEventData eventData, ReplicaLookUp replicaLookUp)
+ Duration refreshInterval, RefreshEventData eventData, ReplicaLookUp replicaLookUp, Context context)
throws AppConfigurationStatusException {
Instant date = Instant.now();
if (date.isAfter(state.getNextRefreshCheck())) {
replicaLookUp.updateAutoFailoverEndpoints();
for (FeatureFlags featureFlags : state.getWatchKeys()) {
-
- if (client.checkWatchKeys(featureFlags.getSettingSelector(), true)) {
+ if (client.checkWatchKeys(featureFlags.getSettingSelector(), context)) {
String eventDataInfo = ".appconfig.featureflag/*";
// Only one refresh Event needs to be call to update all of the
@@ -216,11 +221,10 @@ private static void refreshWithTimeFeatureFlags(AppConfigurationReplicaClient cl
}
private static void refreshWithoutTimeFeatureFlags(AppConfigurationReplicaClient client, FeatureFlagState watchKeys,
- RefreshEventData eventData) throws AppConfigurationStatusException {
+ RefreshEventData eventData, Context context) throws AppConfigurationStatusException {
for (FeatureFlags featureFlags : watchKeys.getWatchKeys()) {
-
- if (client.checkWatchKeys(featureFlags.getSettingSelector(), true)) {
+ if (client.checkWatchKeys(featureFlags.getSettingSelector(), context)) {
String eventDataInfo = ".appconfig.featureflag/*";
// Only one refresh Event needs to be call to update all of the
@@ -273,7 +277,7 @@ RefreshEventData setMessage(String prefix) {
return this;
}
- RefreshEventData setFullMessage(String message) {
+ private RefreshEventData setFullMessage(String message) {
this.message = message;
this.doRefresh = true;
return this;
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 9c43522cafcb..5c192dc04dbc 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
@@ -22,7 +22,6 @@
import com.azure.data.appconfiguration.models.SettingSelector;
import com.azure.data.appconfiguration.models.SnapshotComposition;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags;
-import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.TracingInfo;
import io.netty.handler.codec.http.HttpResponseStatus;
@@ -33,25 +32,25 @@ class AppConfigurationReplicaClient {
private final String endpoint;
+ private final String originClient;
+
private final ConfigurationClient client;
private Instant backoffEndTime;
private int failedAttempts;
- private final TracingInfo tracingInfo;
-
/**
* Holds Configuration Client and info needed to manage backoff.
* @param endpoint client endpoint
* @param client Configuration Client to App Configuration store
*/
- AppConfigurationReplicaClient(String endpoint, ConfigurationClient client, TracingInfo tracingInfo) {
+ AppConfigurationReplicaClient(String endpoint, String originClient, ConfigurationClient client) {
this.endpoint = endpoint;
+ this.originClient = originClient;
this.client = client;
this.backoffEndTime = Instant.now().minusMillis(1);
this.failedAttempts = 0;
- this.tracingInfo = tracingInfo;
}
/**
@@ -84,6 +83,13 @@ String getEndpoint() {
return endpoint;
}
+ /**
+ * @return originClient
+ */
+ String getOriginClient() {
+ return originClient;
+ }
+
/**
* Gets the Configuration Setting for the given config store that match the Setting Selector criteria. Follows
* retry-after-ms header.
@@ -92,10 +98,9 @@ String getEndpoint() {
* @param label String value of the watch key, use \0 for null.
* @return The first returned configuration.
*/
- ConfigurationSetting getWatchKey(String key, String label, Boolean isRefresh)
+ ConfigurationSetting getWatchKey(String key, String label, Context context)
throws HttpResponseException {
try {
- Context context = new Context("refresh", isRefresh);
ConfigurationSetting selector = new ConfigurationSetting().setKey(key).setLabel(label);
ConfigurationSetting watchKey = NormalizeNull
.normalizeNullLabel(
@@ -115,11 +120,10 @@ ConfigurationSetting getWatchKey(String key, String label, Boolean isRefresh)
* @param settingSelector Information on which setting to pull. i.e. number of results, key value...
* @return List of Configuration Settings.
*/
- List listSettings(SettingSelector settingSelector, Boolean isRefresh)
+ List listSettings(SettingSelector settingSelector, Context context)
throws HttpResponseException {
List configurationSettings = new ArrayList<>();
try {
- Context context = new Context("refresh", isRefresh);
PagedIterable settings = client.listConfigurationSettings(settingSelector, context);
settings.forEach(setting -> {
configurationSettings.add(NormalizeNull.normalizeNullLabel(setting));
@@ -134,11 +138,11 @@ List listSettings(SettingSelector settingSelector, Boolean
}
}
- FeatureFlags listFeatureFlags(SettingSelector settingSelector, Boolean isRefresh) throws HttpResponseException {
+ FeatureFlags listFeatureFlags(SettingSelector settingSelector, Context context)
+ throws HttpResponseException {
List configurationSettings = new ArrayList<>();
List checks = new ArrayList<>();
try {
- Context context = new Context("refresh", isRefresh);
client.listConfigurationSettings(settingSelector, context).streamByPage().forEach(pagedResponse -> {
checks.add(
new MatchConditions().setIfNoneMatch(pagedResponse.getHeaders().getValue(HttpHeaderName.ETAG)));
@@ -159,10 +163,12 @@ FeatureFlags listFeatureFlags(SettingSelector settingSelector, Boolean isRefresh
}
}
- List listSettingSnapshot(String snapshotName) {
+ List listSettingSnapshot(String snapshotName, Context context) {
List configurationSettings = new ArrayList<>();
try {
- ConfigurationSnapshot snapshot = client.getSnapshot(snapshotName);
+ // Because Spring always refreshes all we still have to load snapshots on refresh to build the property
+ // sources.
+ ConfigurationSnapshot snapshot = client.getSnapshotWithResponse(snapshotName, null, context).getValue();
if (!SnapshotComposition.KEY.equals(snapshot.getSnapshotComposition())) {
throw new IllegalArgumentException("Snapshot " + snapshotName + " needs to be of type Key.");
}
@@ -178,8 +184,7 @@ List listSettingSnapshot(String snapshotName) {
}
}
- Boolean checkWatchKeys(SettingSelector settingSelector, Boolean isRefresh) {
- Context context = new Context("refresh", isRefresh);
+ boolean checkWatchKeys(SettingSelector settingSelector, Context context) {
List> results = client.listConfigurationSettings(settingSelector, context)
.streamByPage().filter(pagedResponse -> pagedResponse.getStatusCode() != 304).toList();
return results.size() > 0;
@@ -208,8 +213,4 @@ private HttpResponseException hanndleHttpResponseException(HttpResponseException
return e;
}
- TracingInfo getTracingInfo() {
- return tracingInfo;
- }
-
}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactory.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactory.java
index 4f3fd987cc2f..1aef5729e617 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactory.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactory.java
@@ -25,7 +25,7 @@ public class AppConfigurationReplicaClientFactory {
* @param clientBuilder builder for app configuration replica clients
* @param configStores configuration info for config stores
*/
- public AppConfigurationReplicaClientFactory(AppConfigurationReplicaClientsBuilder clientBuilder,
+ AppConfigurationReplicaClientFactory(AppConfigurationReplicaClientsBuilder clientBuilder,
List configStores, ReplicaLookUp replicaLookUp) {
this.configStores = configStores;
if (CONNECTIONS.size() == 0) {
@@ -99,23 +99,6 @@ String findOriginForEndpoint(String endpoint) {
return endpoint;
}
- /**
- * Checks if a given endpoint has any configured replicas.
- * @param endpoint Endpoint to check for replicas
- * @return true if at least one other unique endpoint connects to the same configuration store
- */
- boolean hasReplicas(String endpoint) {
- String originEndpoint = findOriginForEndpoint(endpoint);
- for (ConfigStore store : configStores) {
- if (store.getEndpoint().equals(originEndpoint)) {
- if (store.getConnectionStrings().size() > 0 || store.getEndpoints().size() > 0) {
- return true;
- }
- }
- }
- return false;
- }
-
/**
* Sets the replica as the currently used endpoint for connecting to the config store.
* @param originEndpoint Origin Configuration Store
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientsBuilder.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientsBuilder.java
index 3e225980e1fd..6b4d42827626 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientsBuilder.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientsBuilder.java
@@ -3,6 +3,8 @@
package com.azure.spring.cloud.appconfiguration.config.implementation;
import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.net.URL;
import java.time.Duration;
import java.util.ArrayList;
@@ -14,8 +16,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.convert.DurationStyle;
-import org.springframework.context.EnvironmentAware;
-import org.springframework.core.env.Environment;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -25,7 +25,8 @@
import com.azure.core.util.Configuration;
import com.azure.core.util.CoreUtils;
import com.azure.data.appconfiguration.ConfigurationClientBuilder;
-import com.azure.identity.ManagedIdentityCredentialBuilder;
+import com.azure.identity.DefaultAzureCredential;
+import com.azure.identity.DefaultAzureCredentialBuilder;
import com.azure.spring.cloud.appconfiguration.config.ConfigurationClientCustomizer;
import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.BaseAppConfigurationPolicy;
import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.TracingInfo;
@@ -37,22 +38,22 @@
import com.azure.spring.cloud.core.service.AzureServiceType.AppConfiguration;
import com.azure.spring.cloud.service.implementation.appconfiguration.ConfigurationClientBuilderFactory;
-public class AppConfigurationReplicaClientsBuilder implements EnvironmentAware {
+public class AppConfigurationReplicaClientsBuilder {
private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationReplicaClientsBuilder.class);
/**
* Invalid Connection String error message
*/
- public static final String NON_EMPTY_MSG = "%s property should not be null or empty in the connection string of Azure Config Service.";
+ private static final String NON_EMPTY_MSG = "%s property should not be null or empty in the connection string of Azure Config Service.";
- public static final String RETRY_MODE_PROPERTY_NAME = "retry.mode";
+ private static final String RETRY_MODE_PROPERTY_NAME = "retry.mode";
- public static final String MAX_RETRIES_PROPERTY_NAME = "retry.exponential.max-retries";
+ private static final String MAX_RETRIES_PROPERTY_NAME = "retry.exponential.max-retries";
- public static final String BASE_DELAY_PROPERTY_NAME = "retry.exponential.base-delay";
+ private static final String BASE_DELAY_PROPERTY_NAME = "retry.exponential.base-delay";
- public static final String MAX_DELAY_PROPERTY_NAME = "retry.exponential.max-delay";
+ private static final String MAX_DELAY_PROPERTY_NAME = "retry.exponential.max-delay";
private static final Duration DEFAULT_MIN_RETRY_POLICY = Duration.ofMillis(800);
@@ -66,30 +67,27 @@ public class AppConfigurationReplicaClientsBuilder implements EnvironmentAware {
/**
* Invalid Formatted Connection String Error message
*/
- public static final String ENDPOINT_ERR_MSG = String.format("Connection string does not follow format %s.",
+ private static final String ENDPOINT_ERR_MSG = String.format("Connection string does not follow format %s.",
CONN_STRING_REGEXP);
private static final Pattern CONN_STRING_PATTERN = Pattern.compile(CONN_STRING_REGEXP);
- private ConfigurationClientCustomizer clientProvider;
+ private ConfigurationClientCustomizer clientCustomizer;
private final ConfigurationClientBuilderFactory clientFactory;
- private Environment env;
-
- private boolean isDev = false;
-
- private boolean isKeyVaultConfigured = false;
+ private boolean isKeyVaultConfigured;
private final boolean credentialConfigured;
- private final int defaultMaxRetries;
+ private final int defaultMaxRetries = 2;
- public AppConfigurationReplicaClientsBuilder(int defaultMaxRetries, ConfigurationClientBuilderFactory clientFactory,
- boolean credentialConfigured) {
- this.defaultMaxRetries = defaultMaxRetries;
- this.clientFactory = clientFactory;
+ AppConfigurationReplicaClientsBuilder(ConfigurationClientBuilderFactory clientFactory,
+ ConfigurationClientCustomizer clientCustomizer, boolean credentialConfigured, boolean isKeyVaultConfigured) {
this.credentialConfigured = credentialConfigured;
+ this.clientFactory = clientFactory;
+ this.clientCustomizer = clientCustomizer;
+ this.isKeyVaultConfigured = isKeyVaultConfigured;
}
/**
@@ -114,17 +112,6 @@ public static String getEndpointFromConnectionString(String connectionString) {
return endpoint;
}
- /**
- * @param clientProvider the clientProvider to set
- */
- public void setClientProvider(ConfigurationClientCustomizer clientProvider) {
- this.clientProvider = clientProvider;
- }
-
- public void setIsKeyVaultConfigured(boolean isKeyVaultConfigured) {
- this.isKeyVaultConfigured = isKeyVaultConfigured;
- }
-
/**
* Builds all the clients for a connection.
*
@@ -169,79 +156,64 @@ List buildClients(ConfigStore configStore) {
LOGGER.debug("Connecting to " + endpoint + " using Connecting String.");
ConfigurationClientBuilder builder = createBuilderInstance().connectionString(connectionString);
- clients.add(modifyAndBuildClient(builder, endpoint, connectionStrings.size() - 1));
+ clients.add(modifyAndBuildClient(builder, endpoint, configStore.getEndpoint(), connectionStrings.size() - 1));
}
} else {
+ DefaultAzureCredential defautAzureCredential = new DefaultAzureCredentialBuilder().build();
for (String endpoint : endpoints) {
ConfigurationClientBuilder builder = this.createBuilderInstance();
if (!credentialConfigured) {
- // System Assigned Identity. Needs to be checked last as all of the above should
- // have an Endpoint.
- LOGGER.debug("Connecting to {} using Azure System Assigned Identity.", endpoint);
- builder.credential(new ManagedIdentityCredentialBuilder().build());
+ builder.credential(defautAzureCredential);
}
builder.endpoint(endpoint);
- clients.add(modifyAndBuildClient(builder, endpoint, endpoints.size() - 1));
+ clients.add(modifyAndBuildClient(builder, endpoint, configStore.getEndpoint(), endpoints.size() - 1));
}
}
return clients;
}
- public AppConfigurationReplicaClient buildClient(String failoverEndpoint, ConfigStore configStore) {
+ AppConfigurationReplicaClient buildClient(String failoverEndpoint, ConfigStore configStore) {
if (StringUtils.hasText(configStore.getConnectionString())) {
ConnectionString connectionString = new ConnectionString(configStore.getConnectionString());
connectionString.setUri(failoverEndpoint);
ConfigurationClientBuilder builder = createBuilderInstance().connectionString(connectionString.toString());
- return modifyAndBuildClient(builder, failoverEndpoint, 0);
+ return modifyAndBuildClient(builder, failoverEndpoint, configStore.getEndpoint(), 0);
} else if (configStore.getConnectionStrings().size() > 0) {
ConnectionString connectionString = new ConnectionString(configStore.getConnectionStrings().get(0));
connectionString.setUri(failoverEndpoint);
ConfigurationClientBuilder builder = createBuilderInstance().connectionString(connectionString.toString());
- return modifyAndBuildClient(builder, failoverEndpoint, 0);
+ return modifyAndBuildClient(builder, failoverEndpoint, configStore.getEndpoint(), 0);
} else {
ConfigurationClientBuilder builder = createBuilderInstance();
if (!credentialConfigured) {
- // System Assigned Identity. Needs to be checked last as all of the above should
- // have an Endpoint.
- LOGGER.debug("Connecting to {} using Azure System Assigned Identity.", failoverEndpoint);
- builder.credential(new ManagedIdentityCredentialBuilder().build());
+ builder.credential(new DefaultAzureCredentialBuilder().build());
}
builder.endpoint(failoverEndpoint);
- return modifyAndBuildClient(builder, failoverEndpoint, 0);
+ return modifyAndBuildClient(builder, failoverEndpoint, configStore.getEndpoint(), 0);
}
}
- private AppConfigurationReplicaClient modifyAndBuildClient(ConfigurationClientBuilder builder, String endpoint,
+ private AppConfigurationReplicaClient modifyAndBuildClient(ConfigurationClientBuilder builder, String endpoint, String originEndpoint,
Integer replicaCount) {
- TracingInfo tracingInfo = new TracingInfo(isDev, isKeyVaultConfigured, replicaCount,
+ TracingInfo tracingInfo = new TracingInfo(isKeyVaultConfigured, replicaCount,
Configuration.getGlobalConfiguration());
builder.addPolicy(new BaseAppConfigurationPolicy(tracingInfo));
- if (clientProvider != null) {
- clientProvider.customize(builder, endpoint);
+ if (clientCustomizer != null) {
+ clientCustomizer.customize(builder, endpoint);
}
- return new AppConfigurationReplicaClient(endpoint, builder.buildClient(), tracingInfo);
+ return new AppConfigurationReplicaClient(endpoint, originEndpoint, builder.buildClient());
}
- @Override
- public void setEnvironment(Environment environment) {
- for (String profile : environment.getActiveProfiles()) {
- if ("dev".equalsIgnoreCase(profile)) {
- this.isDev = true;
- break;
- }
- }
- this.env = environment;
- }
-
- protected ConfigurationClientBuilder createBuilderInstance() {
+ private ConfigurationClientBuilder createBuilderInstance() {
RetryStrategy retryStatagy = null;
- String mode = env.getProperty(AzureGlobalProperties.PREFIX + "." + RETRY_MODE_PROPERTY_NAME);
- String modeService = env.getProperty(AzureAppConfigurationProperties.PREFIX + "." + RETRY_MODE_PROPERTY_NAME);
+ String mode = System.getProperty(AzureGlobalProperties.PREFIX + "." + RETRY_MODE_PROPERTY_NAME);
+ String modeService = System
+ .getProperty(AzureAppConfigurationProperties.PREFIX + "." + RETRY_MODE_PROPERTY_NAME);
if ("exponential".equals(mode) || "exponential".equals(modeService) || (mode == null && modeService == null)) {
Function checkPropertyInt = parameter -> (Integer.parseInt(parameter));
@@ -322,7 +294,6 @@ private static class ConnectionString {
private final String secret;
- @SuppressWarnings("deprecation")
ConnectionString(String connectionString) {
if (CoreUtils.isNullOrEmpty(connectionString)) {
throw new IllegalArgumentException("'connectionString' cannot be null or empty.");
@@ -338,9 +309,11 @@ private static class ConnectionString {
String segment = arg.trim();
if (ENDPOINT.regionMatches(true, 0, segment, 0, ENDPOINT.length())) {
try {
- baseUri = new URL(segment.substring(ENDPOINT.length()));
+ baseUri = new URI(segment.substring(ENDPOINT.length())).toURL();
} catch (MalformedURLException ex) {
throw new IllegalArgumentException(ex);
+ } catch (URISyntaxException ex) {
+ throw new IllegalArgumentException(ex);
}
} else if (ID.regionMatches(true, 0, segment, 0, ID.length())) {
id = segment.substring(ID.length());
@@ -358,12 +331,13 @@ private static class ConnectionString {
}
}
- @SuppressWarnings("deprecation")
protected ConnectionString setUri(String uri) {
try {
- this.baseUri = new URL(uri);
+ this.baseUri = new URI(uri).toURL();
} catch (MalformedURLException ex) {
throw new IllegalArgumentException(ex);
+ } catch (URISyntaxException ex) {
+ throw new IllegalArgumentException(ex);
}
return this;
}
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 e4569b384150..e585997e05a5 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
@@ -7,6 +7,7 @@
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
+import com.azure.core.util.Context;
import com.azure.data.appconfiguration.models.ConfigurationSetting;
import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags;
@@ -42,10 +43,11 @@ final class AppConfigurationSnapshotPropertySource extends AppConfigurationAppli
*
*
* @param trim prefix to trim
+ * @param isRefresh true if a refresh triggered the loading of the Snapshot.
* @throws InvalidConfigurationPropertyValueException thrown if fails to parse Json content type
*/
- public void initProperties(List trim) throws InvalidConfigurationPropertyValueException {
- processConfigurationSettings(replicaClient.listSettingSnapshot(snapshotName), null, trim);
+ public void initProperties(List trim, Context context) throws InvalidConfigurationPropertyValueException {
+ processConfigurationSettings(replicaClient.listSettingSnapshot(snapshotName, context), null, trim);
FeatureFlags featureFlags = new FeatureFlags(null, featureFlagsList);
featureFlagClient.proccessFeatureFlags(featureFlags, replicaClient.getEndpoint());
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigBoostrapRegistrar.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigBoostrapRegistrar.java
new file mode 100644
index 000000000000..fb0a090f0910
--- /dev/null
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigBoostrapRegistrar.java
@@ -0,0 +1,139 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.azure.spring.cloud.appconfiguration.config.implementation;
+
+import org.springframework.boot.BootstrapRegistry.InstanceSupplier;
+import org.springframework.boot.context.config.ConfigDataLocationResolverContext;
+import org.springframework.boot.context.properties.bind.Bindable;
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.util.StringUtils;
+
+import com.azure.data.appconfiguration.ConfigurationClientBuilder;
+import com.azure.spring.cloud.appconfiguration.config.ConfigurationClientCustomizer;
+import com.azure.spring.cloud.appconfiguration.config.KeyVaultSecretProvider;
+import com.azure.spring.cloud.appconfiguration.config.SecretClientCustomizer;
+import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProperties;
+import com.azure.spring.cloud.autoconfigure.implementation.appconfiguration.AzureAppConfigurationProperties;
+import com.azure.spring.cloud.autoconfigure.implementation.context.properties.AzureGlobalProperties;
+import com.azure.spring.cloud.autoconfigure.implementation.keyvault.secrets.properties.AzureKeyVaultSecretProperties;
+import com.azure.spring.cloud.autoconfigure.implementation.properties.core.AbstractAzureHttpConfigurationProperties;
+import com.azure.spring.cloud.autoconfigure.implementation.properties.core.authentication.TokenCredentialConfigurationProperties;
+import com.azure.spring.cloud.autoconfigure.implementation.properties.utils.AzureGlobalPropertiesUtils;
+import com.azure.spring.cloud.core.customizer.AzureServiceClientBuilderCustomizer;
+import com.azure.spring.cloud.core.implementation.util.AzureSpringIdentifier;
+import com.azure.spring.cloud.service.implementation.appconfiguration.ConfigurationClientBuilderFactory;
+import com.azure.spring.cloud.service.implementation.keyvault.secrets.SecretClientBuilderFactory;
+
+class AzureAppConfigurationBootstrapRegistrar {
+
+ static void register(ConfigDataLocationResolverContext context, Binder binder,
+ AppConfigurationProperties properties, ReplicaLookUp replicaLookup) {
+
+ AzureGlobalProperties globalProperties = binder
+ .bind(AzureGlobalProperties.PREFIX, Bindable.of(AzureGlobalProperties.class))
+ .orElseGet(AzureGlobalProperties::new);
+ AzureAppConfigurationProperties appConfigurationProperties = binder
+ .bind(AzureAppConfigurationProperties.PREFIX, Bindable.of(AzureAppConfigurationProperties.class))
+ .orElseGet(AzureAppConfigurationProperties::new);
+ // the properties are used to custom the ConfigurationClientBuilder
+ AzureAppConfigurationProperties loadedProperties = AzureGlobalPropertiesUtils.loadProperties(globalProperties,
+ appConfigurationProperties);
+
+ boolean isCredentialConfigured = isCredentialConfigured(loadedProperties);
+
+ AppConfigurationKeyVaultClientFactory keyVaultClientFactory = appConfigurationKeyVaultClientFactory(context,
+ binder, isCredentialConfigured);
+ AppConfigurationReplicaClientsBuilder replicaClientsBuilder = replicaClientBuilder(context,
+ keyVaultClientFactory, loadedProperties, isCredentialConfigured);
+
+ context.getBootstrapContext().registerIfAbsent(AppConfigurationKeyVaultClientFactory.class,
+ InstanceSupplier.from(() -> keyVaultClientFactory));
+ context.getBootstrapContext().registerIfAbsent(AppConfigurationReplicaClientFactory.class,
+ InstanceSupplier.from(() -> buildClientFactory(replicaClientsBuilder, properties, replicaLookup)));
+ }
+
+ private static AppConfigurationKeyVaultClientFactory appConfigurationKeyVaultClientFactory(
+ ConfigDataLocationResolverContext context, Binder binder, boolean isCredentialConfigured)
+ throws IllegalArgumentException {
+
+ SecretClientCustomizer customizer = context.getBootstrapContext().getOrElse(SecretClientCustomizer.class, null);
+ KeyVaultSecretProvider secretProvider = context.getBootstrapContext().getOrElse(KeyVaultSecretProvider.class,
+ null);
+
+ AzureKeyVaultSecretProperties secretClientProperties = binder
+ .bind(AzureKeyVaultSecretProperties.PREFIX, Bindable.of(AzureKeyVaultSecretProperties.class))
+ .orElseGet(AzureKeyVaultSecretProperties::new);
+ SecretClientBuilderFactory secretClientBuilderFactory = new SecretClientBuilderFactory(secretClientProperties);
+
+ context.getBootstrapContext().registerIfAbsent(SecretClientBuilderFactory.class,
+ InstanceSupplier.from(() -> secretClientBuilderFactory));
+
+ return new AppConfigurationKeyVaultClientFactory(customizer, secretProvider, secretClientBuilderFactory,
+ isCredentialConfigured);
+ }
+
+ private static AppConfigurationReplicaClientFactory buildClientFactory(
+ AppConfigurationReplicaClientsBuilder clientBuilder, AppConfigurationProperties properties,
+ ReplicaLookUp replicaLookup) {
+ return new AppConfigurationReplicaClientFactory(clientBuilder, properties.getStores(), replicaLookup);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static AppConfigurationReplicaClientsBuilder replicaClientBuilder(ConfigDataLocationResolverContext context,
+ AppConfigurationKeyVaultClientFactory keyVaultClientFactory, AzureAppConfigurationProperties properties,
+ boolean isCredentialConfigured) {
+
+ InstanceSupplier> customizer = context
+ .getBootstrapContext()
+ .getRegisteredInstanceSupplier(
+ (Class>) (Class>) AzureServiceClientBuilderCustomizer.class);
+ ConfigurationClientBuilderFactory clientFactory = context.getBootstrapContext()
+ .getOrElseSupply(ConfigurationClientBuilderFactory.class, () -> {
+ ConfigurationClientBuilderFactory factory = new ConfigurationClientBuilderFactory(properties);
+ factory.setSpringIdentifier(AzureSpringIdentifier.AZURE_SPRING_APP_CONFIG);
+ if (customizer != null) {
+ factory.addBuilderCustomizer(customizer.get(context.getBootstrapContext()));
+ }
+ return factory;
+ });
+ if (customizer != null) {
+ clientFactory.addBuilderCustomizer(customizer.get(context.getBootstrapContext()));
+ }
+
+ InstanceSupplier configurationClientCustomizer = context
+ .getBootstrapContext()
+ .getRegisteredInstanceSupplier(
+ (Class) (Class>) ConfigurationClientCustomizer.class);
+
+ ConfigurationClientCustomizer clientCustomizer = null;
+ if (configurationClientCustomizer != null) {
+ clientCustomizer = configurationClientCustomizer.get(context.getBootstrapContext());
+ }
+
+ return new AppConfigurationReplicaClientsBuilder(clientFactory, clientCustomizer, isCredentialConfigured,
+ keyVaultClientFactory.isConfigured());
+ }
+
+ private static boolean isCredentialConfigured(AbstractAzureHttpConfigurationProperties properties) {
+ if (properties.getCredential() != null) {
+ TokenCredentialConfigurationProperties tokenProps = properties.getCredential();
+ if (StringUtils.hasText(tokenProps.getClientCertificatePassword())) {
+ return true;
+ } else if (StringUtils.hasText(tokenProps.getClientCertificatePath())) {
+ return true;
+ } else if (StringUtils.hasText(tokenProps.getClientId())) {
+ return true;
+ } else if (StringUtils.hasText(tokenProps.getClientSecret())) {
+ return true;
+ } else if (StringUtils.hasText(tokenProps.getUsername())) {
+ return true;
+ } else if (StringUtils.hasText(tokenProps.getPassword())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+}
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
new file mode 100644
index 000000000000..9386a2bab524
--- /dev/null
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java
@@ -0,0 +1,219 @@
+// 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.PUSH_REFRESH;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.logging.Log;
+import org.springframework.boot.BootstrapRegistry.InstanceSupplier;
+import org.springframework.boot.context.config.ConfigData;
+import org.springframework.boot.context.config.ConfigDataLoader;
+import org.springframework.boot.context.config.ConfigDataLoaderContext;
+import org.springframework.boot.context.config.ConfigDataResourceNotFoundException;
+import org.springframework.boot.logging.DeferredLog;
+import org.springframework.boot.logging.DeferredLogFactory;
+import org.springframework.core.env.EnumerablePropertySource;
+import org.springframework.util.StringUtils;
+
+import com.azure.core.util.Context;
+import com.azure.data.appconfiguration.models.ConfigurationSetting;
+import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationKeyValueSelector;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring.PushNotification;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagKeyValueSelector;
+
+public class AzureAppConfigDataLoader implements ConfigDataLoader {
+
+ private static Log logger = new DeferredLog();
+
+ private AzureAppConfigDataResource resource;
+
+ private AppConfigurationReplicaClientFactory replicaClientFactory;
+
+ private AppConfigurationKeyVaultClientFactory keyVaultClientFactory;
+
+ private StateHolder storeState = new StateHolder();
+
+ private FeatureFlagClient featureFlagClient;
+
+ private Context requestContext;
+
+ private static final Instant START_DATE = Instant.now();
+
+ private static final Integer PREKILL_TIME = 5;
+
+ public AzureAppConfigDataLoader(DeferredLogFactory logFactory) {
+ logger = logFactory.getLog(getClass());
+ }
+
+ @Override
+ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResource resource)
+ throws IOException, ConfigDataResourceNotFoundException {
+ this.resource = resource;
+ storeState.setNextForcedRefresh(resource.getRefreshInterval());
+
+ if (context.getBootstrapContext().isRegistered(FeatureFlagClient.class)) {
+ featureFlagClient = context.getBootstrapContext().get(FeatureFlagClient.class);
+ } else {
+ featureFlagClient = new FeatureFlagClient();
+ context.getBootstrapContext().registerIfAbsent(FeatureFlagClient.class,
+ InstanceSupplier.from(() -> featureFlagClient));
+ }
+ // Reset telemetry usage for refresh
+ featureFlagClient.resetTelemetry();
+
+ List> sourceList = new ArrayList<>();
+
+ if (resource.isConfigStoreEnabled()) {
+ replicaClientFactory = context.getBootstrapContext()
+ .get(AppConfigurationReplicaClientFactory.class);
+ keyVaultClientFactory = context.getBootstrapContext()
+ .get(AppConfigurationKeyVaultClientFactory.class);
+
+ List clients = replicaClientFactory
+ .getAvailableClients(resource.getEndpoint(), true);
+
+ boolean reloadFailed = false;
+
+ boolean pushRefresh = false;
+ PushNotification notification = resource.getMonitoring().getPushNotification();
+ if ((notification.getPrimaryToken() != null
+ && StringUtils.hasText(notification.getPrimaryToken().getName()))
+ || (notification.getSecondaryToken() != null
+ && StringUtils.hasText(notification.getPrimaryToken().getName()))) {
+ pushRefresh = true;
+ }
+ requestContext = new Context("refresh", resource.isRefresh()).addData(PUSH_REFRESH,
+ pushRefresh);
+
+ // Feature Management needs to be set in the last config store.
+
+ for (AppConfigurationReplicaClient client : clients) {
+ sourceList = new ArrayList<>();
+
+ if (reloadFailed
+ && !AppConfigurationRefreshUtil.refreshStoreCheck(client,
+ replicaClientFactory.findOriginForEndpoint(client.getEndpoint()), requestContext)) {
+ // This store doesn't have any changes where to refresh store did. Skipping Checking next.
+ continue;
+ }
+
+ // Reverse in order to add Profile specific properties earlier, and last profile comes first
+ try {
+ sourceList.addAll(createSettings(client));
+ List featureFlags = createFeatureFlags(client);
+
+ logger.debug("PropertySource context.");
+ AppConfigurationStoreMonitoring monitoring = resource.getMonitoring();
+
+ storeState.setStateFeatureFlag(resource.getEndpoint(), featureFlags,
+ monitoring.getFeatureFlagRefreshInterval());
+
+ if (monitoring.isEnabled()) {
+ // Setting new ETag values for Watch
+ List watchKeysSettings = monitoring.getTriggers().stream()
+ .map(trigger -> client.getWatchKey(trigger.getKey(), trigger.getLabel(),
+ requestContext))
+ .toList();
+
+ storeState.setState(resource.getEndpoint(), watchKeysSettings, monitoring.getRefreshInterval());
+ }
+ storeState.setLoadState(resource.getEndpoint(), true);
+ } catch (AppConfigurationStatusException e) {
+ reloadFailed = true;
+ replicaClientFactory.backoffClientClient(resource.getEndpoint(), client.getEndpoint());
+ } catch (Exception e) {
+ failedToGeneratePropertySource(e);
+
+ // Not a retiable error
+ break;
+ }
+ if (sourceList.size() > 0) {
+ break;
+ }
+ }
+ }
+
+ StateHolder.updateState(storeState);
+ sourceList.add(new AppConfigurationFeatureManagementPropertySource(featureFlagClient));
+ return new ConfigData(sourceList);
+ }
+
+ private void failedToGeneratePropertySource(Exception e) {
+ logger.error("Fail fast is set and there was an error reading configuration from Azure App "
+ + "Configuration store " + resource.getEndpoint() + ".");
+ delayException();
+ throw new RuntimeException("Failed to generate property sources for " + resource.getEndpoint(), e);
+ }
+
+ /**
+ * Creates a new set of AppConfigurationPropertySources, 1 per Label.
+ *
+ * @param client client for connecting to App Configuration
+ * @return a list of AppConfigurationPropertySources
+ * @throws Exception creating a property source failed
+ */
+ private List createSettings(AppConfigurationReplicaClient client)
+ throws Exception {
+ List sourceList = new ArrayList<>();
+ List selects = resource.getSelects();
+ List profiles = resource.getProfiles().getActive();
+
+ for (AppConfigurationKeyValueSelector selectedKeys : selects) {
+ AppConfigurationPropertySource propertySource = null;
+
+ if (StringUtils.hasText(selectedKeys.getSnapshotName())) {
+ propertySource = new AppConfigurationSnapshotPropertySource(
+ selectedKeys.getSnapshotName() + "/" + resource.getEndpoint(), client, keyVaultClientFactory,
+ selectedKeys.getSnapshotName(), featureFlagClient);
+ } else {
+ propertySource = new AppConfigurationApplicationSettingPropertySource(
+ selectedKeys.getKeyFilter() + resource.getEndpoint() + "/", client, keyVaultClientFactory,
+ selectedKeys.getKeyFilter(), selectedKeys.getLabelFilter(profiles));
+ }
+ propertySource.initProperties(resource.getTrimKeyPrefix(), requestContext);
+ sourceList.add(propertySource);
+ }
+ return sourceList;
+ }
+
+ /**
+ * Creates a new set of AppConfigurationPropertySources, 1 per Label.
+ *
+ * @param client client for connecting to App Configuration
+ * @return a list of AppConfigurationPropertySources
+ * @throws Exception creating a property source failed
+ */
+ private List createFeatureFlags(AppConfigurationReplicaClient client)
+ throws Exception {
+ List featureFlagWatchKeys = new ArrayList<>();
+ List profiles = resource.getProfiles().getActive();
+
+ for (FeatureFlagKeyValueSelector selectedKeys : resource.getFeatureFlagSelects()) {
+ List storesFeatureFlags = featureFlagClient.loadFeatureFlags(client,
+ selectedKeys.getKeyFilter(), selectedKeys.getLabelFilter(profiles), requestContext);
+ featureFlagWatchKeys.addAll(storesFeatureFlags);
+ }
+
+ return featureFlagWatchKeys;
+ }
+
+ private void delayException() {
+ Instant currentDate = Instant.now();
+ Instant preKillTIme = START_DATE.plusSeconds(PREKILL_TIME);
+ if (currentDate.isBefore(preKillTIme)) {
+ long diffInMillies = Math.abs(preKillTIme.toEpochMilli() - currentDate.toEpochMilli());
+ try {
+ Thread.sleep(diffInMillies);
+ } catch (InterruptedException e) {
+ logger.error("Failed to wait before fast fail.");
+ }
+ }
+ }
+}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLocationResolver.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLocationResolver.java
new file mode 100644
index 000000000000..8c5d1cabd38e
--- /dev/null
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLocationResolver.java
@@ -0,0 +1,107 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.azure.spring.cloud.appconfiguration.config.implementation;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.naming.NamingException;
+
+import org.apache.commons.logging.Log;
+import org.springframework.boot.BootstrapRegistry.InstanceSupplier;
+import org.springframework.boot.context.config.ConfigDataLocation;
+import org.springframework.boot.context.config.ConfigDataLocationNotFoundException;
+import org.springframework.boot.context.config.ConfigDataLocationResolver;
+import org.springframework.boot.context.config.ConfigDataLocationResolverContext;
+import org.springframework.boot.context.config.ConfigDataResourceNotFoundException;
+import org.springframework.boot.context.config.Profiles;
+import org.springframework.boot.context.properties.bind.BindHandler;
+import org.springframework.boot.context.properties.bind.Bindable;
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.boot.logging.DeferredLog;
+import org.springframework.util.StringUtils;
+
+import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProperties;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore;
+
+public class AzureAppConfigDataLocationResolver
+ implements ConfigDataLocationResolver {
+
+ private static final Log LOGGER = new DeferredLog();
+
+ public static final String PREFIX = "azureAppConfiguration";
+
+ private static final AtomicBoolean START_UP = new AtomicBoolean(true);
+
+ @Override
+ public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) {
+ if (!location.hasPrefix(PREFIX)) {
+ return false;
+ }
+ Boolean hasEndpoint = StringUtils.hasText(context.getBinder()
+ .bind(AppConfigurationProperties.CONFIG_PREFIX + ".stores[0].endpoint", String.class)
+ .orElse(""));
+ Boolean hasConnectionString = StringUtils.hasText(context.getBinder()
+ .bind(AppConfigurationProperties.CONFIG_PREFIX + ".stores[0].connection-string", String.class)
+ .orElse(""));
+ Boolean hasEndpoints = StringUtils.hasText(context.getBinder()
+ .bind(AppConfigurationProperties.CONFIG_PREFIX + ".stores[0].endpoints", String.class)
+ .orElse(""));
+ Boolean hasConnectionStrings = StringUtils.hasText(context.getBinder()
+ .bind(AppConfigurationProperties.CONFIG_PREFIX + ".stores[0].connection-strings", String.class)
+ .orElse(""));
+
+ return (hasEndpoint || hasConnectionString || hasEndpoints || hasConnectionStrings);
+ }
+
+ @Override
+ public List resolve(ConfigDataLocationResolverContext context,
+ ConfigDataLocation location)
+ throws ConfigDataLocationNotFoundException, ConfigDataResourceNotFoundException {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List resolveProfileSpecific(
+ ConfigDataLocationResolverContext resolverContext, ConfigDataLocation location, Profiles profiles)
+ throws ConfigDataLocationNotFoundException {
+
+ AppConfigurationProperties properties = loadProperties(resolverContext);
+ List locations = new ArrayList<>();
+
+ for (ConfigStore store : properties.getStores()) {
+ locations.add(
+ new AzureAppConfigDataResource(store, profiles, START_UP.get(), properties.getRefreshInterval()));
+ }
+ START_UP.set(false);
+ return locations;
+ }
+
+ protected AppConfigurationProperties loadProperties(ConfigDataLocationResolverContext context) {
+ Binder binder = context.getBinder();
+ BindHandler bindHandler = getBindHandler(context);
+ AppConfigurationProperties properties = binder.bind(AppConfigurationProperties.CONFIG_PREFIX,
+ Bindable.of(AppConfigurationProperties.class), bindHandler).get();
+
+ properties.validateAndInit();
+ ReplicaLookUp replicaLookup = null;
+ try {
+ replicaLookup = new ReplicaLookUp(properties);
+ context.getBootstrapContext().registerIfAbsent(ReplicaLookUp.class, InstanceSupplier.of(replicaLookup));
+ } catch (NamingException e) {
+ LOGGER.info("Failed to find DNS Entry for config store while looking for replicas.");
+ }
+
+ AzureAppConfigurationBootstrapRegistrar.register(context, binder, properties, replicaLookup);
+
+ return properties;
+ }
+
+ private BindHandler getBindHandler(ConfigDataLocationResolverContext context) {
+ return context.getBootstrapContext().getOrElse(BindHandler.class, null);
+ }
+
+}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataResource.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataResource.java
new file mode 100644
index 000000000000..3f31ee7fd926
--- /dev/null
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataResource.java
@@ -0,0 +1,140 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.azure.spring.cloud.appconfiguration.config.implementation;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.boot.context.config.ConfigDataResource;
+import org.springframework.boot.context.config.Profiles;
+
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationKeyValueSelector;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagKeyValueSelector;
+
+public class AzureAppConfigDataResource extends ConfigDataResource {
+
+ private final boolean configStoreEnabled;
+
+ private final String endpoint;
+
+ private List trimKeyPrefix;
+
+ private final Profiles profiles;
+
+ private List selects = new ArrayList<>();
+
+ private List featureFlagSelects = new ArrayList<>();
+
+ private final AppConfigurationStoreMonitoring monitoring;
+
+ private final boolean isRefresh;
+
+ private Duration refreshInterval;
+
+ AzureAppConfigDataResource(ConfigStore configStore, Profiles profiles, boolean isRefresh,
+ Duration refreshInterval) {
+ this.configStoreEnabled = configStore.isEnabled();
+ this.endpoint = configStore.getEndpoint();
+ this.selects = configStore.getSelects();
+ this.featureFlagSelects = configStore.getFeatureFlags().getSelects();
+ this.trimKeyPrefix = configStore.getTrimKeyPrefix();
+ this.monitoring = configStore.getMonitoring();
+ this.profiles = profiles;
+ this.isRefresh = isRefresh;
+ this.refreshInterval = refreshInterval;
+ }
+
+ /**
+ * @return the selects
+ */
+ public List getSelects() {
+ return selects;
+ }
+
+ /**
+ * @param selects the selects to set
+ */
+ public void setSelects(List selects) {
+ this.selects = selects;
+ }
+
+ /**
+ * @return the selects for feature flags
+ */
+ public List getFeatureFlagSelects() {
+ return featureFlagSelects;
+ }
+
+ /**
+ * @param featureFlagSelects the selects to set
+ */
+ public void setFeatureFlagSelects(List featureFlagSelects) {
+ this.featureFlagSelects = featureFlagSelects;
+ }
+
+ /**
+ * @return the configStoreEnabled
+ */
+ public boolean isConfigStoreEnabled() {
+ return configStoreEnabled;
+ }
+
+ /**
+ * @return the endpoint
+ */
+ public String getEndpoint() {
+ return endpoint;
+ }
+
+ /**
+ * @return the monitoring
+ */
+ public AppConfigurationStoreMonitoring getMonitoring() {
+ return monitoring;
+ }
+
+ /**
+ * @return the trimKeyPrefix
+ */
+ public List getTrimKeyPrefix() {
+ return trimKeyPrefix;
+ }
+
+ /**
+ * @param trimKeyPrefix the trimKeyPrefix to set
+ */
+ public void setTrimKeyPrefix(List trimKeyPrefix) {
+ this.trimKeyPrefix = trimKeyPrefix;
+ }
+
+ /**
+ * @return the profiles
+ */
+ public Profiles getProfiles() {
+ return profiles;
+ }
+
+ /**
+ * @return the isRefresh
+ */
+ public boolean isRefresh() {
+ return isRefresh;
+ }
+
+ /**
+ * @return the refreshInterval
+ */
+ public Duration getRefreshInterval() {
+ return refreshInterval;
+ }
+
+ /**
+ * @param refreshInterval the refreshInterval to set
+ */
+ public void setRefreshInterval(Duration refreshInterval) {
+ this.refreshInterval = refreshInterval;
+ }
+}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/BackoffTimeCalculator.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/BackoffTimeCalculator.java
index afc647f1b31e..073691bd9477 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/BackoffTimeCalculator.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/BackoffTimeCalculator.java
@@ -19,16 +19,6 @@ final class BackoffTimeCalculator {
private static Long minBackoff = (long) 30;
- /**
- *
- * @param maxBackoff maximum amount of time between requests
- * @param minBackoff minimum amount of time between requests
- */
- static void setDefaults(Long maxBackoff, Long minBackoff) {
- BackoffTimeCalculator.maxBackoff = maxBackoff;
- BackoffTimeCalculator.minBackoff = minBackoff;
- }
-
/**
* Calculates the new Backoff time for requests.
* @param attempts Number of attempts so far
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManager.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManager.java
index 16c6d2d05fdc..17124ea72904 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManager.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManager.java
@@ -7,7 +7,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -21,7 +20,7 @@
/**
* Holds a set of connections to an app configuration store with zero to many geo-replications.
*/
-public class ConnectionManager {
+class ConnectionManager {
private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionManager.class);
@@ -143,13 +142,6 @@ List getAvailableClients(Boolean useCurrent) {
return availableClients;
}
- List getAllEndpoints() {
- List endpoints = clients.stream().map(AppConfigurationReplicaClient::getEndpoint)
- .collect(Collectors.toList());
- endpoints.addAll(replicaLookUp.getAutoFailoverEndpoints(configStore.getEndpoint()));
- return endpoints;
- }
-
/**
* Call when the current client failed
* @param endpoint replica endpoint
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClient.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClient.java
index 956454d616ca..31892d0a4e18 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClient.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClient.java
@@ -6,7 +6,6 @@
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.DEFAULT_REQUIREMENT_TYPE;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.E_TAG;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_CONTENT_TYPE;
-import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_ID;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_PREFIX;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_REFERENCE;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.REQUIREMENT_TYPE_SERVICE;
@@ -14,41 +13,51 @@
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.TELEMETRY;
import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Base64;
import java.util.Collections;
+import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
-import org.bouncycastle.jcajce.provider.digest.SHA256;
import org.springframework.stereotype.Component;
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.FeatureFlagFilter;
import com.azure.data.appconfiguration.models.SettingSelector;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.entity.Feature;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.entity.FeatureTelemetry;
+import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.FeatureFlagTracing;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
-import com.nimbusds.jose.util.Base64URL;
/**
* Loads sets of feature flags, and de-duplicates the results with previously loaded feature flags. Newer Feature Flags
* take priority.
*/
@Component
-public class FeatureFlagClient {
+class FeatureFlagClient {
- protected final Map properties = new LinkedHashMap<>();
+ private final Map properties = new LinkedHashMap<>();
private static final ObjectMapper CASE_INSENSITIVE_MAPPER = JsonMapper.builder()
.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build();
+
+ private FeatureFlagTracing tracing = new FeatureFlagTracing();
/**
*
@@ -62,8 +71,8 @@ public class FeatureFlagClient {
*
*
*/
- public List loadFeatureFlags(AppConfigurationReplicaClient replicaClient, String customKeyFilter,
- String[] labelFilter, boolean isRefresh) {
+ List loadFeatureFlags(AppConfigurationReplicaClient replicaClient, String customKeyFilter,
+ String[] labelFilter, Context context) {
List loadedFeatureFlags = new ArrayList<>();
String keyFilter = SELECT_ALL_FEATURE_FLAGS;
@@ -77,14 +86,15 @@ public List loadFeatureFlags(AppConfigurationReplicaClient replica
for (String label : labels) {
SettingSelector settingSelector = new SettingSelector().setKeyFilter(keyFilter).setLabelFilter(label);
+ context.addData("FeatureFlagTracing", tracing);
- FeatureFlags features = replicaClient.listFeatureFlags(settingSelector, isRefresh);
- loadedFeatureFlags.addAll(proccessFeatureFlags(features, keyFilter));
+ FeatureFlags features = replicaClient.listFeatureFlags(settingSelector, context);
+ loadedFeatureFlags.addAll(proccessFeatureFlags(features, replicaClient.getOriginClient()));
}
return loadedFeatureFlags;
}
- public List proccessFeatureFlags(FeatureFlags features, String endpoint) {
+ List proccessFeatureFlags(FeatureFlags features, String endpoint) {
List loadedFeatureFlags = new ArrayList<>();
loadedFeatureFlags.add(features);
@@ -93,6 +103,7 @@ public List proccessFeatureFlags(FeatureFlags features, String end
if (setting instanceof FeatureFlagConfigurationSetting
&& FEATURE_FLAG_CONTENT_TYPE.equals(setting.getContentType())) {
FeatureFlagConfigurationSetting featureFlag = (FeatureFlagConfigurationSetting) setting;
+ updateTelemetry(featureFlag);
properties.put(featureFlag.getKey(), createFeature(featureFlag, endpoint));
}
}
@@ -107,7 +118,8 @@ public List proccessFeatureFlags(FeatureFlags features, String end
*/
protected static Feature createFeature(FeatureFlagConfigurationSetting item, String originEndpoint) {
String requirementType = DEFAULT_REQUIREMENT_TYPE;
- FeatureTelemetry featureTelemetry = new FeatureTelemetry();
+ FeatureTelemetry featureTelemetry = null;
+ Feature feature = null;
try {
JsonNode node = CASE_INSENSITIVE_MAPPER.readTree(item.getValue());
JsonNode conditions = node.get(CONDITIONS);
@@ -115,54 +127,154 @@ protected static Feature createFeature(FeatureFlagConfigurationSetting item, Str
requirementType = conditions.get(REQUIREMENT_TYPE_SERVICE).asText();
}
JsonNode telemetryNode = node.get(TELEMETRY);
- if (telemetryNode != null) {
+ if (telemetryNode != null && !telemetryNode.isEmpty()) {
ObjectMapper objectMapper = JsonMapper.builder()
.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build();
featureTelemetry = objectMapper.convertValue(telemetryNode, FeatureTelemetry.class);
}
- } catch (JsonProcessingException e) {
- }
+ feature = new Feature(item, requirementType, featureTelemetry);
- Feature feature = new Feature(item, requirementType, featureTelemetry);
-
- if (feature.getTelemetry() != null) {
- final FeatureTelemetry telemetry = feature.getTelemetry();
- if (telemetry.isEnabled()) {
- final Map originMetadata = telemetry.getMetadata();
- originMetadata.put(FEATURE_FLAG_ID, calculateFeatureFlagId(item.getKey(), item.getLabel()));
- originMetadata.put(E_TAG, item.getETag());
- if (originEndpoint != null && !originEndpoint.isEmpty()) {
- final String labelPart = item.getLabel().isEmpty() ? ""
- : String.format("?label=%s", item.getLabel());
- originMetadata.put(FEATURE_FLAG_REFERENCE,
- String.format("%s/kv/%s%s", originEndpoint, item.getKey(), labelPart));
+ if (feature.getTelemetry() != null) {
+ final FeatureTelemetry telemetry = feature.getTelemetry();
+ if (telemetry.isEnabled()) {
+ final Map originMetadata = telemetry.getMetadata();
+ originMetadata.put(E_TAG, item.getETag());
+ if (originEndpoint != null && !originEndpoint.isEmpty()) {
+ final String labelPart = item.getLabel().isEmpty() ? ""
+ : String.format("?label=%s", item.getLabel());
+ originMetadata.put(FEATURE_FLAG_REFERENCE,
+ String.format("%s/kv/%s%s", originEndpoint, item.getKey(), labelPart));
+ }
+ originMetadata.put("AllocationId", generateAllocationId(node));
}
}
+ } catch (JsonProcessingException e) {
+
}
return feature;
}
/**
- * @param key the key of feature flag
- * @param label the label of feature flag. If label is whitespace, treat as null
- * @return base64_url(SHA256(utf8_bytes("${key}\n${label}"))).replace('+', '-').replace('/', '_').trimEnd('=')
- * trimEnd() means trims everything after the first occurrence of the '='
+ * @return the properties
*/
- private static String calculateFeatureFlagId(String key, String label) {
- final String data = String.format("%s\n%s", key, label.isEmpty() ? null : label);
- final SHA256.Digest digest = new SHA256.Digest();
- final String beforeTrim = Base64URL.encode(digest.digest(data.getBytes(StandardCharsets.UTF_8)))
- .toString().replace('+', '-').replace('/', '_');
- final int index = beforeTrim.indexOf('=');
- return beforeTrim.substring(0, index > -1 ? index : beforeTrim.length());
+ public List getFeatureFlags() {
+ return properties.values().stream().toList();
}
-
+
+ public void resetTelemetry() {
+ tracing.resetFeatureFilterTelemetry();
+ }
+
/**
- * @return the properties
+ * 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.
*/
- public Map getProperties() {
- return properties;
+ private void updateTelemetry(FeatureFlagConfigurationSetting featureFlag) {
+ for (FeatureFlagFilter filter : featureFlag.getClientFilters()) {
+ tracing.updateFeatureFilterTelemetry(filter.getName());
+ }
}
+ /**
+ * Generates a unique allocation ID for the feature flag based on its configuration.
+ *
+ * @param featureFlagValue The feature flag value as a map.
+ * @return A unique allocation ID or null if the allocation is not valid.
+ */
+ static String generateAllocationId(JsonNode featureFlagValue) {
+ StringBuilder allocationId = new StringBuilder();
+ List allocatedVariants = new ArrayList<>();
+
+ // Retrieve allocation object
+ JsonNode allocation = featureFlagValue.get("allocation");
+ if (allocation == null) {
+ return null;
+ }
+
+ // Seed
+ allocationId.append("seed=").append(allocation.has("seed") ? allocation.get("seed").asText() : "");
+
+ // DefaultWhenEnabled
+ if (allocation.has("default_when_enabled")) {
+ allocatedVariants.add(allocation.get("default_when_enabled").asText());
+ }
+ allocationId.append("\ndefault_when_enabled=").append(allocation.has("default_when_enabled") ? allocation.get("default_when_enabled").asText() : "");
+
+ // Percentile
+ allocationId.append("\npercentiles=");
+ JsonNode percentile = allocation.get("percentile");
+ List percentileAllocations = new ArrayList<>();
+ if (percentile != null && percentile.isArray()) {
+ percentile.forEach(p -> {
+ if (!Objects.equals(p.get("from").asText(), p.get("to").asText())) {
+ percentileAllocations.add(p);
+ }
+ });
+ percentileAllocations.sort(Comparator.comparing(p -> p.get("from").asInt()));
+ }
+
+ for (JsonNode percentileAllocation : percentileAllocations) {
+ if (percentileAllocation.has("variant")) {
+ allocatedVariants.add(percentileAllocation.get("variant").asText());
+ }
+ }
+
+ allocationId.append(percentileAllocations.stream()
+ .map(pa -> pa.get("from") + ","
+ + Base64.getEncoder().encodeToString(pa.get("variant").asText().getBytes(StandardCharsets.UTF_8)) + ","
+ + pa.get("to"))
+ .collect(Collectors.joining(";")));
+
+ if (allocatedVariants.isEmpty() && (allocation.get("seed") == null)) {
+ return null;
+ }
+
+ // Variants
+ allocationId.append("\nvariants=");
+ List