From 8dd4aaed48c49ec4c54f1d98a4e00084a3400297 Mon Sep 17 00:00:00 2001 From: Matthew Metcalf Date: Mon, 16 Dec 2024 11:20:59 -0800 Subject: [PATCH 01/41] Spring Feature Management - RemoveV1Schema (#43405) * RemoveV1Schema * Removed targeting change * extra java doc removed * Review comments * Update FeatureManagementException.java --- .../feature/management/FeatureManager.java | 67 ++----- .../implementation/FeatureFilterUtils.java | 8 +- .../FeatureManagementProperties.java | 185 +----------------- .../implementation/models/Feature.java | 87 -------- .../targeting/TargetingFilterSettings.java | 26 --- .../models/Conditions.java | 31 +-- .../Feature.java} | 19 +- .../FeatureFilterEvaluationContext.java | 18 +- ...ntSideFeatureManagementPropertiesTest.java | 44 ----- .../FeatureManagementConfigurationTest.java | 38 ++++ .../FeatureManagementTestConfigurations.java | 23 +++ .../management/FeatureManagerTest.java | 143 +++++--------- ...deFeatureManagementPropertiesListTest.java | 47 ----- ...erSideFeatureManagementPropertiesTest.java | 48 ----- .../filters/TargetingFilterTest.java | 2 +- .../implementation/TestConfiguration.java | 18 -- .../src/test/resources/application.yaml | 16 +- 17 files changed, 190 insertions(+), 630 deletions(-) delete mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/Feature.java delete mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/TargetingFilterSettings.java rename sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/{implementation => }/models/Conditions.java (56%) rename sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/{implementation/models/ServerSideFeature.java => models/Feature.java} (77%) delete mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/ClientSideFeatureManagementPropertiesTest.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagementConfigurationTest.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagementTestConfigurations.java delete mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/ServerSideFeatureManagementPropertiesListTest.java delete mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/ServerSideFeatureManagementPropertiesTest.java delete mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implementation/TestConfiguration.java diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java index 04be3548066f..9574a65703e0 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java @@ -3,7 +3,6 @@ package com.azure.spring.cloud.feature.management; import java.util.HashSet; -import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Stream; @@ -17,7 +16,7 @@ import com.azure.spring.cloud.feature.management.filters.FeatureFilter; import com.azure.spring.cloud.feature.management.implementation.FeatureManagementConfigProperties; import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties; -import com.azure.spring.cloud.feature.management.implementation.models.Feature; +import com.azure.spring.cloud.feature.management.models.Feature; import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext; import com.azure.spring.cloud.feature.management.models.FilterNotFoundException; @@ -51,9 +50,9 @@ public class FeatureManager { } /** - * Checks to see if the feature is enabled. If enabled it check each filter, once a single filter - * returns true it returns true. If no filter returns true, it returns false. If there are no - * filters, it returns true. If feature isn't found it returns false. + * Checks to see if the feature is enabled. If enabled it check each filter, once a single filter returns true it + * returns true. If no filter returns true, it returns false. If there are no filters, it returns true. If feature + * isn't found it returns false. * * @param feature Feature being checked. * @return state of the feature @@ -64,9 +63,9 @@ public Mono isEnabledAsync(String feature) { } /** - * Checks to see if the feature is enabled. If enabled it check each filter, once a single filter - * returns true it returns true. If no filter returns true, it returns false. If there are no - * filters, it returns true. If feature isn't found it returns false. + * Checks to see if the feature is enabled. If enabled it check each filter, once a single filter returns true it + * returns true. If no filter returns true, it returns false. If there are no filters, it returns true. If feature + * isn't found it returns false. * * @param feature Feature being checked. * @return state of the feature @@ -76,34 +75,28 @@ public Boolean isEnabled(String feature) throws FilterNotFoundException { return checkFeature(feature); } - private boolean checkFeature(String feature) throws FilterNotFoundException { - if (featureManagementConfigurations.getFeatureManagement() == null - || featureManagementConfigurations.getOnOff() == null) { - return false; - } - - Boolean boolFeature = featureManagementConfigurations.getOnOff().get(feature); - - if (boolFeature != null) { - return boolFeature; - } + private boolean checkFeature(String featureName) throws FilterNotFoundException { + Feature featureFlag = featureManagementConfigurations.getFeatureFlags().stream() + .filter(feature -> feature.getId().equals(featureName)).findAny().orElse(null); - Feature featureItem = featureManagementConfigurations.getFeatureManagement().get(feature); - - if (featureItem == null || !featureItem.getEvaluate()) { + if (featureFlag == null) { return false; } - Stream filters = featureItem.getEnabledFor().values().stream() + Stream filters = featureFlag.getConditions().getClientFilters().stream() .filter(Objects::nonNull).filter(featureFilter -> featureFilter.getName() != null); + + if (featureFlag.getConditions().getClientFilters().size() == 0) { + return featureFlag.isEnabled(); + } // All Filters must be true - if (featureItem.getRequirementType().equals("All")) { - return filters.allMatch(featureFilter -> isFeatureOn(featureFilter, feature)); + if (featureFlag.getConditions().getRequirementType().equals("All")) { + return filters.allMatch(featureFilter -> isFeatureOn(featureFilter, featureName)); } // Any Filter must be true - return filters.anyMatch(featureFilter -> isFeatureOn(featureFilter, feature)); + return filters.anyMatch(featureFilter -> isFeatureOn(featureFilter, featureName)); } private boolean isFeatureOn(FeatureFilterEvaluationContext filter, String feature) { @@ -129,25 +122,7 @@ private boolean isFeatureOn(FeatureFilterEvaluationContext filter, String featur * @return a set of all feature names */ public Set getAllFeatureNames() { - Set allFeatures = new HashSet<>(); - - allFeatures.addAll(featureManagementConfigurations.getOnOff().keySet()); - allFeatures.addAll(featureManagementConfigurations.getFeatureManagement().keySet()); - return allFeatures; + return new HashSet( + featureManagementConfigurations.getFeatureFlags().stream().map(feature -> feature.getId()).toList()); } - - /** - * @return the featureManagement - */ - Map getFeatureManagement() { - return featureManagementConfigurations.getFeatureManagement(); - } - - /** - * @return the onOff - */ - Map getOnOff() { - return featureManagementConfigurations.getOnOff(); - } - } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java index e95dc6a53404..a8631bfc9366 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java @@ -3,12 +3,13 @@ package com.azure.spring.cloud.feature.management.implementation; -import org.springframework.util.StringUtils; - import java.util.Collection; import java.util.Map; +import org.springframework.util.StringUtils; + public class FeatureFilterUtils { + /** * Looks at the given key in the parameters and coverts it to a list if it is currently a map. * @@ -21,6 +22,8 @@ public static void updateValueFromMapToList(Map parameters, Stri if (objectMap instanceof Map) { Collection toType = ((Map) objectMap).values(); parameters.put(key, toType); + } else if (objectMap != null) { + parameters.put(key, objectMap); } } @@ -30,4 +33,5 @@ public static String getKeyCase(Map parameters, String key) { } return StringUtils.uncapitalize(key); } + } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementProperties.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementProperties.java index 4273edfb8ffa..c9a1da8a834a 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementProperties.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementProperties.java @@ -2,195 +2,28 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.management.implementation; -import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.boot.context.properties.ConfigurationProperties; -import com.azure.spring.cloud.feature.management.implementation.models.Feature; -import com.azure.spring.cloud.feature.management.implementation.models.ServerSideFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.azure.spring.cloud.feature.management.models.Feature; +import com.fasterxml.jackson.annotation.JsonProperty; /** * Configuration Properties for Feature Management. Processes the configurations to be usable by Feature Management. */ @ConfigurationProperties(prefix = "feature-management") -public class FeatureManagementProperties extends HashMap { +public class FeatureManagementProperties { - private static final Logger LOGGER = LoggerFactory.getLogger(FeatureManagementProperties.class); + @JsonProperty("feature-flags") + private List featureFlags; - private static final ObjectMapper MAPPER = new ObjectMapper() - .setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE); - - private static final long serialVersionUID = -1642032123104805346L; - - private static final String FEATURE_FLAG_SNAKE_CASE = "feature_flags"; - - /** - * Map of all Feature Flags that use Feature Filters. - */ - private transient Map featureManagement; - - /** - * Map of all Feature Flags that are just enabled/disabled. - */ - private Map onOff; - - public FeatureManagementProperties() { - featureManagement = new HashMap<>(); - onOff = new HashMap<>(); - } - - @Override - public void putAll(Map m) { - if (m == null) { - return; - } - // Need to reset or switch between on/off to conditional doesn't work - featureManagement = new HashMap<>(); - onOff = new HashMap<>(); - - // try to parse the properties by server side schema as default - tryServerSideSchema(m); - - if (featureManagement.isEmpty() && onOff.isEmpty()) { - tryClientSideSchema(m); - } - } - - @SuppressWarnings({"unchecked", "deprecation"}) - private void tryServerSideSchema(Map features) { - if (features.keySet().isEmpty()) { - return; - } - - // check if FeatureFlags section exist - String featureFlagsSectionKey = ""; - for (String key : features.keySet()) { - if (FEATURE_FLAG_SNAKE_CASE.equalsIgnoreCase(key)) { - featureFlagsSectionKey = key; - break; - } - } - if (featureFlagsSectionKey.isEmpty()) { - return; - } - - // get FeatureFlags section and parse - final Object featureFlagsObject = features.get(featureFlagsSectionKey); - if (Map.class.isAssignableFrom(featureFlagsObject.getClass())) { - final Map featureFlagsSection = (Map) featureFlagsObject; - for (String key : featureFlagsSection.keySet()) { - addServerSideFeature(featureFlagsSection, key); - } - } else { - if (List.class.isAssignableFrom(featureFlagsObject.getClass())) { - final List featureFlagsSection = (List) featureFlagsObject; - for (Object flag : featureFlagsSection) { - addServerSideFeature((Map) flag, null); - } - } - } - } - - private void tryClientSideSchema(Map features) { - for (String key : features.keySet()) { - addFeature(features, key, ""); - } - } - - @SuppressWarnings("unchecked") - private void addFeature(Map features, String key, String combined) { - Object featureValue = features.get(key); - if (!combined.isEmpty() && !combined.endsWith(".")) { - combined += "."; - } - if (featureValue instanceof Boolean) { - onOff.put(combined + key, (Boolean) featureValue); - } else { - Feature feature = null; - try { - feature = MAPPER.convertValue(featureValue, Feature.class); - } catch (IllegalArgumentException e) { - LOGGER.error("Found invalid feature {} with value {}.", combined + key, featureValue.toString()); - } - // When coming from a file "feature.flag" is not a possible flag name - if (feature != null && feature.getEnabledFor() == null && feature.getKey() == null) { - if (Map.class.isAssignableFrom(featureValue.getClass())) { - features = (Map) featureValue; - for (String fKey : features.keySet()) { - addFeature(features, fKey, combined + key); - } - } - } else { - if (feature != null) { - feature.setKey(key); - featureManagement.put(key, feature); - } - } - } - } - - @SuppressWarnings("unchecked") - private void addServerSideFeature(Map features, String key) { - Object featureValue = null; - if (key != null) { - featureValue = features.get(key); - } else { - featureValue = features; - } - - ServerSideFeature serverSideFeature = null; - try { - LinkedHashMap ff = new LinkedHashMap<>(); - if (featureValue.getClass().isAssignableFrom(LinkedHashMap.class)) { - ff = (LinkedHashMap) featureValue; - } - LinkedHashMap conditions = new LinkedHashMap<>(); - if (ff.containsKey("conditions") - && ff.get("conditions").getClass().isAssignableFrom(LinkedHashMap.class)) { - conditions = (LinkedHashMap) ff.get("conditions"); - } - FeatureFilterUtils.updateValueFromMapToList(conditions, "client_filters"); - - serverSideFeature = MAPPER.convertValue(featureValue, ServerSideFeature.class); - } catch (IllegalArgumentException e) { - LOGGER.error("Found invalid feature {} with value {}.", key, featureValue.toString()); - } - - if (serverSideFeature != null && serverSideFeature.getId() != null) { - if (serverSideFeature.getConditions() != null - && serverSideFeature.getConditions().getClientFilters() != null - && serverSideFeature.getConditions().getClientFilters().size() > 0) { - final Feature feature = new Feature(); - feature.setKey(serverSideFeature.getId()); - feature.setEvaluate(serverSideFeature.isEnabled()); - feature.setEnabledFor(serverSideFeature.getConditions().getClientFiltersAsMap()); - feature.setRequirementType(serverSideFeature.getConditions().getRequirementType()); - featureManagement.put(serverSideFeature.getId(), feature); - } else { - onOff.put(serverSideFeature.getId(), serverSideFeature.isEnabled()); - } - } - } - - /** - * @return the featureManagement - */ - public Map getFeatureManagement() { - return featureManagement; + public List getFeatureFlags() { + return featureFlags; } - /** - * @return the onOff - */ - public Map getOnOff() { - return onOff; + public void setFeatureFlags(List featureFlags) { + this.featureFlags = featureFlags; } } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/Feature.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/Feature.java deleted file mode 100644 index 85f0f12841ea..000000000000 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/Feature.java +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.feature.management.implementation.models; - -import static com.azure.spring.cloud.feature.management.implementation.FeatureManagementConstants.DEFAULT_REQUIREMENT_TYPE; - -import java.util.Map; - -import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * App Configuration Feature defines the feature name and a Map of FeatureFilterEvaluationContexts. - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class Feature { - - @JsonProperty("key") - private String key; - - @JsonProperty("evaluate") - private Boolean evaluate = true; - - @JsonProperty("enabled-for") - private Map enabledFor; - - @JsonProperty("requirement-type") - private String requirementType = DEFAULT_REQUIREMENT_TYPE; - - /** - * @return the key - */ - public String getKey() { - return key; - } - - /** - * @param key the key to set - */ - public void setKey(String key) { - this.key = key; - } - - /** - * @return the evaluate - */ - public Boolean getEvaluate() { - return evaluate; - } - - /** - * @param evaluate the evaluate to set - */ - public void setEvaluate(Boolean evaluate) { - this.evaluate = evaluate; - } - - /** - * @return the enabledFor - */ - public Map getEnabledFor() { - return enabledFor; - } - - /** - * @param enabledFor the enabledFor to set - */ - public void setEnabledFor(Map enabledFor) { - this.enabledFor = enabledFor; - } - - /** - * @return the requirementType - */ - public String getRequirementType() { - return requirementType; - } - - /** - * @param requirementType the requirementType to set - */ - public void setRequirementType(String requirementType) { - this.requirementType = requirementType; - } - -} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/TargetingFilterSettings.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/TargetingFilterSettings.java deleted file mode 100644 index 88ad5aae1304..000000000000 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/TargetingFilterSettings.java +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.feature.management.implementation.targeting; - -/** - * The settings that are used to configure the TargetingFilter feature filter. - */ -public class TargetingFilterSettings { - - private Audience audience; - - /** - * @return the audience - */ - public Audience getAudience() { - return audience; - } - - /** - * @param audience the audience to set - */ - public void setAudience(Audience audience) { - this.audience = audience; - } - -} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/Conditions.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Conditions.java similarity index 56% rename from sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/Conditions.java rename to sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Conditions.java index b8632677d0d0..a6504e0f219d 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/Conditions.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Conditions.java @@ -1,22 +1,20 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.feature.management.implementation.models; +package com.azure.spring.cloud.feature.management.models; -import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; +import static com.azure.spring.cloud.feature.management.implementation.FeatureManagementConstants.DEFAULT_REQUIREMENT_TYPE; -import java.util.HashMap; +import java.util.ArrayList; import java.util.List; -import java.util.Map; -import static com.azure.spring.cloud.feature.management.implementation.FeatureManagementConstants.DEFAULT_REQUIREMENT_TYPE; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; @JsonIgnoreProperties(ignoreUnknown = true) public class Conditions { @JsonProperty("client_filters") - private List clientFilters; + private List clientFilters = new ArrayList<>(); @JsonProperty("requirement_type") private String requirementType = DEFAULT_REQUIREMENT_TYPE; @@ -31,8 +29,9 @@ public String getRequirementType() { /** * @param requirementType the requirementType to set */ - public void setRequirementType(String requirementType) { + public Conditions setRequirementType(String requirementType) { this.requirementType = requirementType; + return this; } /** @@ -42,22 +41,12 @@ public List getClientFilters() { return clientFilters; } - /** - * @return the clientFilters in Map format using index of list as key - */ - public Map getClientFiltersAsMap() { - final Map clientFiltersMap = new HashMap<>(); - for (int i = 0; i < clientFilters.size(); i++) { - clientFiltersMap.put(i, clientFilters.get(i)); - } - return clientFiltersMap; - } - /** * @param clientFilters the clientFilters to set */ - public void setClientFilters(List clientFilters) { + public Conditions setClientFilters(List clientFilters) { this.clientFilters = clientFilters; + return this; } } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/ServerSideFeature.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java similarity index 77% rename from sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/ServerSideFeature.java rename to sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java index 3b3213ee804c..de00de138176 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/ServerSideFeature.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.feature.management.implementation.models; +package com.azure.spring.cloud.feature.management.models; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @@ -10,7 +10,7 @@ * App Configuration Feature defines the feature name and a Map of FeatureFilterEvaluationContexts. */ @JsonIgnoreProperties(ignoreUnknown = true) -public class ServerSideFeature { +public class Feature { @JsonProperty("id") private String id; @@ -21,7 +21,7 @@ public class ServerSideFeature { private boolean enabled; @JsonProperty("conditions") - private Conditions conditions; + private Conditions conditions = new Conditions(); /** * @return the id @@ -33,8 +33,9 @@ public String getId() { /** * @param id the id to set */ - public void setId(String id) { + public Feature setId(String id) { this.id = id; + return this; } /** @@ -47,8 +48,9 @@ public boolean isEnabled() { /** * @param enabled the enabled to set */ - public void setEnabled(boolean enabled) { + public Feature setEnabled(boolean enabled) { this.enabled = enabled; + return this; } /** @@ -61,8 +63,9 @@ public String getDescription() { /** * @param description the description to set * */ - public void setDescription(String description) { + public Feature setDescription(String description) { this.description = description; + return this; } /** @@ -75,7 +78,9 @@ public Conditions getConditions() { /** * @param conditions the conditions to set * */ - public void setConditions(Conditions conditions) { + public Feature setConditions(Conditions conditions) { this.conditions = conditions; + return this; } + } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FeatureFilterEvaluationContext.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FeatureFilterEvaluationContext.java index 449c21901dd3..483d95a0cdcc 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FeatureFilterEvaluationContext.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FeatureFilterEvaluationContext.java @@ -2,12 +2,12 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.management.models; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.HashMap; import java.util.Map; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + /** * Context passed into Feature Filters used for evaluation. */ @@ -38,9 +38,11 @@ public String getName() { /** * Set the name * @param name the name to set + * @return FeatureFilterEvaluationContext */ - public void setName(String name) { + public FeatureFilterEvaluationContext setName(String name) { this.name = name; + return this; } /** @@ -58,9 +60,11 @@ public Map getParameters() { /** * Set the parameters * @param parameters the parameters to set + * @return FeatureFilterEvaluationContext */ - public void setParameters(Map parameters) { + public FeatureFilterEvaluationContext setParameters(Map parameters) { this.parameters = parameters; + return this; } /** @@ -74,9 +78,11 @@ public String getFeatureName() { /** * Set the featureName * @param featureName the featureName to set + * @return FeatureFilterEvaluationContext */ - public void setFeatureName(String featureName) { + public FeatureFilterEvaluationContext setFeatureName(String featureName) { this.featureName = featureName; + return this; } } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/ClientSideFeatureManagementPropertiesTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/ClientSideFeatureManagementPropertiesTest.java deleted file mode 100644 index fa0bd49a0457..000000000000 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/ClientSideFeatureManagementPropertiesTest.java +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.spring.cloud.feature.management; - -import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties; -import com.azure.spring.cloud.feature.management.implementation.models.Feature; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@ExtendWith(SpringExtension.class) -@EnableConfigurationProperties(value = FeatureManagementProperties.class) -@SpringBootTest(classes = { SpringBootTest.class }) -@ActiveProfiles("client") -public class ClientSideFeatureManagementPropertiesTest { - @Autowired - private FeatureManagementProperties clientSideProperties; - - @Test - void onOffMapTest() { - assertTrue(clientSideProperties.getOnOff().get("gamma")); - } - - @Test - void featureManagementTest() { - final Feature alphaFeatureItem = clientSideProperties.getFeatureManagement().get("alpha"); - assertEquals(alphaFeatureItem.getKey(), "alpha"); - assertEquals(alphaFeatureItem.getEnabledFor().size(), 1); - assertEquals(alphaFeatureItem.getEnabledFor().get(0).getName(), "randomFilter"); - - final Feature betaFeatureItem = clientSideProperties.getFeatureManagement().get("beta"); - assertEquals(betaFeatureItem.getKey(), "beta"); - assertEquals(betaFeatureItem.getEnabledFor().size(), 1); - assertEquals(betaFeatureItem.getEnabledFor().get(0).getName(), "timeWindowFilter"); - } -} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagementConfigurationTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagementConfigurationTest.java new file mode 100644 index 000000000000..5bb4dc631560 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagementConfigurationTest.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.management; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import com.azure.spring.cloud.feature.management.targeting.TargetingEvaluationOptions; + +public class FeatureManagementConfigurationTest { + + private static final ApplicationContextRunner CONTEXT_RUNNER = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(FeatureManagementConfiguration.class)); + + private static final ApplicationContextRunner CONTEXT_RUNNER_OPTIONS = new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(FeatureManagementTestConfigurations.class, FeatureManagementConfiguration.class)); + + @Test + public void featureManagementTest() { + CONTEXT_RUNNER.run(context -> { + assertThat(context).hasSingleBean(FeatureManager.class); + assertThat(context).doesNotHaveBean(TargetingEvaluationOptions.class); + }); + } + + @Test + public void featureManagementWithEvaluationOptionsTest() { + CONTEXT_RUNNER_OPTIONS.run(context -> { + assertThat(context).hasSingleBean(TargetingEvaluationOptions.class); + assertThat(context).hasSingleBean(FeatureManager.class); + }); + } + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagementTestConfigurations.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagementTestConfigurations.java new file mode 100644 index 000000000000..6fc172aab6c9 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagementTestConfigurations.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.management; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.azure.spring.cloud.feature.management.implementation.FeatureManagementConfigProperties; +import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties; +import com.azure.spring.cloud.feature.management.targeting.TargetingEvaluationOptions; + +@Configuration +@EnableConfigurationProperties({ FeatureManagementConfigProperties.class, FeatureManagementProperties.class }) +class FeatureManagementTestConfigurations { + + @Bean + public TargetingEvaluationOptions targetingEvaluationOptions() { + TargetingEvaluationOptions options = new TargetingEvaluationOptions(); + options.setIgnoreCase(true); + return options; + } +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java index 396be785dc14..75000092a2a3 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java @@ -14,7 +14,6 @@ import java.util.List; import java.util.concurrent.ExecutionException; -import com.azure.spring.cloud.feature.management.filters.TimeWindowFilter; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,17 +25,18 @@ import org.springframework.context.ApplicationContext; import com.azure.spring.cloud.feature.management.filters.FeatureFilter; +import com.azure.spring.cloud.feature.management.filters.TimeWindowFilter; import com.azure.spring.cloud.feature.management.implementation.FeatureManagementConfigProperties; import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties; -import com.azure.spring.cloud.feature.management.implementation.TestConfiguration; -import com.azure.spring.cloud.feature.management.implementation.models.Feature; +import com.azure.spring.cloud.feature.management.models.Conditions; +import com.azure.spring.cloud.feature.management.models.Feature; import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext; import com.azure.spring.cloud.feature.management.models.FilterNotFoundException; /** * Unit tests for FeatureManager. */ -@SpringBootTest(classes = { TestConfiguration.class, SpringBootTest.class }) +@SpringBootTest(classes = { FeatureManagementTestConfigurations.class, SpringBootTest.class }) public class FeatureManagerTest { private FeatureManager featureManager; @@ -66,56 +66,42 @@ public void cleanup() throws Exception { @Test public void isEnabledFeatureNotFound() { assertFalse(featureManager.isEnabledAsync("Non Existed Feature").block()); - verify(featureManagementPropertiesMock, times(2)).getOnOff(); - verify(featureManagementPropertiesMock, times(2)).getFeatureManagement(); + verify(featureManagementPropertiesMock, times(1)).getFeatureFlags(); } @Test public void isEnabledFeatureOff() { - HashMap features = new HashMap<>(); - features.put("Off", false); - when(featureManagementPropertiesMock.getOnOff()).thenReturn(features); + List features = List.of(new Feature().setId("Off").setEnabled(false)); + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); assertFalse(featureManager.isEnabledAsync("Off").block()); - verify(featureManagementPropertiesMock, times(2)).getOnOff(); - verify(featureManagementPropertiesMock, times(1)).getFeatureManagement(); + verify(featureManagementPropertiesMock, times(1)).getFeatureFlags(); } @Test public void isEnabledOnBoolean() throws InterruptedException, ExecutionException, FilterNotFoundException { - HashMap features = new HashMap<>(); - features.put("On", true); - when(featureManagementPropertiesMock.getOnOff()).thenReturn(features); + List features = List.of(new Feature().setId("On").setEnabled(true)); + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + assertTrue(featureManager.isEnabled("On")); assertTrue(featureManager.isEnabledAsync("On").block()); - verify(featureManagementPropertiesMock, times(2)).getOnOff(); - verify(featureManagementPropertiesMock, times(1)).getFeatureManagement(); + verify(featureManagementPropertiesMock, times(2)).getFeatureFlags(); } @Test public void isEnabledFeatureHasNoFilters() { - HashMap features = new HashMap<>(); - Feature noFilters = new Feature(); - noFilters.setKey("NoFilters"); - noFilters.setEnabledFor(new HashMap()); - features.put("NoFilters", noFilters); - when(featureManagementPropertiesMock.getFeatureManagement()).thenReturn(features); + List features = List.of(new Feature().setId("NoFilters").setEnabled(false) + .setConditions(new Conditions().setClientFilters(List.of()))); + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); assertFalse(featureManager.isEnabledAsync("NoFilters").block()); } @Test - public void isEnabledON() throws InterruptedException, ExecutionException, FilterNotFoundException { - HashMap features = new HashMap<>(); - Feature onFeature = new Feature(); - onFeature.setKey("On"); - HashMap filters = new HashMap(); - FeatureFilterEvaluationContext alwaysOn = new FeatureFilterEvaluationContext(); - alwaysOn.setName("AlwaysOn"); - filters.put(0, alwaysOn); - onFeature.setEnabledFor(filters); - features.put("On", onFeature); - when(featureManagementPropertiesMock.getFeatureManagement()).thenReturn(features); + public void isEnabledOn() throws InterruptedException, ExecutionException, FilterNotFoundException { + List features = List.of(new Feature().setId("On").setEnabled(true).setConditions( + new Conditions().setClientFilters(List.of(new FeatureFilterEvaluationContext().setName("AlwaysOn"))))); + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); when(context.getBean(Mockito.matches("AlwaysOn"))).thenReturn(new AlwaysOnFilter()); @@ -124,16 +110,10 @@ public void isEnabledON() throws InterruptedException, ExecutionException, Filte @Test public void noFilter() throws FilterNotFoundException { - HashMap features = new HashMap<>(); - Feature onFeature = new Feature(); - onFeature.setKey("Off"); - HashMap filters = new HashMap(); - FeatureFilterEvaluationContext alwaysOn = new FeatureFilterEvaluationContext(); - alwaysOn.setName("AlwaysOff"); - filters.put(0, alwaysOn); - onFeature.setEnabledFor(filters); - features.put("Off", onFeature); - when(featureManagementPropertiesMock.getFeatureManagement()).thenReturn(features); + List features = List.of(new Feature().setId("Off").setEnabled(true).setConditions( + new Conditions().setClientFilters(List.of(new FeatureFilterEvaluationContext().setName("AlwaysOff"))))); + + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); when(context.getBean(Mockito.matches("AlwaysOff"))).thenThrow(new NoSuchBeanDefinitionException("")); @@ -144,18 +124,12 @@ public void noFilter() throws FilterNotFoundException { @Test public void allOn() { - HashMap features = new HashMap<>(); - Feature onFeature = new Feature(); - onFeature.setKey("On"); - HashMap filters = new HashMap(); - FeatureFilterEvaluationContext alwaysOn = new FeatureFilterEvaluationContext(); - alwaysOn.setName("AlwaysOn"); - filters.put(0, alwaysOn); - filters.put(1, alwaysOn); - onFeature.setEnabledFor(filters); - onFeature.setRequirementType("All"); - features.put("On", onFeature); - when(featureManagementPropertiesMock.getFeatureManagement()).thenReturn(features); + List features = List.of(new Feature().setId("On").setEnabled(true) + .setConditions(new Conditions().setRequirementType("All") + .setClientFilters(List.of(new FeatureFilterEvaluationContext().setName("AlwaysOn"), + new FeatureFilterEvaluationContext().setName("AlwaysOn"))))); + + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); when(context.getBean(Mockito.matches("AlwaysOn"))).thenReturn(new AlwaysOnFilter()) .thenReturn(new AlwaysOnFilter()); @@ -165,57 +139,43 @@ public void allOn() { @Test public void oneOffAny() { - HashMap features = new HashMap<>(); - Feature onFeature = new Feature(); - onFeature.setKey("On"); - HashMap filters = new HashMap(); - FeatureFilterEvaluationContext alwaysOn = new FeatureFilterEvaluationContext(); - alwaysOn.setName("AlwaysOn"); - filters.put(0, alwaysOn); - filters.put(1, alwaysOn); - onFeature.setEnabledFor(filters); - onFeature.setRequirementType("Any"); - features.put("On", onFeature); - when(featureManagementPropertiesMock.getFeatureManagement()).thenReturn(features); + List features = List.of(new Feature().setId("On").setEnabled(true) + .setConditions(new Conditions().setRequirementType("Any") + .setClientFilters(List.of(new FeatureFilterEvaluationContext().setName("AlwaysOn"), + new FeatureFilterEvaluationContext().setName("AlwaysOff"))))); - when(context.getBean(Mockito.matches("AlwaysOn"))).thenReturn(new AlwaysOnFilter()) - .thenReturn(new AlwaysOffFilter()); + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + + when(context.getBean(Mockito.matches("AlwaysOn"))).thenReturn(new AlwaysOnFilter()); + when(context.getBean(Mockito.matches("AlwaysOff"))).thenReturn(new AlwaysOffFilter()); assertTrue(featureManager.isEnabledAsync("On").block()); } @Test public void oneOffAll() { - HashMap features = new HashMap<>(); - Feature onFeature = new Feature(); - onFeature.setKey("On"); - HashMap filters = new HashMap(); - FeatureFilterEvaluationContext alwaysOn = new FeatureFilterEvaluationContext(); - alwaysOn.setName("AlwaysOn"); - filters.put(0, alwaysOn); - filters.put(1, alwaysOn); - onFeature.setEnabledFor(filters); - onFeature.setRequirementType("All"); - features.put("On", onFeature); - when(featureManagementPropertiesMock.getFeatureManagement()).thenReturn(features); + List features = List.of(new Feature().setId("On").setEnabled(true) + .setConditions(new Conditions().setRequirementType("All") + .setClientFilters(List.of(new FeatureFilterEvaluationContext().setName("AlwaysOn"), + new FeatureFilterEvaluationContext().setName("AlwaysOffFilter"))))); - when(context.getBean(Mockito.matches("AlwaysOn"))).thenReturn(new AlwaysOnFilter()) - .thenReturn(new AlwaysOffFilter()); + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + + when(context.getBean(Mockito.matches("AlwaysOn"))).thenReturn(new AlwaysOnFilter()); + when(context.getBean(Mockito.matches("AlwaysOff"))).thenReturn(new AlwaysOffFilter()); assertFalse(featureManager.isEnabledAsync("On").block()); } @Test public void timeWindowFilter() { - final HashMap features = new HashMap<>(); - final HashMap filters = new HashMap(); - final HashMap parameters = new HashMap<>(); parameters.put("Start", "Sun, 14 Jan 2024 00:00:00 GMT"); parameters.put("End", "Mon, 15 Jan 2024 00:00:00 GMT"); final HashMap pattern = new HashMap<>(); pattern.put("Type", "Weekly"); - pattern.put("DaysOfWeek", List.of("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday")); + pattern.put("DaysOfWeek", + List.of("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday")); final HashMap range = new HashMap<>(); range.put("Type", "NoEnd"); final HashMap recurrence = new HashMap<>(); @@ -226,13 +186,12 @@ public void timeWindowFilter() { final FeatureFilterEvaluationContext weeklyAlwaysOn = new FeatureFilterEvaluationContext(); weeklyAlwaysOn.setName("TimeWindowFilter"); weeklyAlwaysOn.setParameters(parameters); - filters.put(0, weeklyAlwaysOn); - final Feature weeklyAlwaysOnFeature = new Feature(); - weeklyAlwaysOnFeature.setEnabledFor(filters); - features.put("Alpha", weeklyAlwaysOnFeature); + List features = List + .of(new Feature().setId("Alpha").setEnabled(true) + .setConditions(new Conditions().setClientFilters(List.of(weeklyAlwaysOn)))); - when(featureManagementPropertiesMock.getFeatureManagement()).thenReturn(features); + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); when(context.getBean(Mockito.matches("TimeWindowFilter"))).thenReturn(new TimeWindowFilter()); assertTrue(featureManager.isEnabled("Alpha")); diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/ServerSideFeatureManagementPropertiesListTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/ServerSideFeatureManagementPropertiesListTest.java deleted file mode 100644 index e4e63a6ef239..000000000000 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/ServerSideFeatureManagementPropertiesListTest.java +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.spring.cloud.feature.management; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import org.junit.jupiter.api.Test; - -import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties; - -public class ServerSideFeatureManagementPropertiesListTest { - - private FeatureManagementProperties serverSideProperties; - - @Test - void featureManagementTest() { - serverSideProperties = new FeatureManagementProperties(); - Map> test = new LinkedHashMap<>(); - Map alpha = new LinkedHashMap<>(); - alpha.put("id", "Alpha"); - alpha.put("enabled", true); - - Map randomFilter = new LinkedHashMap<>(); - randomFilter.put("name", "Microsoft.Random"); - randomFilter.put("parameters", Map.of("Value", 50)); - - Map clientFilters = new LinkedHashMap<>(); - clientFilters.put("0", randomFilter); - Map conditions = new LinkedHashMap<>(); - conditions.put("client_filters", clientFilters); - Map beta = new LinkedHashMap<>(); - beta.put("id", "Beta"); - beta.put("enabled", true); - beta.put("conditions", conditions); - - test.put("feature_flags", List.of(alpha, beta)); - serverSideProperties.putAll(test); - assertEquals(1, serverSideProperties.getOnOff().size()); - assertEquals(1, serverSideProperties.getFeatureManagement().size()); - } - -} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/ServerSideFeatureManagementPropertiesTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/ServerSideFeatureManagementPropertiesTest.java deleted file mode 100644 index 67d1555e3b90..000000000000 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/ServerSideFeatureManagementPropertiesTest.java +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.spring.cloud.feature.management; - -import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties; -import com.azure.spring.cloud.feature.management.implementation.models.Feature; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.boot.test.context.SpringBootTest; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@ExtendWith(SpringExtension.class) -@EnableConfigurationProperties(value = FeatureManagementProperties.class) -@SpringBootTest(classes = { SpringBootTest.class }) -@ActiveProfiles("server") -public class ServerSideFeatureManagementPropertiesTest { - @Autowired - private FeatureManagementProperties serverSideProperties; - - @Test - void onOffMapTest() { - assertTrue(serverSideProperties.getOnOff().get("Gamma")); - } - - @Test - void featureManagementTest() { - final Feature alphaFeatureItem = serverSideProperties.getFeatureManagement().get("Alpha"); - assertEquals(alphaFeatureItem.getKey(), "Alpha"); - assertEquals(alphaFeatureItem.getEnabledFor().size(), 1); - assertEquals(alphaFeatureItem.getEnabledFor().get(0).getName(), "Microsoft.Random"); - - final Feature betaFeatureItem = serverSideProperties.getFeatureManagement().get("Beta"); - assertEquals(betaFeatureItem.getKey(), "Beta"); - assertEquals(betaFeatureItem.getEnabledFor().size(), 1); - assertEquals(betaFeatureItem.getEnabledFor().get(0).getName(), "Microsoft.TimeWindowFilter"); - - final Boolean deltaFeatureItem = serverSideProperties.getOnOff().get("Delta"); - assertTrue(deltaFeatureItem); - } - -} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java index 19c06b3ce4c7..939b05815307 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java @@ -14,8 +14,8 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; -import com.azure.spring.cloud.feature.management.implementation.TestConfiguration; import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext; import com.azure.spring.cloud.feature.management.models.TargetingException; import com.azure.spring.cloud.feature.management.targeting.TargetingContext; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implementation/TestConfiguration.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implementation/TestConfiguration.java deleted file mode 100644 index 9f306b0e695c..000000000000 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implementation/TestConfiguration.java +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.feature.management.implementation; - - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -@ConfigurationProperties -public class TestConfiguration { - - @Bean - public FeatureManagementConfigProperties properties() { - return new FeatureManagementConfigProperties(); - } -} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/resources/application.yaml b/sdk/spring/spring-cloud-azure-feature-management/src/test/resources/application.yaml index a6fda6647d79..3e5ffd598893 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/resources/application.yaml +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/resources/application.yaml @@ -1,10 +1,8 @@ feature-management: - featureSet: - features: - FeatureU: false - FeatureV: - enabledFor: - - name: Random - parameters: - chance: "50" - - name: ClientFilter + FeatureU: false + FeatureV: + enabledFor: + - name: Random + parameters: + chance: "50" + - name: ClientFilter \ No newline at end of file From c7a559aaeee4d6612587e116dcd55cfcfa24f4a0 Mon Sep 17 00:00:00 2001 From: Matthew Metcalf Date: Thu, 23 Jan 2025 14:57:15 -0800 Subject: [PATCH 02/41] New Data loader (#43802) * UpdatedDataLoader * Review Comments * Web Updates * Update ValidationsTest.java * Removing Validation tests. They will be updated in there own PR. * Review comments * Fixing Refresh * Update AzureAppConfigDataLocationResolver.java --- .../AppConfigurationWebAutoConfiguration.java | 6 + .../AppConfigurationEventListener.java | 3 +- ...pConfigurationBusRefreshEventListener.java | 3 +- .../AppConfigurationRefreshEndpoint.java | 8 +- .../AppConfigurationRefreshEventListener.java | 3 +- ...ConfigurationWebAutoConfigurationTest.java | 100 --- .../pom.xml | 17 +- ...pConfigurationWatchAutoConfiguration.java} | 42 +- ...ationApplicationSettingPropertySource.java | 6 +- .../AppConfigurationConstants.java | 35 +- ...AppConfigurationKeyVaultClientFactory.java | 6 +- .../AppConfigurationPropertySource.java | 4 - ...AppConfigurationPropertySourceLocator.java | 304 ---------- .../AppConfigurationRefreshUtil.java | 16 +- .../AppConfigurationReplicaClient.java | 27 +- .../AppConfigurationReplicaClientFactory.java | 19 +- ...AppConfigurationReplicaClientsBuilder.java | 87 +-- ...ppConfigurationSnapshotPropertySource.java | 5 +- .../AzureAppConfigBoostrapRegistrar.java | 136 +++++ .../AzureAppConfigDataLoader.java | 199 ++++++ .../AzureAppConfigDataLocationResolver.java | 129 ++++ .../AzureAppConfigDataResource.java | 152 +++++ .../implementation/ConnectionManager.java | 10 +- .../implementation/FeatureFlagClient.java | 25 +- .../config/implementation/NormalizeNull.java | 4 +- .../config/implementation/StateHolder.java | 9 +- .../autofailover/SRVRecord.java | 6 +- ...ppConfigurationBootstrapConfiguration.java | 199 ------ .../implementation/feature/FeatureFlags.java | 17 - .../feature/entity/Conditions.java | 2 +- .../FeatureFilterEvaluationContext.java | 62 -- .../feature/entity/FeatureSet.java | 42 -- .../policy/BaseAppConfigurationPolicy.java | 5 +- .../http/policy/FeatureFlagTracing.java | 8 +- .../http/policy/TracingInfo.java | 15 +- .../AppConfigurationKeyValueSelector.java | 4 +- .../AppConfigurationStoreMonitoring.java | 2 +- .../AppConfigurationStoreTrigger.java | 2 +- .../properties/ConfigStore.java | 23 - .../FeatureFlagKeyValueSelector.java | 2 +- .../properties/FeatureFlagStore.java | 2 +- .../main/resources/META-INF/spring.factories | 6 +- ...ot.autoconfigure.AutoConfiguration.imports | 3 +- .../resources/appConfiguration.properties | 6 - ...tionSettingPropertySourceSnapshotTest.java | 12 +- ...nfigurationPropertySourceKeyVaultTest.java | 5 +- ...onfigurationPropertySourceLocatorTest.java | 568 ------------------ .../AppConfigurationRefreshUtilTest.java | 79 +-- ...ConfigurationReplicaClientBuilderTest.java | 40 +- ...ConfigurationReplicaClientFactoryTest.java | 10 - .../AppConfigurationReplicaClientTest.java | 83 ++- .../implementation/ConnectionManagerTest.java | 9 - .../implementation/FeatureFlagClientTest.java | 20 +- .../implementation/StateHolderTest.java | 2 +- .../config/implementation/TestConstants.java | 16 + ...nfigurationBootstrapConfigurationTest.java | 56 -- .../BaseAppConfigurationPolicyTest.java | 69 +-- .../http/policy/TracingInfoTest.java | 21 +- .../AppConfigurationPropertiesTest.java | 106 ++-- .../feature/management/ValidationsTest.java | 174 ------ 60 files changed, 941 insertions(+), 2090 deletions(-) delete mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/AppConfigurationWebAutoConfigurationTest.java rename sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/{AppConfigurationAutoConfiguration.java => AppConfigurationWatchAutoConfiguration.java} (55%) delete mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceLocator.java create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigBoostrapRegistrar.java create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLocationResolver.java create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataResource.java delete mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/config/AppConfigurationBootstrapConfiguration.java delete mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/entity/FeatureFilterEvaluationContext.java delete mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/entity/FeatureSet.java delete mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/appConfiguration.properties delete mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceLocatorTest.java delete mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/config/AppConfigurationBootstrapConfigurationTest.java delete mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/ValidationsTest.java 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 ca003da589e4..b04d37285fc6 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.1 true - - org.springframework.cloud - spring-cloud-starter-bootstrap - 4.2.0 - org.springframework.cloud spring-cloud-context @@ -45,6 +40,11 @@ 3.4.1 compile + + jakarta.annotation + jakarta.annotation-api + 3.0.0 + com.azure @@ -177,13 +177,14 @@ - com.fasterxml.jackson.core:jackson-annotations:[2.18.2] - com.fasterxml.jackson.core:jackson-databind:[2.18.2] + jakarta.annotation:jakarta.annotation-api:[3.0.0] + com.fasterxml.jackson.core:jackson-annotations:[2.17.2] + com.fasterxml.jackson.core:jackson-databind:[2.17.2] org.springframework.boot:spring-boot-actuator:[3.4.1] org.springframework.boot:spring-boot-autoconfigure:[3.4.1] org.springframework.cloud:spring-cloud-context:[4.2.0] org.springframework.boot:spring-boot-configuration-processor:[3.4.1] - org.springframework.cloud:spring-cloud-starter-bootstrap:[4.2.0] + org.springframework.cloud:spring-cloud-starter-bootstrap:[4.1.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 55% 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..d2d7262e439e 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; @@ -20,36 +22,28 @@ /** * 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, AppConfigurationProviderProperties.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, + AppConfigurationProviderProperties appProperties, 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(), + appProperties.getDefaultMinBackoff(), 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..f7c2a6c90f8b 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 @@ -107,7 +107,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 { @@ -129,7 +129,7 @@ void handleFeatureFlag(String key, FeatureFlagConfigurationSetting setting, List handleJson(setting, trimStrings); } - 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 +138,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..2daacc4bfd41 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 */ @@ -47,16 +37,11 @@ public class AppConfigurationConstants { */ public static final String KEY_VAULT_CONFIGURED_TRACING = "UsesKeyVault"; - /** - * Constant for tracing for Replica Count - */ - public static final String REPLICA_COUNT = "ReplicaCount"; - /** * Http Header User Agent */ public static final String USER_AGENT_TYPE = "User-Agent"; - + /** * Http Header Correlation Context */ @@ -71,28 +56,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"; 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..6a27f783285f 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; @@ -26,7 +26,7 @@ public class AppConfigurationKeyVaultClientFactory { private final int timeout; - public AppConfigurationKeyVaultClientFactory(SecretClientCustomizer keyVaultClientProvider, + AppConfigurationKeyVaultClientFactory(SecretClientCustomizer keyVaultClientProvider, KeyVaultSecretProvider keyVaultSecretProvider, SecretClientBuilderFactory secretClientFactory, boolean credentialsConfigured, int timeout) { this.keyVaultClientProvider = keyVaultClientProvider; @@ -38,7 +38,7 @@ public AppConfigurationKeyVaultClientFactory(SecretClientCustomizer keyVaultClie 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)) { 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..3a3c84907e22 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; @@ -12,7 +11,6 @@ import org.springframework.core.env.EnumerablePropertySource; 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 +24,6 @@ abstract class AppConfigurationPropertySource extends EnumerablePropertySource properties = new LinkedHashMap<>(); - protected final List featureConfigurationSettings = new ArrayList<>(); - protected final AppConfigurationReplicaClient replicaClient; AppConfigurationPropertySource(String name, AppConfigurationReplicaClient replicaClient) { 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/AppConfigurationRefreshUtil.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtil.java index ca1cc33c3905..94c1b9c8550f 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 @@ -108,12 +108,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,7 +115,7 @@ 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) { RefreshEventData eventData = new RefreshEventData(); if (StateHolder.getLoadState(originEndpoint)) { refreshWithoutTime(client, StateHolder.getState(originEndpoint).getWatchKeys(), eventData); @@ -136,12 +130,12 @@ 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, + static boolean refreshStoreFeatureFlagCheck(Boolean featureStoreEnabled, AppConfigurationReplicaClient client) { RefreshEventData eventData = new RefreshEventData(); String endpoint = client.getEndpoint(); - if (featureStore.getEnabled() && StateHolder.getStateFeatureFlag(endpoint) != null) { + if (featureStoreEnabled && StateHolder.getStateFeatureFlag(endpoint) != null) { refreshWithoutTimeFeatureFlags(client, StateHolder.getStateFeatureFlag(endpoint), eventData); } else { LOGGER.debug("Skipping feature flag refresh check for " + endpoint); @@ -197,7 +191,6 @@ private static void refreshWithTimeFeatureFlags(AppConfigurationReplicaClient cl replicaLookUp.updateAutoFailoverEndpoints(); for (FeatureFlags featureFlags : state.getWatchKeys()) { - if (client.checkWatchKeys(featureFlags.getSettingSelector(), true)) { String eventDataInfo = ".appconfig.featureflag/*"; @@ -219,7 +212,6 @@ private static void refreshWithoutTimeFeatureFlags(AppConfigurationReplicaClient RefreshEventData eventData) throws AppConfigurationStatusException { for (FeatureFlags featureFlags : watchKeys.getWatchKeys()) { - if (client.checkWatchKeys(featureFlags.getSettingSelector(), true)) { String eventDataInfo = ".appconfig.featureflag/*"; @@ -273,7 +265,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..5eea371940c2 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; @@ -39,19 +38,16 @@ class AppConfigurationReplicaClient { 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, ConfigurationClient client) { this.endpoint = endpoint; this.client = client; this.backoffEndTime = Instant.now().minusMillis(1); this.failedAttempts = 0; - this.tracingInfo = tracingInfo; } /** @@ -92,7 +88,7 @@ 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, boolean isRefresh) throws HttpResponseException { try { Context context = new Context("refresh", isRefresh); @@ -115,7 +111,7 @@ 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, boolean isRefresh) throws HttpResponseException { List configurationSettings = new ArrayList<>(); try { @@ -134,7 +130,7 @@ List listSettings(SettingSelector settingSelector, Boolean } } - FeatureFlags listFeatureFlags(SettingSelector settingSelector, Boolean isRefresh) throws HttpResponseException { + FeatureFlags listFeatureFlags(SettingSelector settingSelector, boolean isRefresh) throws HttpResponseException { List configurationSettings = new ArrayList<>(); List checks = new ArrayList<>(); try { @@ -159,10 +155,13 @@ FeatureFlags listFeatureFlags(SettingSelector settingSelector, Boolean isRefresh } } - List listSettingSnapshot(String snapshotName) { + List listSettingSnapshot(String snapshotName, boolean isRefresh) { 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. + Context context = new Context("refresh", isRefresh); + 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 +177,8 @@ List listSettingSnapshot(String snapshotName) { } } - Boolean checkWatchKeys(SettingSelector settingSelector, Boolean isRefresh) { - Context context = new Context("refresh", isRefresh); + boolean checkWatchKeys(SettingSelector settingSelector, boolean isRefresh) { + Context context = new Context("refresh", false); List> results = client.listConfigurationSettings(settingSelector, context) .streamByPage().filter(pagedResponse -> pagedResponse.getStatusCode() != 304).toList(); return results.size() > 0; @@ -208,8 +207,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..4f28d5297f28 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 @@ -14,8 +14,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 +23,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 +36,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 +65,28 @@ 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; - public AppConfigurationReplicaClientsBuilder(int defaultMaxRetries, ConfigurationClientBuilderFactory clientFactory, - boolean credentialConfigured) { + AppConfigurationReplicaClientsBuilder(int defaultMaxRetries, ConfigurationClientBuilderFactory clientFactory, + ConfigurationClientCustomizer clientCustomizer, boolean credentialConfigured, boolean isKeyVaultConfigured) { this.defaultMaxRetries = defaultMaxRetries; - this.clientFactory = clientFactory; this.credentialConfigured = credentialConfigured; + this.clientFactory = clientFactory; + this.clientCustomizer = clientCustomizer; + this.isKeyVaultConfigured = isKeyVaultConfigured; } /** @@ -114,17 +111,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. * @@ -172,13 +158,11 @@ List buildClients(ConfigStore configStore) { clients.add(modifyAndBuildClient(builder, endpoint, 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); @@ -189,7 +173,7 @@ List buildClients(ConfigStore configStore) { 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()); @@ -204,10 +188,7 @@ public AppConfigurationReplicaClient buildClient(String failoverEndpoint, Config } 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); @@ -216,32 +197,22 @@ public AppConfigurationReplicaClient buildClient(String failoverEndpoint, Config private AppConfigurationReplicaClient modifyAndBuildClient(ConfigurationClientBuilder builder, String endpoint, 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); - } - return new AppConfigurationReplicaClient(endpoint, builder.buildClient(), tracingInfo); - } - - @Override - public void setEnvironment(Environment environment) { - for (String profile : environment.getActiveProfiles()) { - if ("dev".equalsIgnoreCase(profile)) { - this.isDev = true; - break; - } + if (clientCustomizer != null) { + clientCustomizer.customize(builder, endpoint); } - this.env = environment; + return new AppConfigurationReplicaClient(endpoint, builder.buildClient()); } - 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 +293,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."); @@ -358,7 +328,6 @@ private static class ConnectionString { } } - @SuppressWarnings("deprecation") protected ConnectionString setUri(String uri) { try { this.baseUri = new URL(uri); 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..853db7e8ab11 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 @@ -42,10 +42,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, boolean isRefresh) throws InvalidConfigurationPropertyValueException { + processConfigurationSettings(replicaClient.listSettingSnapshot(snapshotName, isRefresh), 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..be7d90bb5e06 --- /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,136 @@ +// 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.EnableConfigurationProperties; +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.appconfiguration.config.implementation.properties.AppConfigurationProviderProperties; +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.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; + +@EnableConfigurationProperties(AppConfigurationProviderProperties.class) +class AzureAppConfigurationBootstrapRegistrar { + + static void register(ConfigDataLocationResolverContext context, Binder binder, + AppConfigurationProperties properties, AppConfigurationProviderProperties appProperties, + 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, + isCredentialConfigured, appProperties.getMaxRetryTime()); + AppConfigurationReplicaClientsBuilder replicaClientsBuilder = replicaClientBuilder(context, binder, + keyVaultClientFactory, loadedProperties, isCredentialConfigured, appProperties.getMaxRetries()); + + 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, boolean isCredentialConfigured, Integer maxRetryTime) + throws IllegalArgumentException { + + SecretClientCustomizer customizer = context.getBootstrapContext().getOrElse(SecretClientCustomizer.class, null); + KeyVaultSecretProvider secretProvider = context.getBootstrapContext().getOrElse(KeyVaultSecretProvider.class, + null); + SecretClientBuilderFactory secretClientFactory = context.getBootstrapContext() + .getOrElse(SecretClientBuilderFactory.class, null); + + return new AppConfigurationKeyVaultClientFactory(customizer, secretProvider, secretClientFactory, + isCredentialConfigured, maxRetryTime); + } + + 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, + Binder binder, AppConfigurationKeyVaultClientFactory keyVaultClientFactory, + AzureAppConfigurationProperties properties, boolean isCredentialConfigured, Integer maxRetries) { + + 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(maxRetries, 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..72c1e03c3a3f --- /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,199 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.appconfiguration.config.implementation; + +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.context.properties.EnableConfigurationProperties; +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.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.AppConfigurationProviderProperties; +import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring; +import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagKeyValueSelector; + +@EnableConfigurationProperties(AppConfigurationProviderProperties.class) +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; + + 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)) { + this.featureFlagClient = context.getBootstrapContext().get(FeatureFlagClient.class); + } else { + this.featureFlagClient = new FeatureFlagClient(); + context.getBootstrapContext().registerIfAbsent(FeatureFlagClient.class, + InstanceSupplier.from(() -> this.featureFlagClient)); + } + + 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; + + // 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()))) { + // 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(), + resource.isRefresh())) + .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(), resource.isRefresh()); + 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), resource.isRefresh()); + featureFlagWatchKeys.addAll(storesFeatureFlags); + } + + return featureFlagWatchKeys; + } + + private void delayException() { + Instant currentDate = Instant.now(); + Instant preKillTIme = resource.getAppProperties().getStartDate() + .plusSeconds(resource.getAppProperties().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/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..77ec7057d854 --- /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,129 @@ +// 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.beans.BeanUtils; +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.EnableConfigurationProperties; +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.AppConfigurationProviderProperties; +import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore; + +@EnableConfigurationProperties(AppConfigurationProviderProperties.class) +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 { + + Holder holder = loadProperties(resolverContext); + List locations = new ArrayList<>(); + + for (ConfigStore store : holder.properties.getStores()) { + locations.add( + new AzureAppConfigDataResource(store, profiles, holder.appProperties, START_UP.get(), + holder.properties.getRefreshInterval())); + } + START_UP.set(false); + return locations; + } + + protected Holder loadProperties(ConfigDataLocationResolverContext context) { + Binder binder = context.getBinder(); + BindHandler bindHandler = getBindHandler(context); + AppConfigurationProperties properties; + AppConfigurationProviderProperties appProperties; + Holder holder = new Holder(); + + properties = binder.bind(AppConfigurationProperties.CONFIG_PREFIX, + Bindable.of(AppConfigurationProperties.class), bindHandler).get(); + + appProperties = binder.bind(AppConfigurationProviderProperties.CONFIG_PREFIX, + Bindable.of(AppConfigurationProviderProperties.class), bindHandler) + .orElseGet(AppConfigurationProviderProperties::new); + + 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, appProperties, replicaLookup); + + holder.properties = properties; + holder.appProperties = appProperties; + + return holder; + } + + private BindHandler getBindHandler(ConfigDataLocationResolverContext context) { + return context.getBootstrapContext().getOrElse(BindHandler.class, null); + } + + private class Holder { + AppConfigurationProperties properties; + + AppConfigurationProviderProperties appProperties; + } + +} 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..13f5f1a87ad8 --- /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,152 @@ +// 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.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; + +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 AppConfigurationProviderProperties appProperties; + + private final boolean isRefresh; + + private Duration refreshInterval; + + AzureAppConfigDataResource(ConfigStore configStore, Profiles profiles, + AppConfigurationProviderProperties appProperties, 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.appProperties = appProperties; + 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 appProperties + */ + public AppConfigurationProviderProperties getAppProperties() { + return appProperties; + } + + /** + * @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/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..474f0fda49f0 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 @@ -14,6 +14,8 @@ 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.Collections; @@ -21,7 +23,6 @@ import java.util.List; import java.util.Map; -import org.bouncycastle.jcajce.provider.digest.SHA256; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @@ -43,9 +44,9 @@ * 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(); @@ -62,7 +63,7 @@ public class FeatureFlagClient { *

* */ - public List loadFeatureFlags(AppConfigurationReplicaClient replicaClient, String customKeyFilter, + List loadFeatureFlags(AppConfigurationReplicaClient replicaClient, String customKeyFilter, String[] labelFilter, boolean isRefresh) { List loadedFeatureFlags = new ArrayList<>(); @@ -84,7 +85,7 @@ public List loadFeatureFlags(AppConfigurationReplicaClient replica return loadedFeatureFlags; } - public List proccessFeatureFlags(FeatureFlags features, String endpoint) { + List proccessFeatureFlags(FeatureFlags features, String endpoint) { List loadedFeatureFlags = new ArrayList<>(); loadedFeatureFlags.add(features); @@ -151,11 +152,15 @@ protected static Feature createFeature(FeatureFlagConfigurationSetting item, Str */ 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()); + try { + MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + final String beforeTrim = Base64URL.encode(sha256.digest(data.getBytes(StandardCharsets.UTF_8))) + .toString().replace('+', '-').replace('/', '_'); + final int index = beforeTrim.indexOf('='); + return beforeTrim.substring(0, index > -1 ? index : beforeTrim.length()); + } catch (NoSuchAlgorithmException e) { + } + return ""; } /** diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/NormalizeNull.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/NormalizeNull.java index 8463930ba1c9..3ae7ac5f267b 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/NormalizeNull.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/NormalizeNull.java @@ -9,7 +9,7 @@ * configurations with any label. * */ -public class NormalizeNull { +class NormalizeNull { private static final String EMPTY_LABEL = "\0"; @@ -19,7 +19,7 @@ public class NormalizeNull { * @param setting ConfigurationSetting * @return ConfigurationSetting with label corrected from null to \0 */ - public static ConfigurationSetting normalizeNullLabel(ConfigurationSetting setting) { + static ConfigurationSetting normalizeNullLabel(ConfigurationSetting setting) { return setting.getLabel() == null ? setting.setLabel(EMPTY_LABEL) : setting; } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/StateHolder.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/StateHolder.java index 9de8f3d08228..817ee27d1c6c 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/StateHolder.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/StateHolder.java @@ -132,14 +132,9 @@ Map getLoadState() { /** * @param originEndpoint the configuration store connected to. * @param loaded true if the configuration store was loaded. - * @param failFast application started after it failed to load from a store. */ - void setLoadState(String originEndpoint, Boolean loaded, Boolean failFast) { - if (loaded || !failFast) { - loadState.put(originEndpoint, true); - } else { - loadState.put(originEndpoint, false); - } + void setLoadState(String originEndpoint, Boolean loaded) { + loadState.put(originEndpoint, loaded); } /** diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/autofailover/SRVRecord.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/autofailover/SRVRecord.java index 9646b83527d9..7b7e9c1225f2 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/autofailover/SRVRecord.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/autofailover/SRVRecord.java @@ -2,7 +2,7 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation.autofailover; -public class SRVRecord { +class SRVRecord { private final int priority; @@ -14,7 +14,7 @@ public class SRVRecord { private static final String PROTOCAL = "https://"; - public SRVRecord(String[] record) { + SRVRecord(String[] record) { this.priority = Integer.valueOf(record[0]); this.weight = Integer.valueOf(record[1]); this.port = Integer.valueOf(record[2]); @@ -41,7 +41,7 @@ public String getEndpoint() { return PROTOCAL + target; } - public int compareTo(SRVRecord record) { + int compareTo(SRVRecord record) { if (priority > record.getPriority()) { return 1; } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/config/AppConfigurationBootstrapConfiguration.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/config/AppConfigurationBootstrapConfiguration.java deleted file mode 100644 index 2573d3abf766..000000000000 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/config/AppConfigurationBootstrapConfiguration.java +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.appconfiguration.config.implementation.config; - -import javax.naming.NamingException; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Autowired; -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.boot.context.properties.bind.Binder; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; -import org.springframework.core.env.Environment; -import org.springframework.scheduling.annotation.EnableAsync; -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.AppConfigurationKeyVaultClientFactory; -import com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationPropertySourceLocator; -import com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationReplicaClientFactory; -import com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationReplicaClientsBuilder; -import com.azure.spring.cloud.appconfiguration.config.implementation.FeatureFlagClient; -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; -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.AzurePropertiesUtils; -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; - -/** - * Setup ConnectionPool, AppConfigurationPropertySourceLocator, and ClientStore when - * spring.cloud.azure.appconfiguration.enabled is enabled. - */ -@Configuration -@PropertySource("classpath:appConfiguration.properties") -@EnableConfigurationProperties({ AppConfigurationProperties.class, AppConfigurationProviderProperties.class }) -@ConditionalOnClass(AppConfigurationPropertySourceLocator.class) -@ConditionalOnProperty(prefix = AppConfigurationProperties.CONFIG_PREFIX, name = "enabled", matchIfMissing = true) -@EnableAsync -public class AppConfigurationBootstrapConfiguration { - - @Autowired - private transient ApplicationContext context; - - @Bean - AppConfigurationPropertySourceLocator sourceLocator(AppConfigurationProperties properties, - AppConfigurationProviderProperties appProperties, AppConfigurationReplicaClientFactory clientFactory, - AppConfigurationKeyVaultClientFactory keyVaultClientFactory, ReplicaLookUp replicaLookUp, - FeatureFlagClient featureFlagLoader) - throws IllegalArgumentException { - - return new AppConfigurationPropertySourceLocator(appProperties, clientFactory, keyVaultClientFactory, - properties.getRefreshInterval(), properties.getStores(), replicaLookUp, featureFlagLoader); - } - - @Bean - AppConfigurationKeyVaultClientFactory appConfigurationKeyVaultClientFactory(Environment environment, - AppConfigurationProviderProperties appProperties) - throws IllegalArgumentException { - AzureGlobalProperties globalSource = Binder.get(environment).bindOrCreate(AzureGlobalProperties.PREFIX, - AzureGlobalProperties.class); - AzureGlobalProperties serviceSource = Binder.get(environment).bindOrCreate(AzureKeyVaultSecretProperties.PREFIX, - AzureGlobalProperties.class); - - AzureKeyVaultSecretProperties globalProperties = AzureGlobalPropertiesUtils.loadProperties( - globalSource, - new AzureKeyVaultSecretProperties()); - AzureKeyVaultSecretProperties clientProperties = AzureGlobalPropertiesUtils.loadProperties(serviceSource, - new AzureKeyVaultSecretProperties()); - - AzurePropertiesUtils.copyAzureCommonPropertiesIgnoreNull(globalProperties, clientProperties); - - SecretClientCustomizer keyVaultClientProvider = context.getBeanProvider(SecretClientCustomizer.class) - .getIfAvailable(); - KeyVaultSecretProvider keyVaultSecretProvider = context.getBeanProvider(KeyVaultSecretProvider.class) - .getIfAvailable(); - - SecretClientBuilderFactory secretClientBuilderFactory = new SecretClientBuilderFactory(clientProperties); - - boolean credentialConfigured = isCredentialConfigured(clientProperties); - - return new AppConfigurationKeyVaultClientFactory(keyVaultClientProvider, keyVaultSecretProvider, - secretClientBuilderFactory, credentialConfigured, appProperties.getMaxRetryTime()); - } - - /** - * Factory for working with App Configuration Clients - * - * @param clientBuilder Builder for configuration clients - * @param properties Client configurations for setting up connections to each config store. - * @return AppConfigurationReplicaClientFactory - */ - @Bean - @ConditionalOnMissingBean - AppConfigurationReplicaClientFactory buildClientFactory(AppConfigurationReplicaClientsBuilder clientBuilder, - AppConfigurationProperties properties, ReplicaLookUp replicaLookUp) { - return new AppConfigurationReplicaClientFactory(clientBuilder, properties.getStores(), replicaLookUp); - } - - /** - * Loader for all feature flags. Enables de-duplicating of feature flags when multiple feature flags with the same - * name are loaded. - * @return {@link FeatureFlagClient} - */ - @Bean - @ConditionalOnMissingBean - FeatureFlagClient featureFlagLoader() { - return new FeatureFlagClient(); - } - - /** - * Builder for clients connecting to App Configuration. - * - * @param clientProperties AzureAppConfigurationProperties Spring Cloud Azure global properties. - * @param appProperties Library configurations for setting up connections to each config store. - * @param keyVaultClientFactory used for tracing info for if key vault has been configured - * @param customizers Client Customizers for connecting to Azure App Configuration - * @return ClientStore - */ - @Bean - @ConditionalOnMissingBean - AppConfigurationReplicaClientsBuilder replicaClientBuilder(Environment environment, - AppConfigurationProviderProperties appProperties, AppConfigurationKeyVaultClientFactory keyVaultClientFactory, - ObjectProvider> customizers) { - AzureGlobalProperties globalSource = Binder.get(environment).bindOrCreate(AzureGlobalProperties.PREFIX, - AzureGlobalProperties.class); - AzureGlobalProperties serviceSource = Binder.get(environment).bindOrCreate( - AzureAppConfigurationProperties.PREFIX, - AzureGlobalProperties.class); - - AzureGlobalProperties globalProperties = AzureGlobalPropertiesUtils.loadProperties(globalSource, - new AzureGlobalProperties()); - AzureAppConfigurationProperties clientProperties = AzureGlobalPropertiesUtils.loadProperties(serviceSource, - new AzureAppConfigurationProperties()); - - AzurePropertiesUtils.copyAzureCommonPropertiesIgnoreNull(globalProperties, clientProperties); - - ConfigurationClientBuilderFactory clientFactory = new ConfigurationClientBuilderFactory(clientProperties); - - clientFactory.setSpringIdentifier(AzureSpringIdentifier.AZURE_SPRING_APP_CONFIG); - customizers.orderedStream().forEach(clientFactory::addBuilderCustomizer); - - boolean credentialConfigured = isCredentialConfigured(clientProperties); - - AppConfigurationReplicaClientsBuilder clientBuilder = new AppConfigurationReplicaClientsBuilder( - appProperties.getMaxRetries(), clientFactory, credentialConfigured); - - clientBuilder - .setClientProvider(context.getBeanProvider(ConfigurationClientCustomizer.class) - .getIfAvailable()); - - clientBuilder.setIsKeyVaultConfigured(keyVaultClientFactory.isConfigured()); - - return clientBuilder; - } - - @Bean - ReplicaLookUp replicaLookUp(AppConfigurationProperties properties) throws NamingException { - return new ReplicaLookUp(properties); - } - - private 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/feature/FeatureFlags.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/FeatureFlags.java index dc725a309000..2a1380a3d907 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/FeatureFlags.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/FeatureFlags.java @@ -6,7 +6,6 @@ import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.data.appconfiguration.models.SettingSelector; -import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore; public class FeatureFlags { @@ -14,27 +13,11 @@ public class FeatureFlags { private List featureFlags; - private ConfigStore configStore; - public FeatureFlags(SettingSelector settingSelector, List featureFlags) { this.settingSelector = settingSelector; this.featureFlags = featureFlags; } - /** - * @return the configStore - */ - public ConfigStore getConfigStore() { - return configStore; - } - - /** - * @param configStore the configStore to set - */ - public void setConfigStore(ConfigStore configStore) { - this.configStore = configStore; - } - /** * @return the settingSelector */ diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/entity/Conditions.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/entity/Conditions.java index d882c0399021..e010edfe7e8f 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/entity/Conditions.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/entity/Conditions.java @@ -20,7 +20,7 @@ public class Conditions { @JsonProperty("requirement_type") private String requirementType = DEFAULT_REQUIREMENT_TYPE; - public Conditions(List featureFilters, String requirementType) { + Conditions(List featureFilters, String requirementType) { clientFilters = new ArrayList<>(); clientFilters.addAll(featureFilters); this.requirementType = requirementType; diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/entity/FeatureFilterEvaluationContext.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/entity/FeatureFilterEvaluationContext.java deleted file mode 100644 index 760284fc57bd..000000000000 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/entity/FeatureFilterEvaluationContext.java +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.appconfiguration.config.implementation.feature.entity; - -import java.util.HashMap; -import java.util.Map; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Context passed into Feature Filters used for evaluation. - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public final class FeatureFilterEvaluationContext { - - /** - * Creates an instance of {@link FeatureFilterEvaluationContext} - */ - public FeatureFilterEvaluationContext() { - } - - private String name; - - private Map parameters; - - /** - * Return the name - * @return the name - */ - public String getName() { - return name; - } - - /** - * Set the name - * @param name the name to set - */ - public void setName(String name) { - this.name = name; - } - - /** - * Return the parameters - * @return the parameters - */ - public Map getParameters() { - Map params = new HashMap(); - if (parameters != null) { - params.putAll(parameters); - } - return params; - } - - /** - * Set the parameters - * @param parameters the parameters to set - */ - public void setParameters(Map parameters) { - this.parameters = parameters; - } - -} diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/entity/FeatureSet.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/entity/FeatureSet.java deleted file mode 100644 index 5259f28a3b11..000000000000 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/entity/FeatureSet.java +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.appconfiguration.config.implementation.feature.entity; - -import java.util.HashMap; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Set of Feature Flag Key pairs. - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public final class FeatureSet { - - @JsonProperty("FeatureManagement") - private HashMap featureManagement; - - /** - * Returns Map of Feature Flags. - * - * @return the featureFlags - */ - public HashMap getFeatureManagement() { - return featureManagement; - } - - /** - * Adds a new Feature Flag. - * - * @param key Name of the Feature Flag. - * @param feature true/false, for on/off feature Flag. {@code Feature} if Feature Filter. - */ - public void addFeature(String key, Object feature) { - if (featureManagement == null) { - featureManagement = new HashMap<>(); - } - if (feature != null) { - featureManagement.put(key, feature); - } - } -} diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/BaseAppConfigurationPolicy.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/BaseAppConfigurationPolicy.java index 0f5db1c2ee38..bd72467aa3f9 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/BaseAppConfigurationPolicy.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/BaseAppConfigurationPolicy.java @@ -27,10 +27,10 @@ public final class BaseAppConfigurationPolicy implements HttpPipelinePolicy { /** * Format of User Agent */ - public static final String USER_AGENT = String.format("%s/%s", StringUtils.replace(PACKAGE_NAME, " ", ""), + private static final String USER_AGENT = String.format("%s/%s", StringUtils.replace(PACKAGE_NAME, " ", ""), BaseAppConfigurationPolicy.class.getPackage().getImplementationVersion()); - final TracingInfo tracingInfo; + private final TracingInfo tracingInfo; /** * App Configuration Http Pipeline Policy @@ -40,6 +40,7 @@ public BaseAppConfigurationPolicy(TracingInfo tracingInfo) { this.tracingInfo = tracingInfo; } + @Override public Mono process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) { Boolean watchRequests = (Boolean) context.getData("refresh").orElse(false); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/FeatureFlagTracing.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/FeatureFlagTracing.java index 8a325a4614ce..24b62193cc55 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/FeatureFlagTracing.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/FeatureFlagTracing.java @@ -5,7 +5,7 @@ import java.util.Arrays; import java.util.List; -public class FeatureFlagTracing { +class FeatureFlagTracing { private static final String CUSTOM_FILTER = "CSTM"; @@ -34,18 +34,18 @@ public class FeatureFlagTracing { private Boolean usesTargetingFilter = false; - public boolean usesAnyFilter() { + boolean usesAnyFilter() { return usesCustomFilter || usesPercentageFilter || usesTimeWindowFilter || usesTargetingFilter; } - public void resetFeatureFilterTelemetry() { + void resetFeatureFilterTelemetry() { usesCustomFilter = false; usesPercentageFilter = false; usesTimeWindowFilter = false; usesTargetingFilter = false; } - public void updateFeatureFilterTelemetry(String filterName) { + void updateFeatureFilterTelemetry(String filterName) { if (PERCENTAGE_FILTER_NAMES.stream().anyMatch(name -> name.equalsIgnoreCase(filterName))) { usesPercentageFilter = true; } else if (TIME_WINDOW_FILTER_NAMES.stream().anyMatch(name -> name.equalsIgnoreCase(filterName))) { diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfo.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfo.java index e9608eb72db2..5f57b8e9ab78 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfo.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfo.java @@ -2,7 +2,6 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation.http.policy; -import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.DEV_ENV_TRACING; import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.KEY_VAULT_CONFIGURED_TRACING; import org.springframework.util.StringUtils; @@ -14,8 +13,6 @@ public class TracingInfo { - private boolean isDev = false; - private boolean isKeyVaultConfigured = false; private int replicaCount; @@ -24,17 +21,15 @@ public class TracingInfo { private final Configuration configuration; - public TracingInfo(boolean isDev, boolean isKeyVaultConfigured, int replicaCount, Configuration configuration) { - this.isDev = isDev; + public TracingInfo(boolean isKeyVaultConfigured, int replicaCount, Configuration configuration) { this.isKeyVaultConfigured = isKeyVaultConfigured; this.replicaCount = replicaCount; this.featureFlagTracing = new FeatureFlagTracing(); this.configuration = configuration; } - public String getValue(boolean watchRequests) { - String track = configuration - .get(RequestTracingConstants.REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE.toString()); + String getValue(boolean watchRequests) { + String track = configuration.get(RequestTracingConstants.REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE.toString()); if (track != null && Boolean.valueOf(track)) { return ""; } @@ -52,10 +47,6 @@ public String getValue(boolean watchRequests) { if (!hostType.isEmpty()) { sb.append(",").append(RequestTracingConstants.HOST_TYPE_KEY).append("=").append(hostType); } - - if (isDev) { - sb.append(",Env=").append(DEV_ENV_TRACING); - } if (isKeyVaultConfigured) { sb.append(",").append(KEY_VAULT_CONFIGURED_TRACING); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationKeyValueSelector.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationKeyValueSelector.java index d6d4baed1efd..3b210621899d 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationKeyValueSelector.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationKeyValueSelector.java @@ -30,7 +30,7 @@ public final class AppConfigurationKeyValueSelector { /** * Separator for multiple labels */ - public static final String LABEL_SEPARATOR = ","; + private static final String LABEL_SEPARATOR = ","; @NotNull /** @@ -129,7 +129,7 @@ public void setSnapshotName(String snapshotName) { * Validates key-filter and label-filter are valid. */ @PostConstruct - public void validateAndInit() { + void validateAndInit() { Assert.isTrue(!keyFilter.contains("*"), "KeyFilter must not contain asterisk(*)"); if (labelFilter != null) { Assert.isTrue(!labelFilter.contains("*"), "LabelFilter must not contain asterisk(*)"); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationStoreMonitoring.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationStoreMonitoring.java index 22db1862d354..f90a3a573f12 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationStoreMonitoring.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationStoreMonitoring.java @@ -118,7 +118,7 @@ public void setPushNotification(PushNotification pushNotification) { * Validates refreshIntervals are at least 1 second, and if enabled triggers are valid. */ @PostConstruct - public void validateAndInit() { + void validateAndInit() { if (enabled) { Assert.notEmpty(triggers, "Triggers need to be set if refresh is enabled."); for (AppConfigurationStoreTrigger trigger : triggers) { diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationStoreTrigger.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationStoreTrigger.java index ec69376e1bd7..97499d8b3f23 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationStoreTrigger.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationStoreTrigger.java @@ -58,7 +58,7 @@ public void setLabel(String label) { * Validates key isn't null */ @PostConstruct - public void validateAndInit() { + void validateAndInit() { Assert.notNull(key, "All Triggers need a key value set."); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/ConfigStore.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/ConfigStore.java index 7444d739770f..0f344b1637a2 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/ConfigStore.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/ConfigStore.java @@ -53,15 +53,6 @@ public final class ConfigStore { */ private List selects = new ArrayList<>(); - /** - * If true, the application will fail to start if the Config Store cannot be - * reached. If false, the application will start without the Config Store. - */ - private boolean failFast = true; - - /** - * Options for retrieving Feature Flags from the Azure Config Service. - */ private FeatureFlagStore featureFlags = new FeatureFlagStore(); /** @@ -147,20 +138,6 @@ public void setConnectionStrings(List connectionStrings) { this.connectionStrings = connectionStrings; } - /** - * @return the failFast - */ - public boolean isFailFast() { - return failFast; - } - - /** - * @param failFast the failFast to set - */ - public void setFailFast(boolean failFast) { - this.failFast = failFast; - } - /** * @return the enabled */ diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/FeatureFlagKeyValueSelector.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/FeatureFlagKeyValueSelector.java index b3280178e200..4d1de6b90b11 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/FeatureFlagKeyValueSelector.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/FeatureFlagKeyValueSelector.java @@ -105,7 +105,7 @@ public FeatureFlagKeyValueSelector setLabelFilter(String labelFilter) { * Validates key-filter and label-filter are valid. */ @PostConstruct - public void validateAndInit() { + void validateAndInit() { if (labelFilter != null) { Assert.isTrue(!labelFilter.contains("*"), "LabelFilter must not contain asterisk(*)"); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/FeatureFlagStore.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/FeatureFlagStore.java index c7b5e89646f7..18ee7c2f28b3 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/FeatureFlagStore.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/FeatureFlagStore.java @@ -48,7 +48,7 @@ public void setSelects(List selects) { } @PostConstruct - public void validateAndInit() { + void validateAndInit() { if (enabled && selects.size() == 0) { selects.add(new FeatureFlagKeyValueSelector()); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/META-INF/spring.factories b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/META-INF/spring.factories index 9a76c2325a3c..fb0f411d86c2 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/META-INF/spring.factories +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/META-INF/spring.factories @@ -1,3 +1,5 @@ -org.springframework.cloud.bootstrap.BootstrapConfiguration=\ -com.azure.spring.cloud.appconfiguration.config.implementation.config.AppConfigurationBootstrapConfiguration +# ConfigData Location Resolvers + org.springframework.boot.context.config.ConfigDataLocationResolver= com.azure.spring.cloud.appconfiguration.config.implementation.AzureAppConfigDataLocationResolver + # ConfigData Loaders + org.springframework.boot.context.config.ConfigDataLoader= com.azure.spring.cloud.appconfiguration.config.implementation.AzureAppConfigDataLoader diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 1b36b907ed1b..0472d30fc7d2 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,2 +1 @@ -com.azure.spring.cloud.appconfiguration.config.AppConfigurationAutoConfiguration -com.azure.spring.cloud.appconfiguration.config.implementation.config.AppConfigurationBootstrapConfiguration \ No newline at end of file +com.azure.spring.cloud.appconfiguration.config.AppConfigurationWatchAutoConfiguration diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/appConfiguration.properties b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/appConfiguration.properties deleted file mode 100644 index 9b90ce63ee85..000000000000 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/appConfiguration.properties +++ /dev/null @@ -1,6 +0,0 @@ -spring.cloud.appconfiguration.version=1.0 -spring.cloud.appconfiguration.maxRetries=2 -spring.cloud.appconfiguration.maxRetryTime=60 -spring.cloud.appconfiguration.preKillTime=5 -spring.cloud.appconfiguration.defaultMinBackoff=30 -spring.cloud.appconfiguration.defaultmaxBackoff=600 diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceSnapshotTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceSnapshotTest.java index bd48c729a109..7ef8af30b518 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceSnapshotTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceSnapshotTest.java @@ -128,10 +128,10 @@ public void cleanup() throws Exception { @Test public void testPropCanBeInitAndQueried() throws IOException { when(configurationListMock.iterator()).thenReturn(testItems.iterator()).thenReturn(testItems.iterator()); - when(clientMock.listSettingSnapshot(Mockito.any())).thenReturn(configurationListMock) + when(clientMock.listSettingSnapshot(Mockito.any(), Mockito.anyBoolean())).thenReturn(configurationListMock) .thenReturn(configurationListMock); - propertySource.initProperties(TRIM); + propertySource.initProperties(TRIM, false); String[] keyNames = propertySource.getPropertyNames(); String[] expectedKeyNames = testItems.stream().map(t -> { @@ -155,10 +155,10 @@ public void testPropertyNameSlashConvertedToDots() throws IOException { List settings = new ArrayList<>(); settings.add(slashedProp); when(configurationListMock.iterator()).thenReturn(settings.iterator()).thenReturn(Collections.emptyIterator()); - when(clientMock.listSettingSnapshot(Mockito.any())).thenReturn(configurationListMock) + when(clientMock.listSettingSnapshot(Mockito.any(), Mockito.anyBoolean())).thenReturn(configurationListMock) .thenReturn(configurationListMock); - propertySource.initProperties(TRIM); + propertySource.initProperties(TRIM, false); String expectedKeyName = TEST_SLASH_KEY.replace('/', '.'); String[] actualKeyNames = propertySource.getPropertyNames(); @@ -174,9 +174,9 @@ public void initNullValidContentTypeTest() throws IOException { List items = new ArrayList<>(); items.add(ITEM_NULL); when(configurationListMock.iterator()).thenReturn(items.iterator()).thenReturn(Collections.emptyIterator()); - when(clientMock.listSettingSnapshot(Mockito.any())).thenReturn(configurationListMock); + when(clientMock.listSettingSnapshot(Mockito.any(), Mockito.anyBoolean())).thenReturn(configurationListMock); - propertySource.initProperties(TRIM); + propertySource.initProperties(TRIM, false); String[] keyNames = propertySource.getPropertyNames(); String[] expectedKeyNames = diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java index 2833cc92970c..0bc44e5c546c 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java @@ -2,7 +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.KEY_VAULT_CONTENT_TYPE; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.KEY_VAULT_CONTENT_TYPE; import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_CONN_STRING; import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_KEY_VAULT_1; import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_LABEL_VAULT_1; @@ -146,8 +146,7 @@ public void invalidKeyVaultReferenceParseErrorTest() { .thenReturn(clientManagerMock); when(clientManagerMock.getSecret(Mockito.any())).thenThrow(new RuntimeException("Parse Failed")); - RuntimeException exception = assertThrows(RuntimeException.class, - () -> propertySource.initProperties(null, false)); + RuntimeException exception = assertThrows(RuntimeException.class, () -> propertySource.initProperties(null, false)); assertEquals("Parse Failed", exception.getMessage()); } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceLocatorTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceLocatorTest.java deleted file mode 100644 index a68ac60e28e6..000000000000 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceLocatorTest.java +++ /dev/null @@ -1,568 +0,0 @@ -// 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.EMPTY_LABEL; -import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_CONN_STRING; -import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_CONN_STRING_2; -import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_STORE_NAME; -import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_STORE_NAME_2; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; -import org.mockito.MockitoSession; -import org.mockito.quality.Strictness; -import org.springframework.core.env.CompositePropertySource; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.MutablePropertySources; -import org.springframework.core.env.PropertySource; - -import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting; -import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp; -import com.azure.spring.cloud.appconfiguration.config.implementation.feature.entity.Feature; -import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationKeyValueSelector; -import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProperties; -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.AppConfigurationStoreTrigger; -import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore; -import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagStore; - -@TestMethodOrder(MethodOrderer.MethodName.class) -public class AppConfigurationPropertySourceLocatorTest { - - private static final String PROFILE_NAME_1 = "dev"; - - private static final String PROFILE_NAME_2 = "prod"; - - private static final String KEY_FILTER = "/foo/"; - - @Mock - private ConfigurableEnvironment emptyEnvironment; - - @Mock - private ConfigurableEnvironment devEnvironment; - - @Mock - private ConfigurableEnvironment multiEnvironment; - - @Mock - private AppConfigurationReplicaClientFactory clientFactoryMock; - - @Mock - private AppConfigurationKeyVaultClientFactory keyVaultClientFactory; - - @Mock - private AppConfigurationReplicaClient replicaClientMock; - - @Mock - private FeatureFlagStore featureFlagStoreMock; - - @Mock - private ConfigStore configStoreMockError; - - @Mock - private AppConfigurationProviderProperties appPropertiesMock; - - @Mock - private ReplicaLookUp replicaLookUpMock; - - @Mock - private FeatureFlagClient featureFlagClientMock; - - @Mock - private ConfigStore configStoreMock; - - private AppConfigurationPropertySourceLocator locator; - - private AppConfigurationProperties properties; - - private AppConfigurationProviderProperties appProperties; - - private List stores; - - private AppConfigurationStoreMonitoring monitoring; - - private MutablePropertySources sources = new MutablePropertySources(); - - private MockitoSession session; - - @BeforeEach - public void setup() { - session = Mockito.mockitoSession().initMocks(this).strictness(Strictness.STRICT_STUBS).startMocking(); - MockitoAnnotations.openMocks(this); - - sources.addFirst(new PropertySource("refreshArgs") { - - @Override - public Object getProperty(String name) { - return null; - } - }); - - properties = new AppConfigurationProperties(); - properties.setEnabled(true); - properties.setRefreshInterval(null); - - TestUtils.addStore(properties, TEST_STORE_NAME, TEST_CONN_STRING, KEY_FILTER); - - monitoring = new AppConfigurationStoreMonitoring(); - monitoring.setEnabled(false); - AppConfigurationStoreTrigger trigger = new AppConfigurationStoreTrigger(); - trigger.setKey("test"); - - monitoring.setTriggers(List.of(trigger)); - - appProperties = new AppConfigurationProviderProperties(); - appProperties.setVersion("1.0"); - appProperties.setMaxRetries(12); - appProperties.setMaxRetryTime(0); - appProperties.setDefaultMaxBackoff((long) 600); - appProperties.setDefaultMinBackoff((long) 30); - - properties.getStores().get(0).setFeatureFlags(featureFlagStoreMock); - properties.getStores().get(0).setMonitoring(monitoring); - stores = properties.getStores(); - } - - @AfterEach - public void cleanup() throws Exception { - MockitoAnnotations.openMocks(this).close(); - session.finishMocking(); - AppConfigurationPropertySourceLocator.STARTUP.set(true); - } - - @Test - public void compositeSourceIsCreated() { - setupEmptyEnvironment(); - when(featureFlagClientMock.getProperties()).thenReturn(Map.of()); - - locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, - keyVaultClientFactory, null, stores, replicaLookUpMock, featureFlagClientMock); - - try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { - stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); - PropertySource source = locator.locate(emptyEnvironment); - - assertTrue(source instanceof CompositePropertySource); - - Collection> sources = ((CompositePropertySource) source).getPropertySources(); - - String[] expectedSourceNames = new String[] { - KEY_FILTER + "store1/\0" - }; - assertEquals(expectedSourceNames.length, sources.size()); - assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray()); - } - } - - @Test - public void compositeSourceIsCreatedWithMonitoring() { - setupEmptyEnvironment(); - when(featureFlagClientMock.getProperties()).thenReturn(Map.of()); - - String watchKey = "wk1"; - AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); - monitoring.setEnabled(true); - - AppConfigurationStoreTrigger trigger = new AppConfigurationStoreTrigger(); - trigger.setKey(watchKey); - trigger.setLabel(EMPTY_LABEL); - monitoring.setTriggers(List.of(trigger)); - - properties.getStores().get(0).setMonitoring(monitoring); - - when(replicaClientMock.getWatchKey(Mockito.eq(watchKey), Mockito.anyString(), Mockito.anyBoolean())) - .thenReturn(TestUtils.createItem("", watchKey, "0", EMPTY_LABEL, "")); - - locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, stores, replicaLookUpMock, featureFlagClientMock); - - try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { - stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); - PropertySource source = locator.locate(emptyEnvironment); - - assertTrue(source instanceof CompositePropertySource); - - Collection> sources = ((CompositePropertySource) source).getPropertySources(); - - String[] expectedSourceNames = new String[] { - KEY_FILTER + "store1/\0" - }; - assertEquals(expectedSourceNames.length, sources.size()); - assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray()); - verify(replicaClientMock, times(1)).getWatchKey(Mockito.eq(watchKey), Mockito.anyString(), - Mockito.anyBoolean()); - } - } - - @Test - public void compositeSourceIsCreatedWithMonitoringWatchKeyDoesNotExist() { - // The listed Watch Key doesn't have a value in app config. When one is added will cause a refresh. - setupEmptyEnvironment(); - when(featureFlagClientMock.getProperties()).thenReturn(Map.of()); - - String watchKey = "wk1"; - AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); - monitoring.setEnabled(true); - - AppConfigurationStoreTrigger trigger = new AppConfigurationStoreTrigger(); - trigger.setKey(watchKey); - trigger.setLabel(EMPTY_LABEL); - monitoring.setTriggers(List.of(trigger)); - - properties.getStores().get(0).setMonitoring(monitoring); - - when(replicaClientMock.getWatchKey(Mockito.eq(watchKey), Mockito.anyString(), Mockito.anyBoolean())) - .thenReturn(null); - - locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, stores, replicaLookUpMock, featureFlagClientMock); - - try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { - stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); - PropertySource source = locator.locate(emptyEnvironment); - - assertTrue(source instanceof CompositePropertySource); - - Collection> sources = ((CompositePropertySource) source).getPropertySources(); - - String[] expectedSourceNames = new String[] { - KEY_FILTER + "store1/\0" - }; - assertEquals(expectedSourceNames.length, sources.size()); - assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray()); - verify(replicaClientMock, times(1)).getWatchKey(Mockito.eq(watchKey), Mockito.anyString(), - Mockito.anyBoolean()); - } - } - - @Test - public void devSourceIsCreated() { - when(devEnvironment.getActiveProfiles()).thenReturn(new String[] { PROFILE_NAME_1 }); - when(devEnvironment.getPropertySources()).thenReturn(sources); - when(clientFactoryMock.getAvailableClients(Mockito.anyString(), Mockito.eq(true))).thenReturn(Arrays.asList(replicaClientMock)); - when(replicaClientMock.listSettings(Mockito.any(), Mockito.anyBoolean())).thenReturn(List.of()); - when(featureFlagClientMock.getProperties()).thenReturn(Map.of()); - - locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, stores, replicaLookUpMock, featureFlagClientMock); - - try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { - stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); - PropertySource source = locator.locate(devEnvironment); - assertTrue(source instanceof CompositePropertySource); - - Collection> sources = ((CompositePropertySource) source).getPropertySources(); - - String[] expectedSourceNames = new String[] { - KEY_FILTER + "store1/dev" - }; - assertEquals(expectedSourceNames.length, sources.size()); - assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray()); - } - } - - @Test - public void multiSourceIsCreated() { - when(multiEnvironment.getActiveProfiles()).thenReturn(new String[] { PROFILE_NAME_1, PROFILE_NAME_2 }); - when(multiEnvironment.getPropertySources()).thenReturn(sources); - when(clientFactoryMock.getAvailableClients(Mockito.anyString(), Mockito.eq(true))).thenReturn(Arrays.asList(replicaClientMock)); - when(replicaClientMock.listSettings(Mockito.any(), Mockito.anyBoolean())).thenReturn(List.of()); - when(featureFlagClientMock.getProperties()).thenReturn(Map.of()); - - locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, stores, replicaLookUpMock, featureFlagClientMock); - - try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { - stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); - PropertySource source = locator.locate(multiEnvironment); - assertTrue(source instanceof CompositePropertySource); - - Collection> sources = ((CompositePropertySource) source).getPropertySources(); - - String[] expectedSourceNames = new String[] { - KEY_FILTER + "store1/prod,dev" - }; - assertEquals(expectedSourceNames.length, sources.size()); - assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray()); - } - } - - @Test - public void storeCreatedWithFeatureFlags() { - setupEmptyEnvironment(); - when(featureFlagClientMock.getProperties()).thenReturn(Map.of("fake_features", new Feature())); - - FeatureFlagStore featureFlagStore = new FeatureFlagStore(); - featureFlagStore.setEnabled(true); - featureFlagStore.validateAndInit(); - - FeatureFlagConfigurationSetting featureFlag = new FeatureFlagConfigurationSetting("Alpha", false); - featureFlag.setValue(""); - - properties.getStores().get(0).setFeatureFlags(featureFlagStore); - - when(replicaClientMock.listSettings(Mockito.any(), Mockito.anyBoolean())).thenReturn(List.of(featureFlag)); - - locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, stores, replicaLookUpMock, featureFlagClientMock); - - try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { - stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); - PropertySource source = locator.locate(emptyEnvironment); - assertTrue(source instanceof CompositePropertySource); - - Collection> sources = ((CompositePropertySource) source).getPropertySources(); - String[] expectedSourceNames = new String[] { - KEY_FILTER + "store1/\0", - "feature_management" - }; - assertEquals(expectedSourceNames.length, sources.size()); - assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray()); - } - } - - @Test - public void storeCreatedWithFeatureFlagsWithMonitoring() { - setupEmptyEnvironment(); - when(featureFlagClientMock.getProperties()).thenReturn(Map.of("fake_features", new Feature())); - - AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); - monitoring.setEnabled(true); - FeatureFlagStore featureFlagStore = new FeatureFlagStore(); - featureFlagStore.setEnabled(true); - featureFlagStore.validateAndInit(); - - FeatureFlagConfigurationSetting featureFlag = new FeatureFlagConfigurationSetting("Alpha", false); - featureFlag.setValue(""); - - properties.getStores().get(0).setFeatureFlags(featureFlagStore); - properties.getStores().get(0).setMonitoring(monitoring); - - when(replicaClientMock.listSettings(Mockito.any(), Mockito.anyBoolean())).thenReturn(List.of(featureFlag)); - - locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, stores, replicaLookUpMock, featureFlagClientMock); - - try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { - stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); - PropertySource source = locator.locate(emptyEnvironment); - assertTrue(source instanceof CompositePropertySource); - - Collection> sources = ((CompositePropertySource) source).getPropertySources(); - String[] expectedSourceNames = new String[] { - KEY_FILTER + "store1/\0", - "feature_management" - }; - assertEquals(expectedSourceNames.length, sources.size()); - assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray()); - } - } - - @Test - public void watchedKeyCheck() { - setupEmptyEnvironment(); - when(featureFlagClientMock.getProperties()).thenReturn(Map.of()); - - locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, stores, replicaLookUpMock, featureFlagClientMock); - - try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { - stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); - PropertySource source = locator.locate(emptyEnvironment); - assertTrue(source instanceof CompositePropertySource); - - Collection> sources = ((CompositePropertySource) source).getPropertySources(); - String[] expectedSourceNames = new String[] { - KEY_FILTER + "store1/\0" - }; - assertEquals(expectedSourceNames.length, sources.size()); - assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray()); - } - } - - @Test - public void defaultFailFastThrowException() { - when(emptyEnvironment.getActiveProfiles()).thenReturn(new String[] {}); - when(emptyEnvironment.getPropertySources()).thenReturn(sources); - when(clientFactoryMock.getAvailableClients(Mockito.anyString(), Mockito.eq(true))).thenReturn(Arrays.asList(replicaClientMock)); - when(configStoreMock.getEndpoint()).thenReturn(TEST_STORE_NAME); - when(configStoreMock.isEnabled()).thenReturn(true); - when(configStoreMock.getSelects()).thenReturn(List.of()); - when(configStoreMock.getFeatureFlags()).thenReturn(featureFlagStoreMock); - when(configStoreMock.isFailFast()).thenReturn(true); - - AppConfigurationStoreTrigger trigger = new AppConfigurationStoreTrigger(); - AppConfigurationStoreMonitoring monitor = new AppConfigurationStoreMonitoring(); - monitor.setEnabled(true); - monitor.setTriggers(List.of(trigger)); - - when(configStoreMock.getMonitoring()).thenReturn(monitor); - - locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, List.of(configStoreMock), replicaLookUpMock, featureFlagClientMock); - - when(replicaClientMock.getWatchKey(Mockito.any(), Mockito.anyString(), Mockito.anyBoolean())).thenThrow(new RuntimeException()); - RuntimeException e = assertThrows(RuntimeException.class, () -> locator.locate(emptyEnvironment)); - assertEquals("Failed to generate property sources for " + TEST_STORE_NAME, e.getMessage()); - verify(configStoreMock, times(1)).isFailFast(); - } - - @Test - public void refreshThrowException() throws IllegalArgumentException { - setupEmptyEnvironment(); - when(replicaClientMock.listSettings(any(), Mockito.anyBoolean())).thenThrow(new RuntimeException()); - - AppConfigurationPropertySourceLocator.STARTUP.set(false); - - locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, stores, replicaLookUpMock, featureFlagClientMock); - - try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { - stateHolderMock.when(() -> StateHolder.getLoadState(Mockito.anyString())).thenReturn(true); - RuntimeException e = assertThrows(RuntimeException.class, () -> locator.locate(emptyEnvironment)); - assertEquals("Failed to generate property sources for store1", e.getMessage()); - } - } - - @Test - public void notFailFastShouldPass() { - when(featureFlagClientMock.getProperties()).thenReturn(Map.of()); - when(emptyEnvironment.getActiveProfiles()).thenReturn(new String[] {}); - when(emptyEnvironment.getPropertySources()).thenReturn(sources); - when(clientFactoryMock.getAvailableClients(Mockito.anyString(), Mockito.eq(true))).thenReturn(Arrays.asList(replicaClientMock)); - when(configStoreMock.getEndpoint()).thenReturn(TEST_STORE_NAME); - when(configStoreMock.isEnabled()).thenReturn(true); - when(configStoreMock.getSelects()).thenReturn(List.of()); - when(configStoreMock.getFeatureFlags()).thenReturn(featureFlagStoreMock); - when(configStoreMock.isFailFast()).thenReturn(false); - - locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, List.of(configStoreMock), replicaLookUpMock, featureFlagClientMock); - - properties.getStores().get(0).setFailFast(false); - - try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { - stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); - PropertySource source = locator.locate(emptyEnvironment); - assertTrue(source instanceof CompositePropertySource); - - // Once a store fails it should stop attempting to load - verify(configStoreMock, times(3)).isFailFast(); - } - } - - @Test - public void multiplePropertySourcesExistForMultiStores() { - setupEmptyEnvironment(); - when(featureFlagClientMock.getProperties()).thenReturn(Map.of()); - TestUtils.addStore(properties, TEST_STORE_NAME_2, TEST_CONN_STRING_2, KEY_FILTER); - - locator = new AppConfigurationPropertySourceLocator(appProperties, - clientFactoryMock, keyVaultClientFactory, null, properties.getStores(), replicaLookUpMock, - featureFlagClientMock); - - try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { - stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); - PropertySource source = locator.locate(emptyEnvironment); - assertTrue(source instanceof CompositePropertySource); - - Collection> sources = ((CompositePropertySource) source).getPropertySources(); - String[] expectedSourceNames = new String[] { KEY_FILTER + TEST_STORE_NAME_2 + "/\0", - KEY_FILTER + TEST_STORE_NAME + "/\0" }; - assertEquals(2, sources.size()); - assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray()); - } - } - - @Test - public void awaitOnError() { - when(clientFactoryMock.getAvailableClients(Mockito.anyString(), Mockito.eq(true))).thenReturn(Arrays.asList(replicaClientMock)); - when(replicaClientMock.listSettings(Mockito.any(), Mockito.anyBoolean())).thenReturn(List.of()); - when(appPropertiesMock.getPrekillTime()).thenReturn(5); - - ConfigurableEnvironment env = Mockito.mock(ConfigurableEnvironment.class); - MutablePropertySources sources = new MutablePropertySources(); - - sources.addFirst(new PropertySource("refreshArgs") { - - @Override - public Object getProperty(String name) { - return null; - } - }); - - when(env.getPropertySources()).thenReturn(sources); - - String[] array = {}; - when(env.getActiveProfiles()).thenReturn(array); - AppConfigurationKeyValueSelector selectedKeys = new AppConfigurationKeyValueSelector() - .setKeyFilter("/application/"); - List selects = new ArrayList<>(); - selects.add(selectedKeys); - when(configStoreMockError.getSelects()).thenReturn(selects); - when(configStoreMockError.isEnabled()).thenReturn(true); - when(configStoreMockError.isFailFast()).thenReturn(true); - when(configStoreMockError.getEndpoint()).thenReturn(""); - - when(replicaClientMock.listSettings(Mockito.any(), Mockito.anyBoolean())).thenThrow(new NullPointerException("")); - when(appPropertiesMock.getPrekillTime()).thenReturn(-60); - when(appPropertiesMock.getStartDate()).thenReturn(Instant.now()); - - locator = new AppConfigurationPropertySourceLocator(appPropertiesMock, clientFactoryMock, keyVaultClientFactory, - null, List.of(configStoreMockError), replicaLookUpMock, featureFlagClientMock); - - assertThrows(RuntimeException.class, () -> locator.locate(env)); - verify(appPropertiesMock, times(1)).getPrekillTime(); - } - - @Test - public void storeDisabled() { - when(emptyEnvironment.getActiveProfiles()).thenReturn(new String[] {}); - when(emptyEnvironment.getPropertySources()).thenReturn(sources); - properties.getStores().get(0).setEnabled(false); - properties.getStores().get(0).setMonitoring(monitoring); - - locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, stores, replicaLookUpMock, featureFlagClientMock); - try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { - stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); - PropertySource source = locator.locate(emptyEnvironment); - assertTrue(source instanceof CompositePropertySource); - - Collection> sources = ((CompositePropertySource) source).getPropertySources(); - - assertEquals(0, sources.size()); - } - } - - private void setupEmptyEnvironment() { - when(emptyEnvironment.getActiveProfiles()).thenReturn(new String[] {}); - when(emptyEnvironment.getPropertySources()).thenReturn(sources); - when(clientFactoryMock.getAvailableClients(Mockito.anyString(), Mockito.eq(true))).thenReturn(Arrays.asList(replicaClientMock)); - when(replicaClientMock.listSettings(Mockito.any(), Mockito.anyBoolean())).thenReturn(List.of()); - } -} diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtilTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtilTest.java index ba6b62292b8c..161681f4740b 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtilTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtilTest.java @@ -101,13 +101,12 @@ public void cleanup() throws Exception { public void refreshWithoutTimeWatchKeyConfigStoreNotLoaded(TestInfo testInfo) { endpoint = testInfo.getDisplayName() + ".azconfig.io"; when(clientMock.getEndpoint()).thenReturn(endpoint); - when(clientFactoryMock.findOriginForEndpoint(Mockito.eq(endpoint))).thenReturn(endpoint); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.getLoadState(endpoint)).thenReturn(false); - assertFalse( - AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreCheck(clientMock, endpoint)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreFeatureFlagCheck(true, clientMock)); } } @@ -115,20 +114,18 @@ public void refreshWithoutTimeWatchKeyConfigStoreNotLoaded(TestInfo testInfo) { public void refreshWithoutTimeWatchKeyConfigStoreWatchKeyNotReturned(TestInfo testInfo) { endpoint = testInfo.getDisplayName() + ".azconfig.io"; when(clientMock.getEndpoint()).thenReturn(endpoint); - when(clientFactoryMock.findOriginForEndpoint(Mockito.eq(endpoint))).thenReturn(endpoint); List watchKeys = generateWatchKeys(); State newState = new State(watchKeys, Math.toIntExact(Duration.ofMinutes(10).getSeconds()), endpoint); // Config Store doesn't return a watch key change. - when(clientMock.getWatchKey(Mockito.eq(KEY_FILTER), Mockito.eq(EMPTY_LABEL), Mockito.anyBoolean())) - .thenReturn(watchKeys.get(0)); + when(clientMock.getWatchKey(Mockito.eq(KEY_FILTER), Mockito.eq(EMPTY_LABEL), Mockito.anyBoolean())).thenReturn(watchKeys.get(0)); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.getLoadState(endpoint)).thenReturn(true); stateHolderMock.when(() -> StateHolder.getState(endpoint)).thenReturn(newState); - assertFalse( - AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreCheck(clientMock, endpoint)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreFeatureFlagCheck(true, clientMock)); } } @@ -136,7 +133,6 @@ public void refreshWithoutTimeWatchKeyConfigStoreWatchKeyNotReturned(TestInfo te public void refreshWithoutTimeWatchKeyConfigStoreWatchKeyNoChange(TestInfo testInfo) { endpoint = testInfo.getDisplayName() + ".azconfig.io"; when(clientMock.getEndpoint()).thenReturn(endpoint); - when(clientFactoryMock.findOriginForEndpoint(Mockito.eq(endpoint))).thenReturn(endpoint); FeatureFlagState newState = new FeatureFlagState( List.of(new FeatureFlags(new SettingSelector().setKeyFilter(KEY_FILTER).setLabelFilter(EMPTY_LABEL), null)), @@ -147,38 +143,36 @@ public void refreshWithoutTimeWatchKeyConfigStoreWatchKeyNoChange(TestInfo testI try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.getStateFeatureFlag(endpoint)).thenReturn(newState); - assertFalse( - AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreCheck(clientMock, endpoint)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreFeatureFlagCheck(true, clientMock)); } } - @SuppressWarnings("try") @Test public void refreshWithoutTimeFeatureFlagDisabled(TestInfo testInfo) { endpoint = testInfo.getDisplayName() + ".azconfig.io"; when(clientMock.getEndpoint()).thenReturn(endpoint); - when(clientFactoryMock.findOriginForEndpoint(Mockito.eq(endpoint))).thenReturn(endpoint); configStore.getFeatureFlags().setEnabled(false); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { - assertFalse( - AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreCheck(clientMock, endpoint)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreFeatureFlagCheck(true, clientMock)); + stateHolderMock.verify(() -> StateHolder.getLoadState(Mockito.anyString()), times(1)); } } - @SuppressWarnings("try") @Test public void refreshWithoutTimeFeatureFlagNotLoaded(TestInfo testInfo) { endpoint = testInfo.getDisplayName() + ".azconfig.io"; when(clientMock.getEndpoint()).thenReturn(endpoint); - when(clientFactoryMock.findOriginForEndpoint(Mockito.eq(endpoint))).thenReturn(endpoint); configStore.getFeatureFlags().setEnabled(true); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { - assertFalse( - AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreCheck(clientMock, endpoint)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreFeatureFlagCheck(true, clientMock)); + stateHolderMock.verify(() -> StateHolder.getLoadState(Mockito.anyString()), times(1)); } } @@ -186,8 +180,7 @@ public void refreshWithoutTimeFeatureFlagNotLoaded(TestInfo testInfo) { public void refreshWithoutTimeFeatureFlagNoChange(TestInfo testInfo) { endpoint = testInfo.getDisplayName() + ".azconfig.io"; when(clientMock.getEndpoint()).thenReturn(endpoint); - when(clientFactoryMock.findOriginForEndpoint(Mockito.eq(endpoint))).thenReturn(endpoint); - + FeatureFlagState newState = new FeatureFlagState( List.of(new FeatureFlags(new SettingSelector().setKeyFilter(KEY_FILTER).setLabelFilter(EMPTY_LABEL), null)), Math.toIntExact(Duration.ofMinutes(10).getSeconds()), endpoint); @@ -197,8 +190,8 @@ public void refreshWithoutTimeFeatureFlagNoChange(TestInfo testInfo) { try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.getStateFeatureFlag(endpoint)).thenReturn(newState); - assertFalse( - AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreCheck(clientMock, endpoint)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreFeatureFlagCheck(true, clientMock)); } } @@ -207,8 +200,7 @@ public void refreshWithoutTimeFeatureFlagNoChange(TestInfo testInfo) { public void refreshWithoutTimeFeatureFlagEtagChanged(TestInfo testInfo) { endpoint = testInfo.getDisplayName() + ".azconfig.io"; when(clientMock.getEndpoint()).thenReturn(endpoint); - when(clientFactoryMock.findOriginForEndpoint(Mockito.eq(endpoint))).thenReturn(endpoint); - + FeatureFlags featureFlags = new FeatureFlags(new SettingSelector(), watchKeysFeatureFlags); FeatureFlagState newState = new FeatureFlagState(List.of(featureFlags), Math.toIntExact(Duration.ofMinutes(10).getSeconds()), endpoint); @@ -218,8 +210,8 @@ public void refreshWithoutTimeFeatureFlagEtagChanged(TestInfo testInfo) { try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.getStateFeatureFlag(endpoint)).thenReturn(newState); - assertTrue( - AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreCheck(clientMock, endpoint)); + assertTrue(AppConfigurationRefreshUtil.refreshStoreFeatureFlagCheck(true, clientMock)); } } @@ -241,8 +233,7 @@ public void refreshStoresCheckSettingsTestNotEnabled(TestInfo testInfo) { (long) 60, replicaLookUpMock); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); - verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), - Mockito.anyBoolean()); + verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); } } @@ -261,8 +252,7 @@ public void refreshStoresCheckSettingsTestNotLoaded(TestInfo testInfo) { Duration.ofMinutes(10), (long) 60, replicaLookUpMock); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); - verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), - Mockito.anyBoolean()); + verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); } } @@ -282,8 +272,7 @@ public void refreshStoresCheckSettingsTestNotRefreshTime(TestInfo testInfo) { (long) 60, replicaLookUpMock); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); - verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), - Mockito.anyBoolean()); + verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); } } @@ -305,8 +294,7 @@ public void refreshStoresCheckSettingsTestFailedRequest(TestInfo testInfo) { (long) 60, replicaLookUpMock); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); - verify(clientOriginMock, times(1)).getWatchKey(Mockito.anyString(), Mockito.anyString(), - Mockito.anyBoolean()); + verify(clientOriginMock, times(1)).getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); assertEquals(newState, StateHolder.getState(endpoint)); } } @@ -332,8 +320,7 @@ public void refreshStoresCheckSettingsTestRefreshTimeNoChange(TestInfo testInfo) assertEquals(newState, StateHolder.getState(endpoint)); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); - verify(clientOriginMock, times(1)).getWatchKey(Mockito.anyString(), Mockito.anyString(), - Mockito.anyBoolean()); + verify(clientOriginMock, times(1)).getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); } } @@ -349,8 +336,7 @@ public void refreshStoresCheckSettingsTestTriggerRefresh(TestInfo testInfo) { ConfigurationSetting refreshKey = new ConfigurationSetting().setKey(KEY_FILTER).setLabel(EMPTY_LABEL) .setETag("new"); - when(clientOriginMock.getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean())) - .thenReturn(refreshKey); + when(clientOriginMock.getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean())).thenReturn(refreshKey); State newState = new State(generateWatchKeys(), Math.toIntExact(Duration.ofMinutes(-1).getSeconds()), endpoint); @@ -364,8 +350,7 @@ public void refreshStoresCheckSettingsTestTriggerRefresh(TestInfo testInfo) { (long) 60, replicaLookUpMock); assertTrue(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); - verify(clientOriginMock, times(1)).getWatchKey(Mockito.anyString(), Mockito.anyString(), - Mockito.anyBoolean()); + verify(clientOriginMock, times(1)).getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); verify(currentStateMock, times(1)).updateStateRefresh(Mockito.any(), Mockito.any()); } } @@ -388,8 +373,7 @@ public void refreshStoresCheckFeatureFlagTestNotLoaded(TestInfo testInfo) { (long) 60, replicaLookUpMock); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); - verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), - Mockito.anyBoolean()); + verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); } } @@ -411,8 +395,7 @@ public void refreshStoresCheckFeatureFlagTestNotRefreshTime(TestInfo testInfo) { (long) 60, replicaLookUpMock); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); - verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), - Mockito.anyBoolean()); + verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); } } @@ -440,8 +423,7 @@ public void refreshStoresCheckFeatureFlagTestNoChange(TestInfo testInfo) { Duration.ofMinutes(10), (long) 60, replicaLookUpMock); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); - verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), - Mockito.anyBoolean()); + verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); verify(currentStateMock, times(1)).updateFeatureFlagStateRefresh(Mockito.any(), Mockito.any()); } @@ -468,8 +450,7 @@ public void refreshStoresCheckFeatureFlagTestTriggerRefresh(TestInfo testInfo) { Duration.ofMinutes(10), (long) 60, replicaLookUpMock); assertTrue(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); - verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), - Mockito.anyBoolean()); + verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientBuilderTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientBuilderTest.java index 3dda6cd72a43..c35010596f36 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientBuilderTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientBuilderTest.java @@ -51,7 +51,7 @@ public class AppConfigurationReplicaClientBuilderTest { @Mock private ConfigurationClientBuilderFactory clientFactoryMock; - + @Mock private Environment envMock; @@ -68,7 +68,6 @@ public void setup() { configStore.validateAndInit(); clientBuilder = null; - when(envMock.getActiveProfiles()).thenReturn(new String[0]); } @AfterEach @@ -79,8 +78,7 @@ public void cleanup() throws Exception { @Test public void buildClientFromEndpointTest() { - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); - clientBuilder.setEnvironment(envMock); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); when(clientFactoryMock.build()).thenReturn(builderMock); @@ -102,8 +100,7 @@ public void buildClientFromConnectionStringTest() { configStore.setConnectionString(TEST_CONN_STRING); configStore.validateAndInit(); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); - clientBuilder.setEnvironment(envMock); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); when(clientFactoryMock.build()).thenReturn(builderMock); @@ -120,9 +117,7 @@ public void buildClientFromConnectionStringTest() { @Test public void modifyClientTest() { - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); - clientBuilder.setClientProvider(modifierMock); - clientBuilder.setEnvironment(envMock); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, modifierMock, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); @@ -153,8 +148,7 @@ public void buildClientsFromMultipleEndpointsTest() { configStore.validateAndInit(); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); - clientBuilder.setEnvironment(envMock); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); @@ -180,8 +174,7 @@ public void buildClientsFromMultipleConnectionStringsTest() { configStore.validateAndInit(); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); - clientBuilder.setEnvironment(envMock); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); @@ -205,8 +198,7 @@ public void endpointAndConnectionString() { configStore.setConnectionString(TEST_CONN_STRING); configStore.validateAndInit(); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); - clientBuilder.setEnvironment(envMock); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); String message = assertThrows(IllegalArgumentException.class, () -> clientBuilder.buildClients(configStore).get(0)).getMessage(); @@ -216,8 +208,7 @@ public void endpointAndConnectionString() { @Test public void buildClientTest() { - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); - clientBuilder.setEnvironment(envMock); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); when(clientFactoryMock.build()).thenReturn(builderMock); @@ -236,8 +227,7 @@ public void buildClientTest() { @Test public void buildClientConnectionStringTest() { configStore.setConnectionString(TEST_CONN_STRING); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); - clientBuilder.setEnvironment(envMock); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); when(clientFactoryMock.build()).thenReturn(builderMock); @@ -255,8 +245,7 @@ public void buildClientConnectionStringTest() { @Test public void buildClientConnectionStringsTest() { configStore.setConnectionStrings(List.of(TEST_CONN_STRING, TEST_CONN_STRING_GEO)); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); - clientBuilder.setEnvironment(envMock); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); when(builderMock.addPolicy(Mockito.any())).thenReturn(builderMock); @@ -274,8 +263,7 @@ public void buildClientConnectionStringsTest() { @Test public void buildClientConnectionStringInvalidTest() { configStore.setConnectionString(TEST_CONN_STRING); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); - clientBuilder.setEnvironment(envMock); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, @@ -287,8 +275,7 @@ public void buildClientConnectionStringInvalidTest() { @Test public void buildClientConnectionStringInvalid2Test() { configStore.setConnectionString("Not A Connection String"); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); - clientBuilder.setEnvironment(envMock); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, @@ -300,8 +287,7 @@ public void buildClientConnectionStringInvalid2Test() { @Test public void buildClientConnectionStringInvalid3Test() { configStore.setConnectionString("Not;A;Connection String"); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); - clientBuilder.setEnvironment(envMock); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactoryTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactoryTest.java index 3be2260be0f4..186a6ee8db94 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactoryTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactoryTest.java @@ -3,8 +3,6 @@ package com.azure.spring.cloud.appconfiguration.config.implementation; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; import java.util.List; @@ -79,12 +77,4 @@ public void findOriginTest() { assertEquals(invalidReplica, clientFactory.findOriginForEndpoint(invalidReplica)); } - @Test - public void hasReplicasTest() { - assertTrue(clientFactory.hasReplicas(originEndpoint)); - assertTrue(clientFactory.hasReplicas(replica1)); - assertFalse(clientFactory.hasReplicas(invalidReplica)); - assertFalse(clientFactory.hasReplicas(noReplicaEndpoint)); - } - } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java index 99c71ea169a0..9e328cffd751 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java @@ -37,7 +37,6 @@ import com.azure.core.http.rest.PagedResponse; import com.azure.core.http.rest.PagedResponseBase; import com.azure.core.http.rest.Response; -import com.azure.core.util.Configuration; import com.azure.data.appconfiguration.ConfigurationClient; import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.data.appconfiguration.models.ConfigurationSnapshot; @@ -45,7 +44,6 @@ import com.azure.data.appconfiguration.models.SettingSelector; import com.azure.data.appconfiguration.models.SnapshotComposition; import com.azure.identity.CredentialUnavailableException; -import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.TracingInfo; import reactor.core.publisher.Mono; @@ -67,7 +65,10 @@ public class AppConfigurationReplicaClientTest { private Supplier>> supplierMock; @Mock - private Response mockResponse; + private Response configurationSettingResponse; + + @Mock + private Response snapshotResponseMock; private final String endpoint = "clientTest.azconfig.io"; @@ -87,20 +88,17 @@ public void cleanup() throws Exception { @Test public void getWatchKeyTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock, - new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); ConfigurationSetting watchKey = new ConfigurationSetting().setKey("watch").setLabel("\0"); - when(clientMock.getConfigurationSettingWithResponse(Mockito.any(), Mockito.isNull(), Mockito.anyBoolean(), - Mockito.any())).thenReturn(mockResponse); - when(mockResponse.getValue()).thenReturn(watchKey); + when(clientMock.getConfigurationSettingWithResponse(Mockito.any(), Mockito.any(), Mockito.anyBoolean(), + Mockito.any())).thenReturn(configurationSettingResponse); + when(configurationSettingResponse.getValue()).thenReturn(watchKey); - //assertEquals(watchKey, client.getWatchKey("watch", "\0", false)); + assertEquals(watchKey, client.getWatchKey("watch", "\0", false)); - when(clientMock.getConfigurationSettingWithResponse(Mockito.any(), Mockito.isNull(), Mockito.anyBoolean(), - Mockito.any())).thenReturn(mockResponse); - when(mockResponse.getValue()).thenThrow(exceptionMock); + when(configurationSettingResponse.getValue()).thenThrow(exceptionMock); when(exceptionMock.getResponse()).thenReturn(responseMock); when(responseMock.getStatusCode()).thenReturn(429); assertThrows(AppConfigurationStatusException.class, () -> client.getWatchKey("watch", "\0", false)); @@ -114,15 +112,14 @@ public void getWatchKeyTest() { when(responseMock.getStatusCode()).thenReturn(499); assertThrows(HttpResponseException.class, () -> client.getWatchKey("watch", "\0", false)); - when(clientMock.getConfigurationSettingWithResponse(Mockito.any(), Mockito.isNull(), Mockito.anyBoolean(), + when(clientMock.getConfigurationSettingWithResponse(Mockito.any(), Mockito.any(), Mockito.anyBoolean(), Mockito.any())).thenThrow(new UncheckedIOException(new UnknownHostException())); assertThrows(AppConfigurationStatusException.class, () -> client.getWatchKey("watch", "\0", false)); } @Test public void listSettingsTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock, - new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); ConfigurationSetting configurationSetting = new ConfigurationSetting().setKey("test-key"); List configurations = List.of(configurationSetting); @@ -154,8 +151,7 @@ public void listSettingsTest() { @Test public void listFeatureFlagsTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock, - new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); FeatureFlagConfigurationSetting featureFlag = new FeatureFlagConfigurationSetting("Alpha", false); List configurations = List.of(featureFlag); @@ -192,8 +188,7 @@ public void listFeatureFlagsTest() { @Test public void listSettingsUnknownHostTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock, - new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())) .thenThrow(new UncheckedIOException(new UnknownHostException())); @@ -202,8 +197,7 @@ public void listSettingsUnknownHostTest() { @Test public void listSettingsNoCredentialTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock, - new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())) .thenThrow(new CredentialUnavailableException("No Credential")); @@ -213,12 +207,9 @@ public void listSettingsNoCredentialTest() { @Test public void getWatchNoCredentialTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock, - new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); - when(clientMock.getConfigurationSettingWithResponse(Mockito.any(), Mockito.isNull(), Mockito.anyBoolean(), - Mockito.any())).thenReturn(mockResponse); - when(mockResponse.getValue()) + when(clientMock.getConfigurationSettingWithResponse(Mockito.any(), Mockito.any(), Mockito.anyBoolean(), Mockito.any())) .thenThrow(new CredentialUnavailableException("No Credential")); assertThrows(CredentialUnavailableException.class, () -> client.getWatchKey("key", "label", false)); @@ -226,8 +217,7 @@ public void getWatchNoCredentialTest() { @Test public void backoffTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock, - new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); // Setups in the past and with no errors. assertTrue(client.getBackoffEndTime().isBefore(Instant.now())); @@ -255,55 +245,57 @@ public void backoffTest() { @Test public void listSettingSnapshotTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock, - new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); List configurations = new ArrayList<>(); ConfigurationSnapshot snapshot = new ConfigurationSnapshot(null); snapshot.setSnapshotComposition(SnapshotComposition.KEY); - when(clientMock.getSnapshot(Mockito.any())).thenReturn(snapshot); + when(clientMock.getSnapshotWithResponse(Mockito.any(), Mockito.any(), Mockito.any())) + .thenReturn(snapshotResponseMock); + when(snapshotResponseMock.getValue()).thenReturn(snapshot); when(clientMock.listConfigurationSettingsForSnapshot(Mockito.any())).thenReturn(settingsMock); - assertEquals(configurations, client.listSettingSnapshot("SnapshotName")); + assertEquals(configurations, client.listSettingSnapshot("SnapshotName", false)); when(clientMock.listConfigurationSettingsForSnapshot(Mockito.any())).thenThrow(exceptionMock); when(exceptionMock.getResponse()).thenReturn(responseMock); when(responseMock.getStatusCode()).thenReturn(429); - assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName")); + assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName", false)); when(responseMock.getStatusCode()).thenReturn(408); - assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName")); + assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName", false)); when(responseMock.getStatusCode()).thenReturn(500); - assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName")); + assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName", false)); when(responseMock.getStatusCode()).thenReturn(499); - assertThrows(HttpResponseException.class, () -> client.listSettingSnapshot("SnapshotName")); + assertThrows(HttpResponseException.class, () -> client.listSettingSnapshot("SnapshotName", false)); - when(clientMock.getSnapshot(Mockito.any())).thenThrow(new UncheckedIOException(new UnknownHostException())); - assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName")); + when(clientMock.getSnapshotWithResponse(Mockito.any(), Mockito.any(), Mockito.any())) + .thenThrow(new UncheckedIOException(new UnknownHostException())); + assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName", false)); } @Test public void listSettingSnapshotInvalidCompositionTypeTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock, - new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); ConfigurationSnapshot snapshot = new ConfigurationSnapshot(null); snapshot.setSnapshotComposition(SnapshotComposition.KEY_LABEL); - when(clientMock.getSnapshot(Mockito.any())).thenReturn(snapshot); + when(clientMock.getSnapshotWithResponse(Mockito.any(), Mockito.any(), Mockito.any())) + .thenReturn(snapshotResponseMock); + when(snapshotResponseMock.getValue()).thenReturn(snapshot); IllegalArgumentException e = assertThrows(IllegalArgumentException.class, - () -> client.listSettingSnapshot("SnapshotName")); + () -> client.listSettingSnapshot("SnapshotName", false)); assertEquals("Snapshot SnapshotName needs to be of type Key.", e.getMessage()); } @Test public void updateSyncTokenTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock, - new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); String fakeToken = "fake_sync_token"; client.updateSyncToken(fakeToken); @@ -316,8 +308,7 @@ public void updateSyncTokenTest() { @Test public void checkWatchKeysTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock, - new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); FeatureFlagConfigurationSetting featureFlag = new FeatureFlagConfigurationSetting("Alpha", false); List configurations = List.of(featureFlag); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManagerTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManagerTest.java index ff6a30fed45b..bdaa51cecaf7 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManagerTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManagerTest.java @@ -4,7 +4,6 @@ import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_ENDPOINT; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -118,8 +117,6 @@ public void backoffTest() { expectedEndpoints.add(replicaEndpoint); assertEquals(2, connectionManager.getAvailableClients().size()); - assertEquals(2, connectionManager.getAllEndpoints().size()); - assertTrue(connectionManager.getAllEndpoints().containsAll(expectedEndpoints)); assertEquals(AppConfigurationStoreHealth.UP, connectionManager.getHealth()); connectionManager.backoffClient(originEndpoint); @@ -129,15 +126,11 @@ public void backoffTest() { when(replicaClient1.getBackoffEndTime()).thenReturn(Instant.now().plusSeconds(1000)); assertEquals(1, connectionManager.getAvailableClients().size()); - assertEquals(2, connectionManager.getAllEndpoints().size()); - assertTrue(connectionManager.getAllEndpoints().containsAll(expectedEndpoints)); assertEquals(AppConfigurationStoreHealth.UP, connectionManager.getHealth()); connectionManager.backoffClient(originEndpoint); assertEquals(1, connectionManager.getAvailableClients().size()); - assertEquals(2, connectionManager.getAllEndpoints().size()); - assertTrue(connectionManager.getAllEndpoints().containsAll(expectedEndpoints)); assertEquals(AppConfigurationStoreHealth.UP, connectionManager.getHealth()); connectionManager.backoffClient(replicaEndpoint); @@ -146,8 +139,6 @@ public void backoffTest() { when(replicaClient2.getBackoffEndTime()).thenReturn(Instant.now().plusSeconds(1000)); assertEquals(0, connectionManager.getAvailableClients().size()); - assertEquals(2, connectionManager.getAllEndpoints().size()); - assertTrue(connectionManager.getAllEndpoints().containsAll(expectedEndpoints)); assertEquals(AppConfigurationStoreHealth.DOWN, connectionManager.getHealth()); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java index f473388bd8c3..07c2e5a56fa2 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java @@ -2,18 +2,18 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation; -import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.DEFAULT_ROLLOUT_PERCENTAGE; 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_REFERENCE; -import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.GROUPS; -import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.USERS; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.DEFAULT_ROLLOUT_PERCENTAGE; import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.FEATURE_LABEL; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.FEATURE_VALUE_ALL; import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.FEATURE_VALUE_TELEMETRY; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.GROUPS; import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_ENDPOINT; import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_E_TAG; -import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.FEATURE_VALUE_ALL; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.USERS; import static com.azure.spring.cloud.appconfiguration.config.implementation.TestUtils.createItemFeatureFlag; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -78,8 +78,7 @@ public void loadFeatureFlagsTestNoFeatureFlags() { FeatureFlags featureFlags = new FeatureFlags(null, settings); when(clientMock.listFeatureFlags(Mockito.any(), Mockito.anyBoolean())).thenReturn(featureFlags); - List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, - false); + List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, false); assertEquals(1, featureFlagsList.size()); assertEquals(featureFlags, featureFlagsList.get(0)); assertEquals("FakeKey", featureFlagsList.get(0).getFeatureFlags().get(0).getKey()); @@ -93,8 +92,7 @@ public void loadFeatureFlagsTestFeatureFlags() { FeatureFlags featureFlags = new FeatureFlags(null, settings); when(clientMock.listFeatureFlags(Mockito.any(), Mockito.anyBoolean())).thenReturn(featureFlags); - List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, - false); + List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, false); assertEquals(1, featureFlagsList.size()); assertEquals(featureFlags, featureFlagsList.get(0)); assertEquals(".appconfig.featureflag/Alpha", featureFlagsList.get(0).getFeatureFlags().get(0).getKey()); @@ -109,8 +107,7 @@ public void loadFeatureFlagsTestMultipleLoads() { FeatureFlags featureFlags = new FeatureFlags(null, settings); when(clientMock.listFeatureFlags(Mockito.any(), Mockito.anyBoolean())).thenReturn(featureFlags); - List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, - false); + List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, false); assertEquals(1, featureFlagsList.size()); assertEquals(featureFlags, featureFlagsList.get(0)); assertEquals(".appconfig.featureflag/Alpha", featureFlagsList.get(0).getFeatureFlags().get(0).getKey()); @@ -168,8 +165,7 @@ public void loadFeatureFlagsTestTargetingFilter() { FeatureFlags featureFlags = new FeatureFlags(null, settings); when(clientMock.listFeatureFlags(Mockito.any(), Mockito.anyBoolean())).thenReturn(featureFlags); - List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, - false); + List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, false); assertEquals(1, featureFlagsList.size()); assertEquals(featureFlags, featureFlagsList.get(0)); assertEquals(".appconfig.featureflag/TargetingTest", featureFlagsList.get(0).getFeatureFlags().get(0).getKey()); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/StateHolderTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/StateHolderTest.java index 184a33374bb7..3b10d736f8ec 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/StateHolderTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/StateHolderTest.java @@ -180,7 +180,7 @@ private void updateNextRefreshBackoffCalcTest(TestInfo testInfo) { private void loadStateTest(TestInfo testInfo) { String endpoint = testInfo.getDisplayName() + "updateRefreshTimeBackoffCalc" + ".azconfig.io"; StateHolder testStateHolder = new StateHolder(); - testStateHolder.setLoadState(endpoint, true, false); + testStateHolder.setLoadState(endpoint, true); StateHolder.updateState(testStateHolder); assertEquals(testStateHolder.getLoadState().get(endpoint), StateHolder.getLoadState(endpoint)); assertEquals(testStateHolder, StateHolder.getCurrentState()); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/TestConstants.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/TestConstants.java index 63903d1ef5ec..48e379747e3b 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/TestConstants.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/TestConstants.java @@ -6,6 +6,22 @@ * Test constants which can be shared across different test classes */ public final class TestConstants { + + /** + * Http Header Correlation Context + */ + public static final String CORRELATION_CONTEXT = "Correlation-Context"; + + /** + * App Configurations Key Vault Reference Content Type + */ + public static final String KEY_VAULT_CONTENT_TYPE = "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8"; + + public static final String USERS = "users"; + + public static final String GROUPS = "groups"; + + public static final String DEFAULT_ROLLOUT_PERCENTAGE = "defaultRolloutPercentage"; // Store specific configuration public static final String TEST_STORE_NAME = "store1"; diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/config/AppConfigurationBootstrapConfigurationTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/config/AppConfigurationBootstrapConfigurationTest.java deleted file mode 100644 index 93202173f069..000000000000 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/config/AppConfigurationBootstrapConfigurationTest.java +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.appconfiguration.config.implementation.config; - -import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.CONN_STRING_PROP; -import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.FAIL_FAST_PROP; -import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.STORE_ENDPOINT_PROP; -import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_CONN_STRING; -import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_STORE_NAME; -import static com.azure.spring.cloud.appconfiguration.config.implementation.TestUtils.propPair; -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationPropertySourceLocator; -import com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationReplicaClientFactory; -import com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalPropertiesAutoConfiguration; - -public class AppConfigurationBootstrapConfigurationTest { - - private static final ApplicationContextRunner CONTEXT_RUNNER = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(AppConfigurationBootstrapConfiguration.class, - AzureGlobalPropertiesAutoConfiguration.class)) - .withPropertyValues(propPair("spring.cloud.azure.appconfiguration.enabled", "true")); - - @Test - public void iniConnectionStringSystemAssigned() { - CONTEXT_RUNNER - .withPropertyValues(propPair(STORE_ENDPOINT_PROP, TEST_STORE_NAME), propPair(FAIL_FAST_PROP, "false")) - .run(context -> assertThat(context).hasSingleBean(AppConfigurationPropertySourceLocator.class)); - } - - @Test - public void iniConnectionStringUserAssigned() { - CONTEXT_RUNNER - .withPropertyValues(propPair(STORE_ENDPOINT_PROP, TEST_STORE_NAME), propPair(FAIL_FAST_PROP, "false"), - propPair("spring.cloud.azure.appconfiguration.managed-identity.client-id", "client-id")) - .run(context -> assertThat(context).hasSingleBean(AppConfigurationPropertySourceLocator.class)); - } - - @Test - public void propertySourceLocatorBeanCreated() { - CONTEXT_RUNNER - .withPropertyValues(propPair(CONN_STRING_PROP, TEST_CONN_STRING), propPair(FAIL_FAST_PROP, "false")) - .run(context -> assertThat(context).hasSingleBean(AppConfigurationPropertySourceLocator.class)); - } - - @Test - public void clientsBeanCreated() { - CONTEXT_RUNNER - .withPropertyValues(propPair(CONN_STRING_PROP, TEST_CONN_STRING)) - .run(context -> assertThat(context).hasSingleBean(AppConfigurationReplicaClientFactory.class)); - } -} diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/BaseAppConfigurationPolicyTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/BaseAppConfigurationPolicyTest.java index c08c29975ffa..d3bfdea27cef 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/BaseAppConfigurationPolicyTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/BaseAppConfigurationPolicyTest.java @@ -2,10 +2,7 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation.http.policy; -import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.CORRELATION_CONTEXT; -import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.DEV_ENV_TRACING; import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.KEY_VAULT_CONFIGURED_TRACING; -import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.USER_AGENT_TYPE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; @@ -24,11 +21,13 @@ import org.mockito.MockitoAnnotations; import org.mockito.junit.jupiter.MockitoExtension; +import com.azure.core.http.HttpHeaderName; import com.azure.core.http.HttpMethod; import com.azure.core.http.HttpPipelineCallContext; import com.azure.core.http.HttpPipelineNextPolicy; import com.azure.core.http.HttpRequest; import com.azure.core.util.Configuration; +import com.azure.spring.cloud.appconfiguration.config.implementation.RequestTracingConstants; // This test class needs to be isolated and ran sequential as it uses BaseAppConfigurationPolicy.setWatchRequests // which mutates a global static and can result in race condition failures. @@ -55,27 +54,28 @@ public void cleanup() throws Exception { MockitoAnnotations.openMocks(this).close(); } - @SuppressWarnings("deprecation") @Test public void startupThenWatchUpdateTest() throws MalformedURLException { URL url = new URL("https://www.test.url/kv"); HttpRequest request = new HttpRequest(HttpMethod.GET, url); - request.setHeader(USER_AGENT_TYPE, "PreExistingUserAgent"); + request.setHeader(HttpHeaderName.USER_AGENT, "PreExistingUserAgent"); BaseAppConfigurationPolicy policy = new BaseAppConfigurationPolicy( - new TracingInfo(false, false, 0, Configuration.getGlobalConfiguration())); + new TracingInfo(false, 0, Configuration.getGlobalConfiguration())); when(contextMock.getHttpRequest()).thenReturn(request); policy.process(contextMock, nextMock); - String userAgent = contextMock.getHttpRequest().getHeaders().get(USER_AGENT_TYPE).getValue(); + String userAgent = contextMock.getHttpRequest().getHeaders().get(HttpHeaderName.USER_AGENT).getValue(); assertEquals("null/null " + PRE_USER_AGENT, userAgent); assertEquals("RequestType=Startup", - contextMock.getHttpRequest().getHeaders().get(CORRELATION_CONTEXT).getValue()); + contextMock.getHttpRequest().getHeaders() + .get(HttpHeaderName.fromString(RequestTracingConstants.CORRELATION_CONTEXT_HEADER.toString())) + .getValue()); request = new HttpRequest(HttpMethod.GET, url); - request.setHeader(USER_AGENT_TYPE, "PreExistingUserAgent"); + request.setHeader(HttpHeaderName.USER_AGENT, "PreExistingUserAgent"); when(contextMock.getHttpRequest()).thenReturn(request); when(contextMock.getData("refresh")).thenReturn(Optional.of(true)); @@ -85,10 +85,12 @@ public void startupThenWatchUpdateTest() throws MalformedURLException { assertEquals("null/null " + PRE_USER_AGENT, userAgent); assertEquals("RequestType=Watch", - contextMock.getHttpRequest().getHeaders().get(CORRELATION_CONTEXT).getValue()); + contextMock.getHttpRequest().getHeaders() + .get(HttpHeaderName.fromString(RequestTracingConstants.CORRELATION_CONTEXT_HEADER.toString())) + .getValue()); request = new HttpRequest(HttpMethod.GET, url); - request.setHeader(USER_AGENT_TYPE, "PreExistingUserAgent"); + request.setHeader(HttpHeaderName.USER_AGENT, "PreExistingUserAgent"); when(contextMock.getHttpRequest()).thenReturn(request); @@ -96,55 +98,26 @@ public void startupThenWatchUpdateTest() throws MalformedURLException { assertEquals("null/null " + PRE_USER_AGENT, userAgent); assertEquals("RequestType=Watch", - contextMock.getHttpRequest().getHeaders().get(CORRELATION_CONTEXT).getValue()); + contextMock.getHttpRequest().getHeaders() + .get(HttpHeaderName.fromString(RequestTracingConstants.CORRELATION_CONTEXT_HEADER.toString())) + .getValue()); } - @SuppressWarnings("deprecation") - @Test - public void devIsConfigured() throws MalformedURLException { - BaseAppConfigurationPolicy policy = new BaseAppConfigurationPolicy( - new TracingInfo(true, false, 0, Configuration.getGlobalConfiguration())); - - URL url = new URL("https://www.test.url/kv"); - HttpRequest request = new HttpRequest(HttpMethod.GET, url); - request.setHeader(USER_AGENT_TYPE, "PreExistingUserAgent"); - when(contextMock.getHttpRequest()).thenReturn(request); - - policy.process(contextMock, nextMock); - assertEquals("RequestType=Startup,Env=" + DEV_ENV_TRACING, - contextMock.getHttpRequest().getHeaders().get(CORRELATION_CONTEXT).getValue()); - } - - @SuppressWarnings("deprecation") @Test public void keyVaultIsConfigured() throws MalformedURLException { BaseAppConfigurationPolicy policy = new BaseAppConfigurationPolicy( - new TracingInfo(false, true, 0, Configuration.getGlobalConfiguration())); + new TracingInfo(true, 0, Configuration.getGlobalConfiguration())); URL url = new URL("https://www.test.url/kv"); HttpRequest request = new HttpRequest(HttpMethod.GET, url); - request.setHeader(USER_AGENT_TYPE, "PreExistingUserAgent"); + request.setHeader(HttpHeaderName.USER_AGENT, "PreExistingUserAgent"); when(contextMock.getHttpRequest()).thenReturn(request); policy.process(contextMock, nextMock); assertEquals("RequestType=Startup," + KEY_VAULT_CONFIGURED_TRACING, - contextMock.getHttpRequest().getHeaders().get(CORRELATION_CONTEXT).getValue()); - } - - @SuppressWarnings("deprecation") - @Test - public void devAndKeyVaultAreConfigured() throws MalformedURLException { - BaseAppConfigurationPolicy policy = new BaseAppConfigurationPolicy( - new TracingInfo(true, true, 0, Configuration.getGlobalConfiguration())); - - URL url = new URL("https://www.test.url/kv"); - HttpRequest request = new HttpRequest(HttpMethod.GET, url); - request.setHeader(USER_AGENT_TYPE, "PreExistingUserAgent"); - when(contextMock.getHttpRequest()).thenReturn(request); - - policy.process(contextMock, nextMock); - assertEquals("RequestType=Startup,Env=" + DEV_ENV_TRACING + "," + KEY_VAULT_CONFIGURED_TRACING, - contextMock.getHttpRequest().getHeaders().get(CORRELATION_CONTEXT).getValue()); + contextMock.getHttpRequest().getHeaders() + .get(HttpHeaderName.fromString(RequestTracingConstants.CORRELATION_CONTEXT_HEADER.toString())) + .getValue()); } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfoTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfoTest.java index 67c3aba02153..2bdcdd238590 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfoTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfoTest.java @@ -23,20 +23,17 @@ public class TracingInfoTest { public void getValueTest() { Configuration configuration = getConfiguration("false"); - TracingInfo tracingInfo = new TracingInfo(false, false, 0, configuration); + TracingInfo tracingInfo = new TracingInfo(false, 0, configuration); assertEquals("RequestType=Startup", tracingInfo.getValue(false)); assertEquals("RequestType=Watch", tracingInfo.getValue(true)); - tracingInfo = new TracingInfo(true, false, 0, configuration); - assertEquals("RequestType=Startup,Env=Dev", tracingInfo.getValue(false)); - - tracingInfo = new TracingInfo(false, true, 0, configuration); + tracingInfo = new TracingInfo(true, 0, configuration); assertEquals("RequestType=Startup,UsesKeyVault", tracingInfo.getValue(false)); - tracingInfo = new TracingInfo(false, false, 1, configuration); + tracingInfo = new TracingInfo(false, 1, configuration); assertEquals("RequestType=Startup,ReplicaCount=1", tracingInfo.getValue(false)); - tracingInfo = new TracingInfo(false, false, 0, configuration); + tracingInfo = new TracingInfo(false, 0, configuration); tracingInfo.getFeatureFlagTracing().updateFeatureFilterTelemetry("Random"); assertEquals("RequestType=Startup,Filter=CSTM", tracingInfo.getValue(false)); @@ -44,19 +41,19 @@ public void getValueTest() { @Test public void disableTracingTest() { - TracingInfo tracingInfo = new TracingInfo(false, false, 0, getConfiguration(null)); + TracingInfo tracingInfo = new TracingInfo(false, 0, getConfiguration(null)); assertNotEquals("", tracingInfo.getValue(false)); - tracingInfo = new TracingInfo(false, false, 0, getConfiguration("")); + tracingInfo = new TracingInfo(false, 0, getConfiguration("")); assertNotEquals("", tracingInfo.getValue(false)); - tracingInfo = new TracingInfo(false, false, 0, getConfiguration("true")); + tracingInfo = new TracingInfo(false, 0, getConfiguration("true")); assertEquals("", tracingInfo.getValue(false)); - tracingInfo = new TracingInfo(false, false, 0, getConfiguration("false")); + tracingInfo = new TracingInfo(false, 0, getConfiguration("false")); assertNotEquals("", tracingInfo.getValue(false)); - tracingInfo = new TracingInfo(false, false, 0, getConfiguration("random string")); + tracingInfo = new TracingInfo(false, 0, getConfiguration("random string")); assertNotEquals("", tracingInfo.getValue(false)); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationPropertiesTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationPropertiesTest.java index e8d839e22faa..6641e1ee7108 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationPropertiesTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationPropertiesTest.java @@ -2,35 +2,18 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation.properties; -import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationReplicaClientsBuilder.ENDPOINT_ERR_MSG; -import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.CONN_STRING_PROP; -import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.CONN_STRING_PROP_NEW; -import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.FAIL_FAST_PROP; -import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.KEY_PROP; -import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.LABEL_PROP; -import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.REFRESH_INTERVAL_PROP; -import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.STORE_ENDPOINT_PROP; import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_CONN_STRING; import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_ENDPOINT; import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_ENDPOINT_GEO; -import static com.azure.spring.cloud.appconfiguration.config.implementation.TestUtils.propPair; -import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.time.Duration; import java.util.ArrayList; import java.util.List; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.MockitoAnnotations; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import com.azure.spring.cloud.appconfiguration.config.implementation.config.AppConfigurationBootstrapConfiguration; -import com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalPropertiesAutoConfiguration; public class AppConfigurationPropertiesTest { @@ -43,29 +26,21 @@ public class AppConfigurationPropertiesTest { private static final String VALID_KEY = "/application/"; private static final String ILLEGAL_LABELS = "*,my-label"; + + private AppConfigurationProperties properties; - @InjectMocks - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(AppConfigurationBootstrapConfiguration.class, - AzureGlobalPropertiesAutoConfiguration.class)) - .withPropertyValues("spring.cloud.azure.appconfiguration.endpoint=https://test-appconfig.azconfig.io"); @BeforeEach public void setup() { - MockitoAnnotations.openMocks(this); - } - - @AfterEach - public void cleanup() throws Exception { - MockitoAnnotations.openMocks(this).close(); + properties = new AppConfigurationProperties(); + properties.setStores(List.of(new ConfigStore())); } @Test public void validInputShouldCreatePropertiesBean() { - this.contextRunner - .withPropertyValues(propPair(CONN_STRING_PROP, TEST_CONN_STRING)) - .withPropertyValues(propPair(FAIL_FAST_PROP, "false")) - .run(context -> assertThat(context).hasSingleBean(AppConfigurationProperties.class)); + ConfigStore store = properties.getStores().get(0); + store.setConnectionString(TEST_CONN_STRING); + store.validateAndInit(); } @Test @@ -84,55 +59,54 @@ public void secretMustExistInConnectionString() { } private void testConnStringFields(String connString) { - this.contextRunner - .withPropertyValues(propPair(CONN_STRING_PROP, connString)) - .run(context -> assertThat(context).getFailure().hasStackTraceContaining(ENDPOINT_ERR_MSG)); + ConfigStore store = properties.getStores().get(0); + store.setConnectionString(connString); + IllegalStateException e = assertThrows(IllegalStateException.class, () -> properties.validateAndInit()); + assertEquals("Connection string does not follow format Endpoint=([^;]+);Id=([^;]+);Secret=([^;]+).", e.getMessage()); } @Test public void asteriskShouldNotBeIncludedInTheLabels() { - this.contextRunner - .withPropertyValues( - propPair(CONN_STRING_PROP, TEST_CONN_STRING), - propPair(KEY_PROP, VALID_KEY), - propPair(LABEL_PROP, ILLEGAL_LABELS)) - .run(context -> assertThat(context) - .getFailure() - .hasStackTraceContaining("LabelFilter must not contain asterisk(*)")); + ConfigStore store = properties.getStores().get(0); + store.setConnectionString(TEST_CONN_STRING); + AppConfigurationKeyValueSelector select = new AppConfigurationKeyValueSelector(); + select.setKeyFilter(VALID_KEY); + select.setLabelFilter(ILLEGAL_LABELS); + store.setSelects(List.of(select)); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> properties.validateAndInit()); + assertEquals("LabelFilter must not contain asterisk(*)", e.getMessage()); } @Test public void storeNameCanBeInitIfConnectionStringConfigured() { - this.contextRunner - .withPropertyValues( - propPair(CONN_STRING_PROP, TEST_CONN_STRING), - propPair(STORE_ENDPOINT_PROP, "")) - .withPropertyValues(propPair(FAIL_FAST_PROP, "false")) - .run(context -> { - AppConfigurationProperties properties = context.getBean(AppConfigurationProperties.class); - assertThat(properties.getStores()).isNotNull(); - assertThat(properties.getStores().size()).isEqualTo(1); - assertThat(properties.getStores().get(0).getEndpoint()).isEqualTo("https://fake.test.config.io"); - }); + ConfigStore store = properties.getStores().get(0); + store.setConnectionString(TEST_CONN_STRING); + store.setEndpoint(""); + store.validateAndInit(); + assertEquals(1, properties.getStores().size()); + assertEquals("https://fake.test.config.io", properties.getStores().get(0).getEndpoint()); } @Test public void duplicateConnectionStringIsNotAllowed() { - this.contextRunner - .withPropertyValues( - propPair(CONN_STRING_PROP, TEST_CONN_STRING), - propPair(CONN_STRING_PROP_NEW, TEST_CONN_STRING)) - .run(context -> assertThat(context) - .getFailure() - .hasStackTraceContaining("Duplicate store name exists")); + properties = new AppConfigurationProperties(); + properties.setStores(List.of(new ConfigStore(), new ConfigStore())); + + ConfigStore store = properties.getStores().get(0); + store.setConnectionString(TEST_CONN_STRING); + ConfigStore newStore = properties.getStores().get(1); + newStore.setConnectionString(TEST_CONN_STRING); + + java.lang.IllegalArgumentException e = assertThrows(java.lang.IllegalArgumentException.class, () -> properties.validateAndInit()); + assertEquals("Duplicate store name exists.", e.getMessage()); } @Test public void minValidWatchTime() { - this.contextRunner - .withPropertyValues(propPair(CONN_STRING_PROP, TEST_CONN_STRING)) - .withPropertyValues(propPair(REFRESH_INTERVAL_PROP, "1s")) - .run(context -> assertThat(context).hasSingleBean(AppConfigurationProperties.class)); + ConfigStore store = properties.getStores().get(0); + store.setConnectionString(TEST_CONN_STRING); + properties.setRefreshInterval(Duration.ofSeconds(1)); + properties.validateAndInit(); } @Test diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/ValidationsTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/ValidationsTest.java deleted file mode 100644 index 35d43967f85e..000000000000 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/ValidationsTest.java +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.feature.management; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.Mockito.when; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.nio.file.Files; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.stream.Stream; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.ApplicationContext; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import com.azure.spring.cloud.feature.management.filters.TargetingFilter; -import com.azure.spring.cloud.feature.management.filters.TargetingFilterTestContextAccessor; -import com.azure.spring.cloud.feature.management.filters.TimeWindowFilter; -import com.azure.spring.cloud.feature.management.implementation.FeatureManagementConfigProperties; -import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties; -import com.azure.spring.cloud.feature.management.validationstests.models.ValidationTestCase; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.MapperFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.json.JsonMapper; -import com.fasterxml.jackson.databind.type.CollectionType; -import com.fasterxml.jackson.databind.type.TypeFactory; - -@ExtendWith(SpringExtension.class) -public class ValidationsTest { - @Mock - private ApplicationContext context; - - @Mock - private FeatureManagementConfigProperties configProperties; - - private static final Logger LOGGER = LoggerFactory.getLogger(ValidationsTest.class); - - private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder() - .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build(); - - private static final String TEST_CASE_FOLDER_PATH = "validations-tests"; - - private final String inputsUser = "user"; - - private final String inputsGroups = "groups"; - - private static final String SAMPLE_FILE_NAME_FILTER = "sample"; - - private static final String TESTS_FILE_NAME_FILTER = "tests"; - - @BeforeEach - public void setup() { - MockitoAnnotations.openMocks(this); - when(configProperties.isFailFast()).thenReturn(true); - when(context.getBean(Mockito.contains("TimeWindow"))).thenReturn(new TimeWindowFilter()); - } - - @AfterEach - public void cleanup() throws Exception { - MockitoAnnotations.openMocks(this).close(); - } - - private boolean hasException(ValidationTestCase testCase) { - final String exceptionStr = testCase.getIsEnabled().getException(); - return exceptionStr != null && !exceptionStr.isEmpty(); - } - - private boolean hasInput(ValidationTestCase testCase) { - final LinkedHashMap inputsMap = testCase.getInputs(); - return inputsMap != null && !inputsMap.isEmpty(); - } - - private static File[] getFileList(String fileNameFilter) { - final URL folderUrl = Thread.currentThread().getContextClassLoader().getResource(TEST_CASE_FOLDER_PATH); - assert folderUrl != null; - - final File folderFile = new File(folderUrl.getFile()); - final File[] filteredFiles = folderFile - .listFiles(pathname -> pathname.getName().toLowerCase().contains(fileNameFilter)); - assert filteredFiles != null; - - Arrays.sort(filteredFiles, Comparator.comparing(File::getName)); - return filteredFiles; - } - - private List readTestcasesFromFile(File testFile) throws IOException { - final String jsonString = Files.readString(testFile.toPath()); - final CollectionType typeReference = TypeFactory.defaultInstance().constructCollectionType(List.class, - ValidationTestCase.class); - return OBJECT_MAPPER.readValue(jsonString, typeReference); - } - - @SuppressWarnings("unchecked") - private static LinkedHashMap readConfigurationFromFile(File sampleFile) throws IOException { - final String jsonString = Files.readString(sampleFile.toPath()); - final LinkedHashMap configurations = OBJECT_MAPPER.readValue(jsonString, new TypeReference<>() { - }); - final Object featureManagementSection = configurations.get("feature_management"); - if (featureManagementSection.getClass().isAssignableFrom(LinkedHashMap.class)) { - return (LinkedHashMap) featureManagementSection; - } - throw new IllegalArgumentException("feature_management part is not a map"); - } - - static Stream testProvider() throws IOException { - List arguments = new ArrayList<>(); - File[] files = getFileList(TESTS_FILE_NAME_FILTER); - - final File[] sampleFiles = getFileList(SAMPLE_FILE_NAME_FILTER); - List properties = new ArrayList<>(); - for (File sampleFile : sampleFiles) { - final FeatureManagementProperties managementProperties = new FeatureManagementProperties(); - managementProperties.putAll(readConfigurationFromFile(sampleFile)); - properties.add(managementProperties); - } - - for (int i = 0; i < files.length; i++) { - if (files[i].getName().contains(("TargetingFilter"))) { - continue; // TODO(mametcal). Not run the test case until we release the little endian fix - } - arguments.add(Arguments.of(files[i].getName(), files[i], properties.get(i))); - } - - return arguments.stream(); - } - - @ParameterizedTest(name = "{0}") - @MethodSource("testProvider") - void validationTest(String name, File testsFile, FeatureManagementProperties managementProperties) - throws IOException { - LOGGER.debug("Running test case from file: " + name); - final FeatureManager featureManager = new FeatureManager(context, managementProperties, configProperties); - List testCases = readTestcasesFromFile(testsFile); - for (ValidationTestCase testCase : testCases) { - LOGGER.debug("Test case : " + testCase.getDescription()); - if (hasException(testCase)) { // TODO(mametcal). Currently we didn't throw the exception when parameter is - // invalid - assertNull(managementProperties.getOnOff().get(testCase.getFeatureFlagName())); - continue; - } - if (hasInput(testCase)) { // Set inputs - final Object userObj = testCase.getInputs().get(inputsUser); - final Object groupsObj = testCase.getInputs().get(inputsGroups); - final String user = userObj != null ? userObj.toString() : null; - @SuppressWarnings("unchecked") - final List groups = groupsObj != null ? (List) groupsObj : null; - when(context.getBean(Mockito.contains("Targeting"))) - .thenReturn(new TargetingFilter(new TargetingFilterTestContextAccessor(user, groups))); - } - - final Boolean result = featureManager.isEnabled(testCase.getFeatureFlagName()); - assertEquals(result.toString(), testCase.getIsEnabled().getResult()); - } - } -} From a6d8ed1c7d77e2c8a1dbbaa98812c4bb8ba4eecb Mon Sep 17 00:00:00 2001 From: Matthew Metcalf Date: Mon, 3 Feb 2025 11:56:46 -0800 Subject: [PATCH 03/41] Fix Endian (#43932) * FixEndian * rename method --- .../management/filters/TargetingFilter.java | 41 +++-------------- .../implementation/FeatureFilterUtils.java | 44 +++++++++++++++++++ .../filters/TargetingFilterTest.java | 4 +- .../TargetingFilterUtilsTest.java | 20 +++++++++ 4 files changed, 72 insertions(+), 37 deletions(-) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implemenation/TargetingFilterUtilsTest.java diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java index bea9aa4879db..7028570729d9 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java @@ -2,21 +2,17 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.management.filters; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import com.azure.spring.cloud.feature.management.implementation.FeatureFilterUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; +import com.azure.spring.cloud.feature.management.implementation.FeatureFilterUtils; import com.azure.spring.cloud.feature.management.implementation.targeting.Audience; import com.azure.spring.cloud.feature.management.implementation.targeting.Exclusion; import com.azure.spring.cloud.feature.management.implementation.targeting.GroupRollout; @@ -133,8 +129,10 @@ public boolean evaluate(FeatureFilterEvaluationContext context) { Audience audience; String exclusionValue = FeatureFilterUtils.getKeyCase(parameters, EXCLUSION_CAMEL); - String exclusionUserValue = FeatureFilterUtils.getKeyCase((Map) parameters.get(exclusionValue), "Users"); - String exclusionGroupsValue = FeatureFilterUtils.getKeyCase((Map) parameters.get(exclusionValue), "Groups"); + String exclusionUserValue = FeatureFilterUtils.getKeyCase((Map) parameters.get(exclusionValue), + "Users"); + String exclusionGroupsValue = FeatureFilterUtils + .getKeyCase((Map) parameters.get(exclusionValue), "Groups"); if (((Map) parameters.getOrDefault(exclusionValue, new HashMap<>())) .get(exclusionUserValue) instanceof List) { @@ -227,35 +225,8 @@ private boolean validateTargetingContext(TargetingFilterContext targetingContext return (!hasUserDefined && !(hasGroupsDefined && hasAtLeastOneGroup)); } - /** - * Computes the percentage that the contextId falls into. - * - * @param contextId Id of the context being targeted - * @return the bucket value of the context id - * @throws TargetingException Unable to create hash of target context - */ - protected double isTargetedPercentage(String contextId) { - byte[] hash = null; - - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - hash = digest.digest(contextId.getBytes(Charset.defaultCharset())); - } catch (NoSuchAlgorithmException e) { - throw new TargetingException("Unable to find SHA-256 for targeting.", e); - } - - if (hash == null) { - throw new TargetingException("Unable to create Targeting Hash for " + contextId); - } - - ByteBuffer wrapped = ByteBuffer.wrap(hash); - int contextMarker = Math.abs(wrapped.getInt()); - - return (contextMarker / (double) Integer.MAX_VALUE) * 100; - } - private boolean isTargeted(String contextId, double percentage) { - return isTargetedPercentage(contextId) < percentage; + return FeatureFilterUtils.isTargetedPercentage(contextId) < percentage; } /** diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java index a8631bfc9366..90deefc80b0c 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java @@ -3,11 +3,16 @@ package com.azure.spring.cloud.feature.management.implementation; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Collection; import java.util.Map; import org.springframework.util.StringUtils; +import com.azure.spring.cloud.feature.management.models.TargetingException; + public class FeatureFilterUtils { /** @@ -34,4 +39,43 @@ public static String getKeyCase(Map parameters, String key) { return StringUtils.uncapitalize(key); } + /** + * Computes the percentage that the contextId falls into. + * + * @param contextId Id of the context being targeted + * @return the bucket value of the context id + * @throws TargetingException Unable to create hash of target context + */ + public static double isTargetedPercentage(String contextId) { + byte[] hash = null; + if (contextId == null) { + contextId = "\n"; + } + + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + hash = digest.digest(contextId.getBytes()); + + } catch (NoSuchAlgorithmException e) { + throw new TargetingException("Unable to find SHA-256 for targeting.", e); + } + + if (hash == null) { + throw new TargetingException("Unable to create Targeting Hash for " + contextId); + } + + BigInteger bi = bigEndianToLittleEndian(hash); + + return (bi.longValue() / (Math.pow(2, 32) - 1)) * 100; + } + + public static BigInteger bigEndianToLittleEndian(byte[] bigEndian) { + byte[] reversedBytes = new byte[4]; + for (int i = 0; i < 4; i++) { + reversedBytes[i] = bigEndian[3 - i]; + } + + return new BigInteger(1, reversedBytes); + } + } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java index 15b4c271da7e..ed45ced91d9c 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java @@ -218,7 +218,7 @@ public void targetedGroupFiftyPass() { TargetingFilter filter = new TargetingFilter(new TargetingFilterTestContextAccessor("Jane", targetedGroups)); - assertTrue(filter.evaluate(context)); + assertFalse(filter.evaluate(context)); } @Test @@ -246,7 +246,7 @@ public void targetedGroupFiftyFalse() { TargetingFilter filter = new TargetingFilter(new TargetingFilterTestContextAccessor("Doe", targetedGroups)); - assertFalse(filter.evaluate(context)); + assertTrue(filter.evaluate(context)); } @Test diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implemenation/TargetingFilterUtilsTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implemenation/TargetingFilterUtilsTest.java new file mode 100644 index 000000000000..ab85d8a7c543 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implemenation/TargetingFilterUtilsTest.java @@ -0,0 +1,20 @@ +package com.azure.spring.cloud.feature.management.implemenation; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import com.azure.spring.cloud.feature.management.implementation.FeatureFilterUtils; + +public class TargetingFilterUtilsTest { + + @Test + public void isTargetedPercentageTest() { + assertEquals(FeatureFilterUtils.isTargetedPercentage(null), 9.875071074318855); + assertEquals(FeatureFilterUtils.isTargetedPercentage(""), 26.0813765987012); + assertEquals(FeatureFilterUtils.isTargetedPercentage("Alice"), 38.306839656621875); + assertEquals(FeatureFilterUtils.isTargetedPercentage("Quinn\nDeb"), 38.306839656621875); + assertEquals(FeatureFilterUtils.isTargetedPercentage("\nProd"), 79.98622464481421); + } + +} From d3750bf731df65ddad6a5eab35d9168e885694bb Mon Sep 17 00:00:00 2001 From: Matthew Metcalf Date: Wed, 5 Feb 2025 15:55:17 -0800 Subject: [PATCH 04/41] Feature context and Async Filters (#43435) * Object Context and AsyncFilters * Apply suggestions from code review Co-authored-by: Ross Grambo * Review comments * Review comments * Review Comments --------- Co-authored-by: Ross Grambo --- .../feature/management/FeatureManager.java | 115 +++++++++++++----- .../filters/ContextualFeatureFilter.java | 25 ++++ .../filters/ContextualFeatureFilterAsync.java | 27 ++++ .../filters/FeatureFilterAsync.java | 25 ++++ .../management/filters/TargetingFilter.java | 65 ++++++---- .../implementation/FeatureFilterUtils.java | 9 +- .../FeatureManagementConstants.java | 2 + .../feature/management/models/Feature.java | 3 + .../management/FeatureManagerTest.java | 25 ++++ .../filters/TargetingFilterTest.java | 30 ++++- 10 files changed, 263 insertions(+), 63 deletions(-) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilter.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilterAsync.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/FeatureFilterAsync.java diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java index 9574a65703e0..97f864d0060a 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java @@ -2,10 +2,13 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.management; +import static com.azure.spring.cloud.feature.management.implementation.FeatureManagementConstants.ALL_REQUIREMENT_TYPE; + +import java.time.Duration; +import java.util.ArrayList; import java.util.HashSet; -import java.util.Objects; +import java.util.List; import java.util.Set; -import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,13 +16,18 @@ import org.springframework.context.ApplicationContext; import org.springframework.util.ReflectionUtils; +import com.azure.spring.cloud.feature.management.filters.ContextualFeatureFilter; +import com.azure.spring.cloud.feature.management.filters.ContextualFeatureFilterAsync; import com.azure.spring.cloud.feature.management.filters.FeatureFilter; +import com.azure.spring.cloud.feature.management.filters.FeatureFilterAsync; import com.azure.spring.cloud.feature.management.implementation.FeatureManagementConfigProperties; import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties; +import com.azure.spring.cloud.feature.management.models.Conditions; import com.azure.spring.cloud.feature.management.models.Feature; import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext; import com.azure.spring.cloud.feature.management.models.FilterNotFoundException; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** @@ -34,6 +42,8 @@ public class FeatureManager { private final FeatureManagementProperties featureManagementConfigurations; private transient FeatureManagementConfigProperties properties; + + private static final Duration DEFAULT_REQUEST_TIMEOUT = Duration.ofSeconds(100); /** * Can be called to check if a feature is enabled or disabled. @@ -59,7 +69,7 @@ public class FeatureManager { * @throws FilterNotFoundException file not found */ public Mono isEnabledAsync(String feature) { - return Mono.just(checkFeature(feature)); + return checkFeature(feature, null); } /** @@ -72,48 +82,93 @@ public Mono isEnabledAsync(String feature) { * @throws FilterNotFoundException file not found */ public Boolean isEnabled(String feature) throws FilterNotFoundException { - return checkFeature(feature); + return checkFeature(feature, null).block(DEFAULT_REQUEST_TIMEOUT); + } + + /** + * Checks to see if the feature is enabled. If enabled it check each filter, once a single filter returns true it + * returns true. If no filter returns true, it returns false. If there are no filters, it returns true. If feature + * isn't found it returns false. + * + * @param feature Feature being checked. + * @param featureContext Local context + * @return state of the feature + * @throws FilterNotFoundException file not found + */ + public Mono isEnabledAsync(String feature, Object featureContext) { + return checkFeature(feature, featureContext); } - private boolean checkFeature(String featureName) throws FilterNotFoundException { + /** + * Checks to see if the feature is enabled. If enabled it checks each filter, once a single filter returns true it + * returns true. If no filter returns true, it returns false. If there are no filters, it returns true. If feature + * isn't found it returns false. + * + * @param feature Feature being checked. + * @param featureContext Local context + * @return state of the feature + * @throws FilterNotFoundException file not found + */ + public Boolean isEnabled(String feature, Object featureContext) throws FilterNotFoundException { + return checkFeature(feature, featureContext).block(DEFAULT_REQUEST_TIMEOUT); + } + + private Mono checkFeature(String featureName, Object featureContext) throws FilterNotFoundException { Feature featureFlag = featureManagementConfigurations.getFeatureFlags().stream() .filter(feature -> feature.getId().equals(featureName)).findAny().orElse(null); if (featureFlag == null) { - return false; + return Mono.just(false); } - Stream filters = featureFlag.getConditions().getClientFilters().stream() - .filter(Objects::nonNull).filter(featureFilter -> featureFilter.getName() != null); - if (featureFlag.getConditions().getClientFilters().size() == 0) { - return featureFlag.isEnabled(); - } - - // All Filters must be true - if (featureFlag.getConditions().getRequirementType().equals("All")) { - return filters.allMatch(featureFilter -> isFeatureOn(featureFilter, featureName)); + return Mono.just(featureFlag.isEnabled()); } - // Any Filter must be true - return filters.anyMatch(featureFilter -> isFeatureOn(featureFilter, featureName)); + return checkFeatureFilters(featureFlag, featureContext); } - private boolean isFeatureOn(FeatureFilterEvaluationContext filter, String feature) { - try { - FeatureFilter featureFilter = (FeatureFilter) context.getBean(filter.getName()); - filter.setFeatureName(feature); - - return featureFilter.evaluate(filter); - } catch (NoSuchBeanDefinitionException e) { - LOGGER.error("Was unable to find Filter {}. Does the class exist and set as an @Component?", - filter.getName()); - if (properties.isFailFast()) { - String message = "Fail fast is set and a Filter was unable to be found"; - ReflectionUtils.rethrowRuntimeException(new FilterNotFoundException(message, e, filter)); + private Mono checkFeatureFilters(Feature featureFlag, Object featureContext) { + Conditions conditions = featureFlag.getConditions(); + List featureFilters = conditions.getClientFilters(); + + if (featureFilters.size() == 0) { + return Mono.just(true); + } + + List> filterResults = new ArrayList>(); + for (FeatureFilterEvaluationContext featureFilter : featureFilters) { + String filterName = featureFilter.getName(); + + try { + Object filter = context.getBean(filterName); + featureFilter.setFeatureName(featureFlag.getId()); + if (filter instanceof FeatureFilter) { + filterResults.add(Mono.just(((FeatureFilter) filter).evaluate(featureFilter))); + } else if (filter instanceof ContextualFeatureFilter) { + filterResults + .add(Mono.just(((ContextualFeatureFilter) filter).evaluate(featureFilter, featureContext))); + } else if (filter instanceof FeatureFilterAsync) { + filterResults.add(((FeatureFilterAsync) filter).evaluateAsync(featureFilter)); + } else if (filter instanceof ContextualFeatureFilterAsync) { + filterResults + .add(((ContextualFeatureFilterAsync) filter).evaluateAsync(featureFilter, featureContext)); + } + } catch (NoSuchBeanDefinitionException e) { + LOGGER.error("Was unable to find Filter {}. Does the class exist and set as an @Component?", + filterName); + if (properties.isFailFast()) { + String message = "Fail fast is set and a Filter was unable to be found"; + ReflectionUtils.rethrowRuntimeException(new FilterNotFoundException(message, e, featureFilter)); + } } } - return false; + + if (ALL_REQUIREMENT_TYPE.equals(featureFlag.getConditions().getRequirementType())) { + return Flux.merge(filterResults).reduce((a, b) -> a && b).single(); + } + // Any Filter must be true + return Flux.merge(filterResults).reduce((a, b) -> a || b).single(); } /** diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilter.java new file mode 100644 index 000000000000..8432c1fbc3a9 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilter.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.management.filters; + +import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext; + +/** + * A Filter for Feature Management that is attached to Features. The filter needs to have @Component set to be found by + * feature management. As a Contextual feature filter any context that is passed in to the feature request will be + * passed along to the filter(s). + */ +@FunctionalInterface +public interface ContextualFeatureFilter { + + /** + * Evaluates if the filter is on or off. Returning true results in Feature evaluation ending and returning true. + * Returning false results in the next Feature evaluation to continue. + * + * @param context The context for whether or not the filter is passed. + * @param appContext The internal app context + * @return True if the feature is enabled, false otherwise. + */ + boolean evaluate(FeatureFilterEvaluationContext context, Object appContext); + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilterAsync.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilterAsync.java new file mode 100644 index 000000000000..6725e88ca6b7 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilterAsync.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.management.filters; + +import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext; + +import reactor.core.publisher.Mono; + +/** + * A Filter for Feature Management that is attached to Features. The filter needs to have @Component set to be found by + * feature management. As a Contextual feature filter any context that is passed in to the feature request will be + * passed along to the filter(s). + */ +@FunctionalInterface +public interface ContextualFeatureFilterAsync { + + /** + * Evaluates if the filter is on or off. Returning true results in Feature evaluation ending and returning true. + * Returning false results in the next Feature evaluation to continue. + * + * @param context The context for whether or not the filter is passed. + * @param appContext The internal app context + * @return true if the feature is enabled, false otherwise. + */ + Mono evaluateAsync(FeatureFilterEvaluationContext context, Object appContext); + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/FeatureFilterAsync.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/FeatureFilterAsync.java new file mode 100644 index 000000000000..e484a2795045 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/FeatureFilterAsync.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.management.filters; + +import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext; + +import reactor.core.publisher.Mono; + +/** + * A Filter for Feature Management that is attached to Features. The filter needs to have @Component set to be found by + * feature management. + */ +@FunctionalInterface +public interface FeatureFilterAsync { + + /** + * Evaluates if the filter is on or off. Returning true results in Feature evaluation ending and returning true. + * Returning false results in the next Feature evaluation to continue. + * + * @param context The context for whether or not the filter is passed. + * @return True if the feature is enabled, false otherwise. + */ + Mono evaluateAsync(FeatureFilterEvaluationContext context); + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java index 7028570729d9..fcaa357aa947 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java @@ -3,6 +3,7 @@ package com.azure.spring.cloud.feature.management.filters; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -14,10 +15,10 @@ import com.azure.spring.cloud.feature.management.implementation.FeatureFilterUtils; import com.azure.spring.cloud.feature.management.implementation.targeting.Audience; -import com.azure.spring.cloud.feature.management.implementation.targeting.Exclusion; import com.azure.spring.cloud.feature.management.implementation.targeting.GroupRollout; import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext; import com.azure.spring.cloud.feature.management.models.TargetingException; +import com.azure.spring.cloud.feature.management.targeting.TargetingContext; import com.azure.spring.cloud.feature.management.targeting.TargetingContextAccessor; import com.azure.spring.cloud.feature.management.targeting.TargetingEvaluationOptions; import com.azure.spring.cloud.feature.management.targeting.TargetingFilterContext; @@ -28,7 +29,7 @@ /** * `Microsoft.TargetingFilter` enables evaluating a user/group/overall rollout of a feature. */ -public class TargetingFilter implements FeatureFilter { +public class TargetingFilter implements FeatureFilter, ContextualFeatureFilter { private static final Logger LOGGER = LoggerFactory.getLogger(TargetingFilter.class); @@ -50,10 +51,8 @@ public class TargetingFilter implements FeatureFilter { /** * Audience that always returns false */ - protected static final String EXCLUSION = "exclusion"; - private static final String EXCLUSION_CAMEL = "Exclusion"; - + protected static final String EXCLUSION = "Exclusion"; /** * Error message for when the total Audience value is greater than 100 percent. */ @@ -99,15 +98,27 @@ public TargetingFilter(TargetingContextAccessor contextAccessor, TargetingEvalua } @Override - @SuppressWarnings("unchecked") public boolean evaluate(FeatureFilterEvaluationContext context) { + return evaluate(context, null); + } + + @SuppressWarnings("unchecked") + @Override + public boolean evaluate(FeatureFilterEvaluationContext context, Object appContext) { + if (context == null) { throw new IllegalArgumentException("Targeting Context not configured."); } - TargetingFilterContext targetingContext = new TargetingFilterContext(); + TargetingContext targetingContext = new TargetingFilterContext(); - contextAccessor.configureTargetingContext(targetingContext); + if (appContext != null && appContext instanceof TargetingContext) { + // Use this if, there is an appContext + the contextualAccessor, or there is no contextAccessor. + targetingContext = (TargetingContext) appContext; + } else if (contextAccessor != null) { + // If this is the only one provided just use it. + contextAccessor.configureTargetingContext(targetingContext); + } if (validateTargetingContext(targetingContext)) { LOGGER.warn("No targeting context available for targeting evaluation."); @@ -121,11 +132,8 @@ public boolean evaluate(FeatureFilterEvaluationContext context) { parameters = (Map) audienceObject; } - // The correct value is Users/Groups but keeping users/groups to make sure no one is broken. - FeatureFilterUtils.updateValueFromMapToList(parameters, USERS); - FeatureFilterUtils.updateValueFromMapToList(parameters, USERS.toLowerCase()); - FeatureFilterUtils.updateValueFromMapToList(parameters, GROUPS); - FeatureFilterUtils.updateValueFromMapToList(parameters, GROUPS.toLowerCase()); + FeatureFilterUtils.updateValueFromMapToList(parameters, USERS, true); + FeatureFilterUtils.updateValueFromMapToList(parameters, GROUPS, true); Audience audience; String exclusionValue = FeatureFilterUtils.getKeyCase(parameters, EXCLUSION_CAMEL); @@ -143,20 +151,21 @@ public boolean evaluate(FeatureFilterEvaluationContext context) { if (exclusionMap == null) { exclusionMap = new HashMap<>(); } - - audience = OBJECT_MAPPER.convertValue(parameters, Audience.class); - - Exclusion exclusion = new Exclusion(); + Object users = exclusionMap.get(exclusionUserValue); Object groups = exclusionMap.get(exclusionGroupsValue); + + Map exclusion = new HashMap<>(); if (users instanceof Map) { - exclusion.setUsers(new ArrayList<>(((Map) users).values())); + exclusion.put(USERS, new ArrayList<>(((Map) users).values())); } if (groups instanceof Map) { - exclusion.setGroups(new ArrayList<>(((Map) groups).values())); + exclusion.put(GROUPS, new ArrayList<>(((Map) groups).values())); } - audience.setExclusion(exclusion); + parameters.put(exclusionValue, exclusion); + + audience = OBJECT_MAPPER.convertValue(parameters, Audience.class); } validateSettings(audience); @@ -194,17 +203,21 @@ public boolean evaluate(FeatureFilterEvaluationContext context) { return isTargeted(defaultContextId, audience.getDefaultRolloutPercentage()); } - private boolean targetUser(String userId, List users) { + private boolean targetUser(String userId, Collection users) { return userId != null && users != null && users.stream().anyMatch(user -> equals(userId, user)); } - private boolean targetGroup(Audience audience, TargetingFilterContext targetingContext, + private boolean targetGroup(Audience audience, TargetingContext targetingContext, FeatureFilterEvaluationContext context, String group) { Optional groupRollout = audience.getGroups().stream() .filter(g -> equals(g.getName(), group)).findFirst(); if (groupRollout.isPresent()) { - String audienceContextId = targetingContext.getUserId() + "\n" + context.getName() + "\n" + group; + String userId = ""; + if (targetingContext.getUserId() != null) { + userId = targetingContext.getUserId(); + } + String audienceContextId = userId + "\n" + context.getFeatureName() + "\n" + group; if (isTargeted(audienceContextId, groupRollout.get().getRolloutPercentage())) { return true; @@ -213,7 +226,7 @@ private boolean targetGroup(Audience audience, TargetingFilterContext targetingC return false; } - private boolean validateTargetingContext(TargetingFilterContext targetingContext) { + private boolean validateTargetingContext(TargetingContext targetingContext) { boolean hasUserDefined = StringUtils.hasText(targetingContext.getUserId()); boolean hasGroupsDefined = targetingContext.getGroups() != null; boolean hasAtLeastOneGroup = false; @@ -253,8 +266,8 @@ void validateSettings(Audience audience) { throw new TargetingException(paramName + " : " + reason); } - List groups = audience.getGroups(); - if (groups != null) { + if (audience.getGroups() != null) { + List groups = new ArrayList(audience.getGroups()); for (int index = 0; index < groups.size(); index++) { GroupRollout groupRollout = groups.get(index); if (groupRollout.getRolloutPercentage() < 0 || groupRollout.getRolloutPercentage() > 100) { diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java index 90deefc80b0c..cb25caac6203 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java @@ -15,6 +15,10 @@ public class FeatureFilterUtils { + public static void updateValueFromMapToList(Map parameters, String key) { + updateValueFromMapToList(parameters, key, false); + } + /** * Looks at the given key in the parameters and coverts it to a list if it is currently a map. * @@ -22,11 +26,13 @@ public class FeatureFilterUtils { * @param key key of object int the parameters map */ @SuppressWarnings("unchecked") - public static void updateValueFromMapToList(Map parameters, String key) { + public static void updateValueFromMapToList(Map parameters, String key, boolean fixNull) { Object objectMap = parameters.get(key); if (objectMap instanceof Map) { Collection toType = ((Map) objectMap).values(); parameters.put(key, toType); + } else if ((objectMap != null && objectMap.equals("")) || (objectMap == null && fixNull)) { + parameters.put(key, new ArrayList()); } else if (objectMap != null) { parameters.put(key, objectMap); } @@ -74,7 +80,6 @@ public static BigInteger bigEndianToLittleEndian(byte[] bigEndian) { for (int i = 0; i < 4; i++) { reversedBytes[i] = bigEndian[3 - i]; } - return new BigInteger(1, reversedBytes); } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementConstants.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementConstants.java index 1ce1fc46aca3..64165c5ddedd 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementConstants.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementConstants.java @@ -5,5 +5,7 @@ public class FeatureManagementConstants { public static final String DEFAULT_REQUIREMENT_TYPE = "Any"; + + public static final String ALL_REQUIREMENT_TYPE = "All"; } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java index de00de138176..e3b43b0013c4 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java @@ -3,6 +3,8 @@ package com.azure.spring.cloud.feature.management.models; +import org.springframework.lang.NonNull; + import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @@ -21,6 +23,7 @@ public class Feature { private boolean enabled; @JsonProperty("conditions") + @NonNull private Conditions conditions = new Conditions(); /** diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java index 75000092a2a3..e119279cfb9b 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java @@ -24,6 +24,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ApplicationContext; +import com.azure.spring.cloud.feature.management.filters.ContextualFeatureFilter; import com.azure.spring.cloud.feature.management.filters.FeatureFilter; import com.azure.spring.cloud.feature.management.filters.TimeWindowFilter; import com.azure.spring.cloud.feature.management.implementation.FeatureManagementConfigProperties; @@ -88,6 +89,19 @@ public void isEnabledOnBoolean() throws InterruptedException, ExecutionException verify(featureManagementPropertiesMock, times(2)).getFeatureFlags(); } + @Test + public void isEnabledOnContext() throws InterruptedException, ExecutionException, FilterNotFoundException { + List features = List.of(new Feature().setId("On").setEnabled(true).setConditions(new Conditions() + .setClientFilters(List.of(new FeatureFilterEvaluationContext().setName("AlwaysOnContext"))))); + + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + + when(context.getBean(Mockito.matches("AlwaysOnContext"))).thenReturn(new AlwaysOnContextFilter()); + + assertFalse(featureManager.isEnabled("On", false)); + assertFalse(featureManager.isEnabledAsync("On", false).block()); + } + @Test public void isEnabledFeatureHasNoFilters() { List features = List.of(new Feature().setId("NoFilters").setEnabled(false) @@ -206,6 +220,17 @@ public boolean evaluate(FeatureFilterEvaluationContext context) { } + class AlwaysOnContextFilter implements ContextualFeatureFilter { + + @Override + public boolean evaluate(FeatureFilterEvaluationContext context, Object localContext) { + if (localContext == Boolean.FALSE) { + return false; + } + return true; + } + } + class AlwaysOffFilter implements FeatureFilter { @Override diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java index ed45ced91d9c..660e2b774fab 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java @@ -14,13 +14,14 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.TestConfiguration; import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext; import com.azure.spring.cloud.feature.management.models.TargetingException; +import com.azure.spring.cloud.feature.management.targeting.TargetingContext; +import com.azure.spring.cloud.feature.management.targeting.TargetingContextAccessor; import com.azure.spring.cloud.feature.management.targeting.TargetingEvaluationOptions; -@SpringBootTest(classes = { TestConfiguration.class, SpringBootTest.class }) +@SpringBootTest(classes = { SpringBootTest.class }) public class TargetingFilterTest { private static final String USERS = "Users"; @@ -29,7 +30,7 @@ public class TargetingFilterTest { private static final String AUDIENCE = "Audience"; - private static final String DEFAULT_ROLLOUT_PERCENTAGE = "defaultRolloutPercentage"; + private static final String DEFAULT_ROLLOUT_PERCENTAGE = "DefaultRolloutPercentage"; private static final String OUT_OF_RANGE = "The value is out of the accepted range."; @@ -59,7 +60,7 @@ public void targetedUser() { assertTrue(filter.evaluate(context)); } - + @Test public void targetedUserLower() { FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext(); @@ -136,7 +137,7 @@ public void targetedGroup() { assertTrue(filter.evaluate(context)); } - + @Test public void targetedGroupLower() { FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext(); @@ -455,4 +456,23 @@ private Map emptyExclusion() { excludes.put(GROUPS, excludedGroups); return excludes; } + + class TargetingFilterTestContextAccessor implements TargetingContextAccessor { + + private String user; + + private ArrayList groups; + + TargetingFilterTestContextAccessor(String user, ArrayList groups) { + this.user = user; + this.groups = groups; + } + + @Override + public void configureTargetingContext(TargetingContext context) { + context.setUserId(user); + context.setGroups(groups); + } + + } } From a4de2bc5d52dbf96db1b13d8e44b34260e191aa8 Mon Sep 17 00:00:00 2001 From: Matthew Metcalf Date: Thu, 13 Feb 2025 11:13:09 -0800 Subject: [PATCH 05/41] Push Refresh Telemetry (#44170) * Updating to use Context to pass around telemetry + added pushRefreshEnabled * Updated to PUSH_REFRESH + tests --- ...ationApplicationSettingPropertySource.java | 5 +- .../AppConfigurationConstants.java | 5 + .../AppConfigurationPropertySource.java | 3 +- .../AppConfigurationRefreshUtil.java | 40 +++-- .../AppConfigurationReplicaClient.java | 16 +- ...ppConfigurationSnapshotPropertySource.java | 5 +- .../AzureAppConfigDataLoader.java | 23 ++- .../implementation/FeatureFlagClient.java | 5 +- .../policy/BaseAppConfigurationPolicy.java | 6 +- .../http/policy/TracingInfo.java | 7 +- ...tionSettingPropertySourceSnapshotTest.java | 17 +- ...nApplicationSettingPropertySourceTest.java | 22 ++- ...nfigurationPropertySourceKeyVaultTest.java | 16 +- .../AppConfigurationRefreshUtilTest.java | 168 ++++++++++++++---- .../AppConfigurationReplicaClientTest.java | 62 ++++--- .../implementation/FeatureFlagClientTest.java | 24 +-- .../http/policy/TracingInfoTest.java | 24 +-- 17 files changed, 306 insertions(+), 142 deletions(-) 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 f7c2a6c90f8b..7302de70d4a2 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); } } 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 2daacc4bfd41..24e5fba7b6d0 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 @@ -36,6 +36,11 @@ 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 if Push Refresh is enabled for the store. + */ + public static final String PUSH_REFRESH = "PushRefresh"; /** * Http Header User Agent 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 3a3c84907e22..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 @@ -10,6 +10,7 @@ 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; /** @@ -51,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/AppConfigurationRefreshUtil.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtil.java index 94c1b9c8550f..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; @@ -115,10 +127,10 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF * @param originEndpoint config store origin endpoint * @return A refresh should be triggered. */ - 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(); } @@ -131,12 +143,12 @@ static boolean refreshStoreCheck(AppConfigurationReplicaClient client, String or * @return true if a refresh should be triggered. */ static boolean refreshStoreFeatureFlagCheck(Boolean featureStoreEnabled, - AppConfigurationReplicaClient client) { + AppConfigurationReplicaClient client, Context context) { RefreshEventData eventData = new RefreshEventData(); String endpoint = client.getEndpoint(); if (featureStoreEnabled && StateHolder.getStateFeatureFlag(endpoint) != null) { - refreshWithoutTimeFeatureFlags(client, StateHolder.getStateFeatureFlag(endpoint), eventData); + refreshWithoutTimeFeatureFlags(client, StateHolder.getStateFeatureFlag(endpoint), eventData, context); } else { LOGGER.debug("Skipping feature flag refresh check for " + endpoint); } @@ -151,10 +163,10 @@ static boolean refreshStoreFeatureFlagCheck(Boolean featureStoreEnabled, * @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); } @@ -168,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. @@ -184,14 +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 @@ -209,10 +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 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 5eea371940c2..95e3e90768b1 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 @@ -88,10 +88,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( @@ -111,11 +110,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)); @@ -130,11 +128,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))); @@ -155,12 +153,11 @@ FeatureFlags listFeatureFlags(SettingSelector settingSelector, boolean isRefresh } } - List listSettingSnapshot(String snapshotName, boolean isRefresh) { + List listSettingSnapshot(String snapshotName, Context context) { List configurationSettings = new ArrayList<>(); try { // Because Spring always refreshes all we still have to load snapshots on refresh to build the property // sources. - Context context = new Context("refresh", isRefresh); 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."); @@ -177,8 +174,7 @@ List listSettingSnapshot(String snapshotName, boolean isRe } } - boolean checkWatchKeys(SettingSelector settingSelector, boolean isRefresh) { - Context context = new Context("refresh", false); + boolean checkWatchKeys(SettingSelector settingSelector, Context context) { List> results = client.listConfigurationSettings(settingSelector, context) .streamByPage().filter(pagedResponse -> pagedResponse.getStatusCode() != 304).toList(); return results.size() > 0; 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 853db7e8ab11..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; @@ -45,8 +46,8 @@ final class AppConfigurationSnapshotPropertySource extends AppConfigurationAppli * @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, boolean isRefresh) throws InvalidConfigurationPropertyValueException { - processConfigurationSettings(replicaClient.listSettingSnapshot(snapshotName, isRefresh), 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/AzureAppConfigDataLoader.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java index 72c1e03c3a3f..82bbd918900c 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java @@ -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.io.IOException; import java.time.Instant; import java.util.ArrayList; @@ -19,11 +20,13 @@ 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.AppConfigurationProviderProperties; 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; @EnableConfigurationProperties(AppConfigurationProviderProperties.class) @@ -40,6 +43,8 @@ public class AzureAppConfigDataLoader implements ConfigDataLoader watchKeysSettings = monitoring.getTriggers().stream() .map(trigger -> client.getWatchKey(trigger.getKey(), trigger.getLabel(), - resource.isRefresh())) + requestContext)) .toList(); storeState.setState(resource.getEndpoint(), watchKeysSettings, monitoring.getRefreshInterval()); @@ -157,7 +171,7 @@ private List createSettings(AppConfigurationRepl selectedKeys.getKeyFilter() + resource.getEndpoint() + "/", client, keyVaultClientFactory, selectedKeys.getKeyFilter(), selectedKeys.getLabelFilter(profiles)); } - propertySource.initProperties(resource.getTrimKeyPrefix(), resource.isRefresh()); + propertySource.initProperties(resource.getTrimKeyPrefix(), requestContext); sourceList.add(propertySource); } return sourceList; @@ -174,9 +188,10 @@ private List createFeatureFlags(AppConfigurationReplicaClient clie 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), resource.isRefresh()); + selectedKeys.getKeyFilter(), selectedKeys.getLabelFilter(profiles), requestContext); featureFlagWatchKeys.addAll(storesFeatureFlags); } 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 474f0fda49f0..047dee654f29 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 @@ -26,6 +26,7 @@ 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.SettingSelector; @@ -64,7 +65,7 @@ class FeatureFlagClient { * */ List loadFeatureFlags(AppConfigurationReplicaClient replicaClient, String customKeyFilter, - String[] labelFilter, boolean isRefresh) { + String[] labelFilter, Context context) { List loadedFeatureFlags = new ArrayList<>(); String keyFilter = SELECT_ALL_FEATURE_FLAGS; @@ -79,7 +80,7 @@ List loadFeatureFlags(AppConfigurationReplicaClient replicaClient, for (String label : labels) { SettingSelector settingSelector = new SettingSelector().setKeyFilter(keyFilter).setLabelFilter(label); - FeatureFlags features = replicaClient.listFeatureFlags(settingSelector, isRefresh); + FeatureFlags features = replicaClient.listFeatureFlags(settingSelector, context); loadedFeatureFlags.addAll(proccessFeatureFlags(features, keyFilter)); } return loadedFeatureFlags; diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/BaseAppConfigurationPolicy.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/BaseAppConfigurationPolicy.java index bd72467aa3f9..e46a413fac23 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/BaseAppConfigurationPolicy.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/BaseAppConfigurationPolicy.java @@ -2,6 +2,8 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation.http.policy; +import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.PUSH_REFRESH; + import org.springframework.util.StringUtils; import com.azure.core.http.HttpHeaderName; @@ -40,15 +42,15 @@ public BaseAppConfigurationPolicy(TracingInfo tracingInfo) { this.tracingInfo = tracingInfo; } - @Override public Mono process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) { Boolean watchRequests = (Boolean) context.getData("refresh").orElse(false); + Boolean pushRefresh = (Boolean) context.getData(PUSH_REFRESH).orElse(false); HttpHeaders headers = context.getHttpRequest().getHeaders(); String sdkUserAgent = headers.get(HttpHeaderName.USER_AGENT).getValue(); headers.set(HttpHeaderName.USER_AGENT, USER_AGENT + " " + sdkUserAgent); headers.set(HttpHeaderName.fromString(AppConfigurationConstants.CORRELATION_CONTEXT), - tracingInfo.getValue(watchRequests)); + tracingInfo.getValue(watchRequests, pushRefresh)); return next.process(); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfo.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfo.java index 5f57b8e9ab78..22f1b74118ff 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfo.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfo.java @@ -3,6 +3,7 @@ package com.azure.spring.cloud.appconfiguration.config.implementation.http.policy; import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.KEY_VAULT_CONFIGURED_TRACING; +import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.PUSH_REFRESH; import org.springframework.util.StringUtils; @@ -28,7 +29,7 @@ public TracingInfo(boolean isKeyVaultConfigured, int replicaCount, Configuration this.configuration = configuration; } - String getValue(boolean watchRequests) { + String getValue(boolean watchRequests, boolean pushRefresh) { String track = configuration.get(RequestTracingConstants.REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE.toString()); if (track != null && Boolean.valueOf(track)) { return ""; @@ -50,6 +51,10 @@ String getValue(boolean watchRequests) { if (isKeyVaultConfigured) { sb.append(",").append(KEY_VAULT_CONFIGURED_TRACING); } + + if (pushRefresh) { + sb.append(",").append(PUSH_REFRESH); + } if (replicaCount > 0) { sb.append(",").append(RequestTracingConstants.REPLICA_COUNT).append("=").append(replicaCount); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceSnapshotTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceSnapshotTest.java index 7ef8af30b518..c4c9c95c025a 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceSnapshotTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceSnapshotTest.java @@ -39,12 +39,14 @@ import org.mockito.MockitoSession; import org.mockito.quality.Strictness; +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.properties.AppConfigurationProperties; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; + public class AppConfigurationApplicationSettingPropertySourceSnapshotTest { private static final String EMPTY_CONTENT_TYPE = ""; @@ -90,6 +92,9 @@ public class AppConfigurationApplicationSettingPropertySourceSnapshotTest { @Mock private List configurationListMock; + @Mock + private Context contextMock; + FeatureFlagClient featureFlagLoader = new FeatureFlagClient(); private MockitoSession session; @@ -128,10 +133,10 @@ public void cleanup() throws Exception { @Test public void testPropCanBeInitAndQueried() throws IOException { when(configurationListMock.iterator()).thenReturn(testItems.iterator()).thenReturn(testItems.iterator()); - when(clientMock.listSettingSnapshot(Mockito.any(), Mockito.anyBoolean())).thenReturn(configurationListMock) + when(clientMock.listSettingSnapshot(Mockito.any(), Mockito.any(Context.class))).thenReturn(configurationListMock) .thenReturn(configurationListMock); - propertySource.initProperties(TRIM, false); + propertySource.initProperties(TRIM, contextMock); String[] keyNames = propertySource.getPropertyNames(); String[] expectedKeyNames = testItems.stream().map(t -> { @@ -155,10 +160,10 @@ public void testPropertyNameSlashConvertedToDots() throws IOException { List settings = new ArrayList<>(); settings.add(slashedProp); when(configurationListMock.iterator()).thenReturn(settings.iterator()).thenReturn(Collections.emptyIterator()); - when(clientMock.listSettingSnapshot(Mockito.any(), Mockito.anyBoolean())).thenReturn(configurationListMock) + when(clientMock.listSettingSnapshot(Mockito.any(), Mockito.any(Context.class))).thenReturn(configurationListMock) .thenReturn(configurationListMock); - propertySource.initProperties(TRIM, false); + propertySource.initProperties(TRIM, contextMock); String expectedKeyName = TEST_SLASH_KEY.replace('/', '.'); String[] actualKeyNames = propertySource.getPropertyNames(); @@ -174,9 +179,9 @@ public void initNullValidContentTypeTest() throws IOException { List items = new ArrayList<>(); items.add(ITEM_NULL); when(configurationListMock.iterator()).thenReturn(items.iterator()).thenReturn(Collections.emptyIterator()); - when(clientMock.listSettingSnapshot(Mockito.any(), Mockito.anyBoolean())).thenReturn(configurationListMock); + when(clientMock.listSettingSnapshot(Mockito.any(), Mockito.any(Context.class))).thenReturn(configurationListMock); - propertySource.initProperties(TRIM, false); + propertySource.initProperties(TRIM, contextMock); String[] keyNames = propertySource.getPropertyNames(); String[] expectedKeyNames = diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceTest.java index 1f7fe5ce9760..6aac4f284cb8 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceTest.java @@ -34,12 +34,13 @@ import org.mockito.MockitoAnnotations; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; +import com.azure.core.util.Context; import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProperties; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; public class AppConfigurationApplicationSettingPropertySourceTest { @@ -82,6 +83,9 @@ public class AppConfigurationApplicationSettingPropertySourceTest { @Mock private List configurationListMock; + @Mock + private Context contextMock; + private MockitoSession session; @BeforeAll @@ -116,10 +120,10 @@ public void cleanup() throws Exception { @Test public void testPropCanBeInitAndQueried() throws IOException { when(configurationListMock.iterator()).thenReturn(testItems.iterator()); - when(clientMock.listSettings(Mockito.any(), Mockito.anyBoolean())).thenReturn(configurationListMock) + when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(configurationListMock) .thenReturn(configurationListMock); - propertySource.initProperties(null, false); + propertySource.initProperties(null, contextMock); String[] keyNames = propertySource.getPropertyNames(); String[] expectedKeyNames = testItems.stream() @@ -140,10 +144,10 @@ public void testPropertyNameSlashConvertedToDots() throws IOException { settings.add(slashedProp); when(configurationListMock.iterator()).thenReturn(settings.iterator()) .thenReturn(Collections.emptyIterator()); - when(clientMock.listSettings(Mockito.any(), Mockito.anyBoolean())).thenReturn(configurationListMock) + when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(configurationListMock) .thenReturn(configurationListMock); - propertySource.initProperties(null, false); + propertySource.initProperties(null, contextMock); String expectedKeyName = TEST_SLASH_KEY.replace('/', '.'); String[] actualKeyNames = propertySource.getPropertyNames(); @@ -160,9 +164,9 @@ public void initNullValidContentTypeTest() throws IOException { items.add(ITEM_NULL); when(configurationListMock.iterator()).thenReturn(items.iterator()) .thenReturn(Collections.emptyIterator()); - when(clientMock.listSettings(Mockito.any(), Mockito.anyBoolean())).thenReturn(configurationListMock); + when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(configurationListMock); - propertySource.initProperties(null, false); + propertySource.initProperties(null, contextMock); String[] keyNames = propertySource.getPropertyNames(); String[] expectedKeyNames = items.stream() @@ -177,9 +181,9 @@ public void jsonContentTypeWithInvalidJsonValueTest() { items.add(ITEM_INVALID_JSON); when(configurationListMock.iterator()).thenReturn(items.iterator()) .thenReturn(Collections.emptyIterator()); - when(clientMock.listSettings(Mockito.any(), Mockito.anyBoolean())).thenReturn(configurationListMock); + when(clientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(configurationListMock); - assertThatThrownBy(() -> propertySource.initProperties(null, false)) + assertThatThrownBy(() -> propertySource.initProperties(null, contextMock)) .isInstanceOf(InvalidConfigurationPropertyValueException.class) .hasMessageNotContaining(ITEM_INVALID_JSON.getValue()); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java index 0bc44e5c546c..74d97b275702 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java @@ -30,6 +30,7 @@ import org.mockito.quality.Strictness; 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.SecretReferenceConfigurationSetting; import com.azure.security.keyvault.secrets.SecretAsyncClient; @@ -70,6 +71,9 @@ public class AppConfigurationPropertySourceKeyVaultTest { @Mock private List keyVaultSecretListMock; + + @Mock + private Context contextMock; private MockitoSession session; @@ -98,7 +102,7 @@ public void testKeyVaultTest() { List settings = List.of(KEY_VAULT_ITEM); when(keyVaultSecretListMock.iterator()).thenReturn(settings.iterator()) .thenReturn(Collections.emptyIterator()); - when(replicaClientMock.listSettings(Mockito.any(), Mockito.anyBoolean())).thenReturn(keyVaultSecretListMock) + when(replicaClientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(keyVaultSecretListMock) .thenReturn(keyVaultSecretListMock); KeyVaultSecret secret = new KeyVaultSecret("mySecret", "mySecretValue"); @@ -107,7 +111,7 @@ public void testKeyVaultTest() { when(clientManagerMock.getSecret(Mockito.any(URI.class))).thenReturn(secret); try { - propertySource.initProperties(null, false); + propertySource.initProperties(null, contextMock); } catch (InvalidConfigurationPropertyValueException e) { fail("Failed Reading in Feature Flags"); } @@ -125,11 +129,11 @@ public void invalidKeyVaultReferenceInvalidURITest() { List settings = List.of(KEY_VAULT_ITEM_INVALID_URI); when(keyVaultSecretListMock.iterator()).thenReturn(settings.iterator()) .thenReturn(Collections.emptyIterator()); - when(replicaClientMock.listSettings(Mockito.any(), Mockito.anyBoolean())).thenReturn(keyVaultSecretListMock) + when(replicaClientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(keyVaultSecretListMock) .thenReturn(keyVaultSecretListMock); InvalidConfigurationPropertyValueException exception = assertThrows( - InvalidConfigurationPropertyValueException.class, () -> propertySource.initProperties(null, false)); + InvalidConfigurationPropertyValueException.class, () -> propertySource.initProperties(null, contextMock)); assertEquals("test_key_vault_1", exception.getName()); assertEquals("", exception.getValue()); assertEquals("Invalid URI found in JSON property field 'uri' unable to parse.", exception.getReason()); @@ -140,13 +144,13 @@ public void invalidKeyVaultReferenceParseErrorTest() { List settings = List.of(KEY_VAULT_ITEM); when(keyVaultSecretListMock.iterator()).thenReturn(settings.iterator()) .thenReturn(Collections.emptyIterator()); - when(replicaClientMock.listSettings(Mockito.any(), Mockito.anyBoolean())).thenReturn(keyVaultSecretListMock) + when(replicaClientMock.listSettings(Mockito.any(), Mockito.any(Context.class))).thenReturn(keyVaultSecretListMock) .thenReturn(keyVaultSecretListMock); when(keyVaultClientFactoryMock.getClient(Mockito.eq("https://test.key.vault.com"))) .thenReturn(clientManagerMock); when(clientManagerMock.getSecret(Mockito.any())).thenThrow(new RuntimeException("Parse Failed")); - RuntimeException exception = assertThrows(RuntimeException.class, () -> propertySource.initProperties(null, false)); + RuntimeException exception = assertThrows(RuntimeException.class, () -> propertySource.initProperties(null, contextMock)); assertEquals("Parse Failed", exception.getMessage()); } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtilTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtilTest.java index 161681f4740b..9e5ab6bcaf9e 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtilTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtilTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.Mockito; @@ -28,6 +29,7 @@ import org.mockito.MockitoSession; import org.mockito.quality.Strictness; +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.SettingSelector; @@ -36,6 +38,8 @@ 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.AccessToken; +import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring.PushNotification; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagKeyValueSelector; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagStore; @@ -62,6 +66,9 @@ public class AppConfigurationRefreshUtilTest { @Mock private ReplicaLookUp replicaLookUpMock; + @Mock + private Context contextMock; + private ConfigStore configStore; private String endpoint; @@ -105,8 +112,8 @@ public void refreshWithoutTimeWatchKeyConfigStoreNotLoaded(TestInfo testInfo) { try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.getLoadState(endpoint)).thenReturn(false); - assertFalse(AppConfigurationRefreshUtil.refreshStoreCheck(clientMock, endpoint)); - assertFalse(AppConfigurationRefreshUtil.refreshStoreFeatureFlagCheck(true, clientMock)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreCheck(clientMock, endpoint, contextMock)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreFeatureFlagCheck(true, clientMock, contextMock)); } } @@ -119,13 +126,14 @@ public void refreshWithoutTimeWatchKeyConfigStoreWatchKeyNotReturned(TestInfo te State newState = new State(watchKeys, Math.toIntExact(Duration.ofMinutes(10).getSeconds()), endpoint); // Config Store doesn't return a watch key change. - when(clientMock.getWatchKey(Mockito.eq(KEY_FILTER), Mockito.eq(EMPTY_LABEL), Mockito.anyBoolean())).thenReturn(watchKeys.get(0)); + when(clientMock.getWatchKey(Mockito.eq(KEY_FILTER), Mockito.eq(EMPTY_LABEL), Mockito.any(Context.class))) + .thenReturn(watchKeys.get(0)); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.getLoadState(endpoint)).thenReturn(true); stateHolderMock.when(() -> StateHolder.getState(endpoint)).thenReturn(newState); - assertFalse(AppConfigurationRefreshUtil.refreshStoreCheck(clientMock, endpoint)); - assertFalse(AppConfigurationRefreshUtil.refreshStoreFeatureFlagCheck(true, clientMock)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreCheck(clientMock, endpoint, contextMock)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreFeatureFlagCheck(true, clientMock, contextMock)); } } @@ -139,12 +147,13 @@ public void refreshWithoutTimeWatchKeyConfigStoreWatchKeyNoChange(TestInfo testI Math.toIntExact(Duration.ofMinutes(10).getSeconds()), endpoint); // Config Store does return a watch key change. - when(clientMock.checkWatchKeys(Mockito.any(SettingSelector.class), Mockito.anyBoolean())).thenReturn(false); + when(clientMock.checkWatchKeys(Mockito.any(SettingSelector.class), Mockito.any(Context.class))) + .thenReturn(false); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.getStateFeatureFlag(endpoint)).thenReturn(newState); - assertFalse(AppConfigurationRefreshUtil.refreshStoreCheck(clientMock, endpoint)); - assertFalse(AppConfigurationRefreshUtil.refreshStoreFeatureFlagCheck(true, clientMock)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreCheck(clientMock, endpoint, contextMock)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreFeatureFlagCheck(true, clientMock, contextMock)); } } @@ -156,8 +165,8 @@ public void refreshWithoutTimeFeatureFlagDisabled(TestInfo testInfo) { configStore.getFeatureFlags().setEnabled(false); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { - assertFalse(AppConfigurationRefreshUtil.refreshStoreCheck(clientMock, endpoint)); - assertFalse(AppConfigurationRefreshUtil.refreshStoreFeatureFlagCheck(true, clientMock)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreCheck(clientMock, endpoint, contextMock)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreFeatureFlagCheck(true, clientMock, contextMock)); stateHolderMock.verify(() -> StateHolder.getLoadState(Mockito.anyString()), times(1)); } } @@ -170,8 +179,8 @@ public void refreshWithoutTimeFeatureFlagNotLoaded(TestInfo testInfo) { configStore.getFeatureFlags().setEnabled(true); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { - assertFalse(AppConfigurationRefreshUtil.refreshStoreCheck(clientMock, endpoint)); - assertFalse(AppConfigurationRefreshUtil.refreshStoreFeatureFlagCheck(true, clientMock)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreCheck(clientMock, endpoint, contextMock)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreFeatureFlagCheck(true, clientMock, contextMock)); stateHolderMock.verify(() -> StateHolder.getLoadState(Mockito.anyString()), times(1)); } } @@ -180,18 +189,19 @@ public void refreshWithoutTimeFeatureFlagNotLoaded(TestInfo testInfo) { public void refreshWithoutTimeFeatureFlagNoChange(TestInfo testInfo) { endpoint = testInfo.getDisplayName() + ".azconfig.io"; when(clientMock.getEndpoint()).thenReturn(endpoint); - + FeatureFlagState newState = new FeatureFlagState( List.of(new FeatureFlags(new SettingSelector().setKeyFilter(KEY_FILTER).setLabelFilter(EMPTY_LABEL), null)), Math.toIntExact(Duration.ofMinutes(10).getSeconds()), endpoint); // Config Store doesn't return a watch key change. - when(clientMock.checkWatchKeys(Mockito.any(SettingSelector.class), Mockito.anyBoolean())).thenReturn(false); + when(clientMock.checkWatchKeys(Mockito.any(SettingSelector.class), Mockito.any(Context.class))) + .thenReturn(false); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.getStateFeatureFlag(endpoint)).thenReturn(newState); - assertFalse(AppConfigurationRefreshUtil.refreshStoreCheck(clientMock, endpoint)); - assertFalse(AppConfigurationRefreshUtil.refreshStoreFeatureFlagCheck(true, clientMock)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreCheck(clientMock, endpoint, contextMock)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreFeatureFlagCheck(true, clientMock, contextMock)); } } @@ -200,18 +210,19 @@ public void refreshWithoutTimeFeatureFlagNoChange(TestInfo testInfo) { public void refreshWithoutTimeFeatureFlagEtagChanged(TestInfo testInfo) { endpoint = testInfo.getDisplayName() + ".azconfig.io"; when(clientMock.getEndpoint()).thenReturn(endpoint); - + FeatureFlags featureFlags = new FeatureFlags(new SettingSelector(), watchKeysFeatureFlags); FeatureFlagState newState = new FeatureFlagState(List.of(featureFlags), Math.toIntExact(Duration.ofMinutes(10).getSeconds()), endpoint); // Config Store does return a watch key change. - when(clientMock.checkWatchKeys(Mockito.any(SettingSelector.class), Mockito.anyBoolean())).thenReturn(true); + when(clientMock.checkWatchKeys(Mockito.any(SettingSelector.class), Mockito.any(Context.class))) + .thenReturn(true); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.getStateFeatureFlag(endpoint)).thenReturn(newState); - assertFalse(AppConfigurationRefreshUtil.refreshStoreCheck(clientMock, endpoint)); - assertTrue(AppConfigurationRefreshUtil.refreshStoreFeatureFlagCheck(true, clientMock)); + assertFalse(AppConfigurationRefreshUtil.refreshStoreCheck(clientMock, endpoint, contextMock)); + assertTrue(AppConfigurationRefreshUtil.refreshStoreFeatureFlagCheck(true, clientMock, contextMock)); } } @@ -233,7 +244,8 @@ public void refreshStoresCheckSettingsTestNotEnabled(TestInfo testInfo) { (long) 60, replicaLookUpMock); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); - verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); + verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), + Mockito.any(Context.class)); } } @@ -252,7 +264,8 @@ public void refreshStoresCheckSettingsTestNotLoaded(TestInfo testInfo) { Duration.ofMinutes(10), (long) 60, replicaLookUpMock); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); - verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); + verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), + Mockito.any(Context.class)); } } @@ -272,7 +285,8 @@ public void refreshStoresCheckSettingsTestNotRefreshTime(TestInfo testInfo) { (long) 60, replicaLookUpMock); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); - verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); + verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), + Mockito.any(Context.class)); } } @@ -293,9 +307,14 @@ public void refreshStoresCheckSettingsTestFailedRequest(TestInfo testInfo) { Duration.ofMinutes(10), (long) 60, replicaLookUpMock); assertFalse(eventData.getDoRefresh()); + ArgumentCaptor captorParam = ArgumentCaptor.forClass(Context.class); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); - verify(clientOriginMock, times(1)).getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); + verify(clientOriginMock, times(1)).getWatchKey(Mockito.anyString(), Mockito.anyString(), + captorParam.capture()); assertEquals(newState, StateHolder.getState(endpoint)); + Context testContext = captorParam.getValue(); + assertTrue((Boolean) testContext.getData("refresh").get()); + assertFalse((Boolean) testContext.getData("PushRefresh").get()); } } @@ -304,7 +323,77 @@ public void refreshStoresCheckSettingsTestRefreshTimeNoChange(TestInfo testInfo) endpoint = testInfo.getDisplayName() + ".azconfig.io"; setupFeatureFlagLoad(); - when(clientOriginMock.getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean())) + when(clientOriginMock.getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.any(Context.class))) + .thenReturn(generateWatchKeys().get(0)); + + State newState = new State(generateWatchKeys(), Math.toIntExact(Duration.ofMinutes(-1).getSeconds()), endpoint); + + try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { + stateHolderMock.when(() -> StateHolder.getLoadState(endpoint)).thenReturn(true); + stateHolderMock.when(() -> StateHolder.getState(Mockito.any())).thenReturn(newState); + StateHolder updatedStateHolder = new StateHolder(); + stateHolderMock.when(() -> StateHolder.getCurrentState()).thenReturn(updatedStateHolder); + + RefreshEventData eventData = new AppConfigurationRefreshUtil().refreshStoresCheck(clientFactoryMock, + Duration.ofMinutes(10), (long) 60, replicaLookUpMock); + assertEquals(newState, StateHolder.getState(endpoint)); + assertFalse(eventData.getDoRefresh()); + verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); + verify(clientOriginMock, times(1)).getWatchKey(Mockito.anyString(), Mockito.anyString(), + Mockito.any(Context.class)); + } + } + + @Test + public void refreshStoresPushRefreshEnabledPrimary(TestInfo testInfo) { + endpoint = testInfo.getDisplayName() + ".azconfig.io"; + setupFeatureFlagLoad(); + PushNotification pushNotificaiton = new PushNotification(); + AccessToken p1 = new AccessToken(); + p1.setName("fake name"); + p1.setSecret("value"); + pushNotificaiton.setPrimaryToken(p1); + monitoring.setPushNotification(pushNotificaiton); + when(connectionManagerMock.getMonitoring()).thenReturn(monitoring); + + when(clientOriginMock.getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.any(Context.class))) + .thenReturn(generateWatchKeys().get(0)); + + State newState = new State(generateWatchKeys(), Math.toIntExact(Duration.ofMinutes(-1).getSeconds()), endpoint); + + try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { + stateHolderMock.when(() -> StateHolder.getLoadState(endpoint)).thenReturn(true); + stateHolderMock.when(() -> StateHolder.getState(Mockito.any())).thenReturn(newState); + StateHolder updatedStateHolder = new StateHolder(); + stateHolderMock.when(() -> StateHolder.getCurrentState()).thenReturn(updatedStateHolder); + + RefreshEventData eventData = new AppConfigurationRefreshUtil().refreshStoresCheck(clientFactoryMock, + Duration.ofMinutes(10), (long) 60, replicaLookUpMock); + assertEquals(newState, StateHolder.getState(endpoint)); + assertFalse(eventData.getDoRefresh()); + ArgumentCaptor captorParam = ArgumentCaptor.forClass(Context.class); + verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); + verify(clientOriginMock, times(1)).getWatchKey(Mockito.anyString(), Mockito.anyString(), + captorParam.capture()); + Context testContext = captorParam.getValue(); + assertTrue((Boolean) testContext.getData("refresh").get()); + assertTrue((Boolean) testContext.getData("PushRefresh").get()); + } + } + + @Test + public void refreshStoresPushRefreshEnabledSecondary(TestInfo testInfo) { + endpoint = testInfo.getDisplayName() + ".azconfig.io"; + setupFeatureFlagLoad(); + PushNotification pushNotificaiton = new PushNotification(); + AccessToken p2 = new AccessToken(); + p2.setName("fake name"); + p2.setSecret("value"); + pushNotificaiton.setPrimaryToken(p2); + monitoring.setPushNotification(pushNotificaiton); + when(connectionManagerMock.getMonitoring()).thenReturn(monitoring); + + when(clientOriginMock.getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.any(Context.class))) .thenReturn(generateWatchKeys().get(0)); State newState = new State(generateWatchKeys(), Math.toIntExact(Duration.ofMinutes(-1).getSeconds()), endpoint); @@ -319,8 +408,13 @@ public void refreshStoresCheckSettingsTestRefreshTimeNoChange(TestInfo testInfo) Duration.ofMinutes(10), (long) 60, replicaLookUpMock); assertEquals(newState, StateHolder.getState(endpoint)); assertFalse(eventData.getDoRefresh()); + ArgumentCaptor captorParam = ArgumentCaptor.forClass(Context.class); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); - verify(clientOriginMock, times(1)).getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); + verify(clientOriginMock, times(1)).getWatchKey(Mockito.anyString(), Mockito.anyString(), + captorParam.capture()); + Context testContext = captorParam.getValue(); + assertTrue((Boolean) testContext.getData("refresh").get()); + assertTrue((Boolean) testContext.getData("PushRefresh").get()); } } @@ -336,7 +430,8 @@ public void refreshStoresCheckSettingsTestTriggerRefresh(TestInfo testInfo) { ConfigurationSetting refreshKey = new ConfigurationSetting().setKey(KEY_FILTER).setLabel(EMPTY_LABEL) .setETag("new"); - when(clientOriginMock.getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean())).thenReturn(refreshKey); + when(clientOriginMock.getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.any(Context.class))) + .thenReturn(refreshKey); State newState = new State(generateWatchKeys(), Math.toIntExact(Duration.ofMinutes(-1).getSeconds()), endpoint); @@ -350,7 +445,8 @@ public void refreshStoresCheckSettingsTestTriggerRefresh(TestInfo testInfo) { (long) 60, replicaLookUpMock); assertTrue(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); - verify(clientOriginMock, times(1)).getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); + verify(clientOriginMock, times(1)).getWatchKey(Mockito.anyString(), Mockito.anyString(), + Mockito.any(Context.class)); verify(currentStateMock, times(1)).updateStateRefresh(Mockito.any(), Mockito.any()); } } @@ -373,7 +469,8 @@ public void refreshStoresCheckFeatureFlagTestNotLoaded(TestInfo testInfo) { (long) 60, replicaLookUpMock); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); - verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); + verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), + Mockito.any(Context.class)); } } @@ -395,7 +492,8 @@ public void refreshStoresCheckFeatureFlagTestNotRefreshTime(TestInfo testInfo) { (long) 60, replicaLookUpMock); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); - verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); + verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), + Mockito.any(Context.class)); } } @@ -407,7 +505,7 @@ public void refreshStoresCheckFeatureFlagTestNoChange(TestInfo testInfo) { configStore.setMonitoring(monitoring); setupFeatureFlagLoad(); - when(clientOriginMock.checkWatchKeys(Mockito.any(), Mockito.anyBoolean())).thenReturn(false); + when(clientOriginMock.checkWatchKeys(Mockito.any(), Mockito.any(Context.class))).thenReturn(false); FeatureFlagState newState = new FeatureFlagState( List.of(new FeatureFlags(new SettingSelector().setKeyFilter(KEY_FILTER).setLabelFilter(EMPTY_LABEL), null)), @@ -423,7 +521,8 @@ public void refreshStoresCheckFeatureFlagTestNoChange(TestInfo testInfo) { Duration.ofMinutes(10), (long) 60, replicaLookUpMock); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); - verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); + verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), + Mockito.any(Context.class)); verify(currentStateMock, times(1)).updateFeatureFlagStateRefresh(Mockito.any(), Mockito.any()); } @@ -433,7 +532,7 @@ public void refreshStoresCheckFeatureFlagTestNoChange(TestInfo testInfo) { public void refreshStoresCheckFeatureFlagTestTriggerRefresh(TestInfo testInfo) { endpoint = testInfo.getDisplayName() + ".azconfig.io"; setupFeatureFlagLoad(); - when(clientOriginMock.checkWatchKeys(Mockito.any(), Mockito.anyBoolean())).thenReturn(true); + when(clientOriginMock.checkWatchKeys(Mockito.any(), Mockito.any(Context.class))).thenReturn(true); FeatureFlags featureFlags = new FeatureFlags(new SettingSelector(), watchKeysFeatureFlags); @@ -450,7 +549,8 @@ public void refreshStoresCheckFeatureFlagTestTriggerRefresh(TestInfo testInfo) { Duration.ofMinutes(10), (long) 60, replicaLookUpMock); assertTrue(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); - verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); + verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(), + Mockito.any(Context.class)); } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java index 9e328cffd751..f19f3c54532f 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java @@ -37,6 +37,7 @@ import com.azure.core.http.rest.PagedResponse; import com.azure.core.http.rest.PagedResponseBase; import com.azure.core.http.rest.Response; +import com.azure.core.util.Context; import com.azure.data.appconfiguration.ConfigurationClient; import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.data.appconfiguration.models.ConfigurationSnapshot; @@ -69,6 +70,9 @@ public class AppConfigurationReplicaClientTest { @Mock private Response snapshotResponseMock; + + @Mock + private Context contextMock; private final String endpoint = "clientTest.azconfig.io"; @@ -96,25 +100,25 @@ public void getWatchKeyTest() { Mockito.any())).thenReturn(configurationSettingResponse); when(configurationSettingResponse.getValue()).thenReturn(watchKey); - assertEquals(watchKey, client.getWatchKey("watch", "\0", false)); + assertEquals(watchKey, client.getWatchKey("watch", "\0", contextMock)); when(configurationSettingResponse.getValue()).thenThrow(exceptionMock); when(exceptionMock.getResponse()).thenReturn(responseMock); when(responseMock.getStatusCode()).thenReturn(429); - assertThrows(AppConfigurationStatusException.class, () -> client.getWatchKey("watch", "\0", false)); + assertThrows(AppConfigurationStatusException.class, () -> client.getWatchKey("watch", "\0", contextMock)); when(responseMock.getStatusCode()).thenReturn(408); - assertThrows(AppConfigurationStatusException.class, () -> client.getWatchKey("watch", "\0", false)); + assertThrows(AppConfigurationStatusException.class, () -> client.getWatchKey("watch", "\0", contextMock)); when(responseMock.getStatusCode()).thenReturn(500); - assertThrows(AppConfigurationStatusException.class, () -> client.getWatchKey("watch", "\0", false)); + assertThrows(AppConfigurationStatusException.class, () -> client.getWatchKey("watch", "\0", contextMock)); when(responseMock.getStatusCode()).thenReturn(499); - assertThrows(HttpResponseException.class, () -> client.getWatchKey("watch", "\0", false)); + assertThrows(HttpResponseException.class, () -> client.getWatchKey("watch", "\0", contextMock)); when(clientMock.getConfigurationSettingWithResponse(Mockito.any(), Mockito.any(), Mockito.anyBoolean(), Mockito.any())).thenThrow(new UncheckedIOException(new UnknownHostException())); - assertThrows(AppConfigurationStatusException.class, () -> client.getWatchKey("watch", "\0", false)); + assertThrows(AppConfigurationStatusException.class, () -> client.getWatchKey("watch", "\0", contextMock)); } @Test @@ -132,21 +136,21 @@ public void listSettingsTest() { when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())) .thenReturn(new PagedIterable<>(pagedFlux)); - assertEquals(configurations, client.listSettings(new SettingSelector(), false)); + assertEquals(configurations, client.listSettings(new SettingSelector(), contextMock)); when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())).thenThrow(exceptionMock); when(exceptionMock.getResponse()).thenReturn(responseMock); when(responseMock.getStatusCode()).thenReturn(429); - assertThrows(AppConfigurationStatusException.class, () -> client.listSettings(new SettingSelector(), false)); + assertThrows(AppConfigurationStatusException.class, () -> client.listSettings(new SettingSelector(), contextMock)); when(responseMock.getStatusCode()).thenReturn(408); - assertThrows(AppConfigurationStatusException.class, () -> client.listSettings(new SettingSelector(), false)); + assertThrows(AppConfigurationStatusException.class, () -> client.listSettings(new SettingSelector(), contextMock)); when(responseMock.getStatusCode()).thenReturn(500); - assertThrows(AppConfigurationStatusException.class, () -> client.listSettings(new SettingSelector(), false)); + assertThrows(AppConfigurationStatusException.class, () -> client.listSettings(new SettingSelector(), contextMock)); when(responseMock.getStatusCode()).thenReturn(499); - assertThrows(HttpResponseException.class, () -> client.listSettings(new SettingSelector(), false)); + assertThrows(HttpResponseException.class, () -> client.listSettings(new SettingSelector(), contextMock)); } @Test @@ -166,24 +170,24 @@ public void listFeatureFlagsTest() { when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())) .thenReturn(new PagedIterable<>(pagedFlux)); - assertEquals(configurations, client.listFeatureFlags(new SettingSelector(), false).getFeatureFlags()); + assertEquals(configurations, client.listFeatureFlags(new SettingSelector(), contextMock).getFeatureFlags()); when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())).thenThrow(exceptionMock); when(exceptionMock.getResponse()).thenReturn(responseMock); when(responseMock.getStatusCode()).thenReturn(429); assertThrows(AppConfigurationStatusException.class, - () -> client.listFeatureFlags(new SettingSelector(), false)); + () -> client.listFeatureFlags(new SettingSelector(), contextMock)); when(responseMock.getStatusCode()).thenReturn(408); assertThrows(AppConfigurationStatusException.class, - () -> client.listFeatureFlags(new SettingSelector(), false)); + () -> client.listFeatureFlags(new SettingSelector(), contextMock)); when(responseMock.getStatusCode()).thenReturn(500); assertThrows(AppConfigurationStatusException.class, - () -> client.listFeatureFlags(new SettingSelector(), false)); + () -> client.listFeatureFlags(new SettingSelector(), contextMock)); when(responseMock.getStatusCode()).thenReturn(499); - assertThrows(HttpResponseException.class, () -> client.listFeatureFlags(new SettingSelector(), false)); + assertThrows(HttpResponseException.class, () -> client.listFeatureFlags(new SettingSelector(), contextMock)); } @Test @@ -192,7 +196,7 @@ public void listSettingsUnknownHostTest() { when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())) .thenThrow(new UncheckedIOException(new UnknownHostException())); - assertThrows(AppConfigurationStatusException.class, () -> client.listSettings(new SettingSelector(), false)); + assertThrows(AppConfigurationStatusException.class, () -> client.listSettings(new SettingSelector(), contextMock)); } @Test @@ -202,7 +206,7 @@ public void listSettingsNoCredentialTest() { when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())) .thenThrow(new CredentialUnavailableException("No Credential")); - assertThrows(CredentialUnavailableException.class, () -> client.listSettings(new SettingSelector(), false)); + assertThrows(CredentialUnavailableException.class, () -> client.listSettings(new SettingSelector(), contextMock)); } @Test @@ -212,7 +216,7 @@ public void getWatchNoCredentialTest() { when(clientMock.getConfigurationSettingWithResponse(Mockito.any(), Mockito.any(), Mockito.anyBoolean(), Mockito.any())) .thenThrow(new CredentialUnavailableException("No Credential")); - assertThrows(CredentialUnavailableException.class, () -> client.getWatchKey("key", "label", false)); + assertThrows(CredentialUnavailableException.class, () -> client.getWatchKey("key", "label", contextMock)); } @Test @@ -238,7 +242,7 @@ public void backoffTest() { when(clientMock.listConfigurationSettings(Mockito.any(SettingSelector.class), Mockito.any())) .thenReturn(settingsMock); - client.listSettings(new SettingSelector(), false); + client.listSettings(new SettingSelector(), contextMock); assertTrue(client.getBackoffEndTime().isBefore(Instant.now())); assertEquals(0, client.getFailedAttempts()); } @@ -256,25 +260,25 @@ public void listSettingSnapshotTest() { when(snapshotResponseMock.getValue()).thenReturn(snapshot); when(clientMock.listConfigurationSettingsForSnapshot(Mockito.any())).thenReturn(settingsMock); - assertEquals(configurations, client.listSettingSnapshot("SnapshotName", false)); + assertEquals(configurations, client.listSettingSnapshot("SnapshotName", contextMock)); when(clientMock.listConfigurationSettingsForSnapshot(Mockito.any())).thenThrow(exceptionMock); when(exceptionMock.getResponse()).thenReturn(responseMock); when(responseMock.getStatusCode()).thenReturn(429); - assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName", false)); + assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName", contextMock)); when(responseMock.getStatusCode()).thenReturn(408); - assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName", false)); + assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName", contextMock)); when(responseMock.getStatusCode()).thenReturn(500); - assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName", false)); + assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName", contextMock)); when(responseMock.getStatusCode()).thenReturn(499); - assertThrows(HttpResponseException.class, () -> client.listSettingSnapshot("SnapshotName", false)); + assertThrows(HttpResponseException.class, () -> client.listSettingSnapshot("SnapshotName", contextMock)); when(clientMock.getSnapshotWithResponse(Mockito.any(), Mockito.any(), Mockito.any())) .thenThrow(new UncheckedIOException(new UnknownHostException())); - assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName", false)); + assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName", contextMock)); } @Test @@ -289,7 +293,7 @@ public void listSettingSnapshotInvalidCompositionTypeTest() { when(snapshotResponseMock.getValue()).thenReturn(snapshot); IllegalArgumentException e = assertThrows(IllegalArgumentException.class, - () -> client.listSettingSnapshot("SnapshotName", false)); + () -> client.listSettingSnapshot("SnapshotName", contextMock)); assertEquals("Snapshot SnapshotName needs to be of type Key.", e.getMessage()); } @@ -324,7 +328,7 @@ public void checkWatchKeysTest() { when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())) .thenReturn(new PagedIterable<>(pagedFlux)); - assertTrue(client.checkWatchKeys(new SettingSelector(), false)); + assertTrue(client.checkWatchKeys(new SettingSelector(), contextMock)); pagedResponse.close(); } catch (IOException e) { e.printStackTrace(); @@ -338,7 +342,7 @@ public void checkWatchKeysTest() { when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())).thenReturn(new PagedIterable<>(pagedFlux)); - assertFalse(client.checkWatchKeys(new SettingSelector(), false)); + assertFalse(client.checkWatchKeys(new SettingSelector(), contextMock)); pagedResponse.close(); } catch (IOException e) { e.printStackTrace(); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java index 07c2e5a56fa2..92704d28e88b 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java @@ -33,6 +33,7 @@ import org.mockito.MockitoSession; import org.mockito.quality.Strictness; +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; @@ -43,6 +44,9 @@ public class FeatureFlagClientTest { @Mock private AppConfigurationReplicaClient clientMock; + + @Mock + private Context contextMock; private FeatureFlagClient featureFlagClient; @@ -76,9 +80,9 @@ public void cleanup() throws Exception { public void loadFeatureFlagsTestNoFeatureFlags() { List settings = List.of(new ConfigurationSetting().setKey("FakeKey")); FeatureFlags featureFlags = new FeatureFlags(null, settings); - when(clientMock.listFeatureFlags(Mockito.any(), Mockito.anyBoolean())).thenReturn(featureFlags); + when(clientMock.listFeatureFlags(Mockito.any(), Mockito.any(Context.class))).thenReturn(featureFlags); - List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, false); + List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, contextMock); assertEquals(1, featureFlagsList.size()); assertEquals(featureFlags, featureFlagsList.get(0)); assertEquals("FakeKey", featureFlagsList.get(0).getFeatureFlags().get(0).getKey()); @@ -90,9 +94,9 @@ public void loadFeatureFlagsTestFeatureFlags() { List settings = List.of(new FeatureFlagConfigurationSetting("Alpha", false), new FeatureFlagConfigurationSetting("Beta", true)); FeatureFlags featureFlags = new FeatureFlags(null, settings); - when(clientMock.listFeatureFlags(Mockito.any(), Mockito.anyBoolean())).thenReturn(featureFlags); + when(clientMock.listFeatureFlags(Mockito.any(), Mockito.any(Context.class))).thenReturn(featureFlags); - List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, false); + List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, contextMock); assertEquals(1, featureFlagsList.size()); assertEquals(featureFlags, featureFlagsList.get(0)); assertEquals(".appconfig.featureflag/Alpha", featureFlagsList.get(0).getFeatureFlags().get(0).getKey()); @@ -105,9 +109,9 @@ public void loadFeatureFlagsTestMultipleLoads() { List settings = List.of(new FeatureFlagConfigurationSetting("Alpha", false), new FeatureFlagConfigurationSetting("Beta", true)); FeatureFlags featureFlags = new FeatureFlags(null, settings); - when(clientMock.listFeatureFlags(Mockito.any(), Mockito.anyBoolean())).thenReturn(featureFlags); + when(clientMock.listFeatureFlags(Mockito.any(), Mockito.any(Context.class))).thenReturn(featureFlags); - List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, false); + List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, contextMock); assertEquals(1, featureFlagsList.size()); assertEquals(featureFlags, featureFlagsList.get(0)); assertEquals(".appconfig.featureflag/Alpha", featureFlagsList.get(0).getFeatureFlags().get(0).getKey()); @@ -117,9 +121,9 @@ public void loadFeatureFlagsTestMultipleLoads() { List settings2 = List.of(new FeatureFlagConfigurationSetting("Alpha", true), new FeatureFlagConfigurationSetting("Gamma", false)); featureFlags = new FeatureFlags(null, settings2); - when(clientMock.listFeatureFlags(Mockito.any(), Mockito.anyBoolean())).thenReturn(featureFlags); + when(clientMock.listFeatureFlags(Mockito.any(), Mockito.any(Context.class))).thenReturn(featureFlags); - featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, false); + featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, contextMock); assertEquals(1, featureFlagsList.size()); assertEquals(featureFlags, featureFlagsList.get(0)); assertEquals(".appconfig.featureflag/Alpha", featureFlagsList.get(0).getFeatureFlags().get(0).getKey()); @@ -163,9 +167,9 @@ public void loadFeatureFlagsTestTargetingFilter() { targetingFlag.addClientFilter(targetingFilter); List settings = List.of(targetingFlag); FeatureFlags featureFlags = new FeatureFlags(null, settings); - when(clientMock.listFeatureFlags(Mockito.any(), Mockito.anyBoolean())).thenReturn(featureFlags); + when(clientMock.listFeatureFlags(Mockito.any(), Mockito.any(Context.class))).thenReturn(featureFlags); - List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, false); + List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, contextMock); assertEquals(1, featureFlagsList.size()); assertEquals(featureFlags, featureFlagsList.get(0)); assertEquals(".appconfig.featureflag/TargetingTest", featureFlagsList.get(0).getFeatureFlags().get(0).getKey()); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfoTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfoTest.java index 2bdcdd238590..f93ffa0c63e6 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfoTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfoTest.java @@ -24,37 +24,41 @@ public void getValueTest() { Configuration configuration = getConfiguration("false"); TracingInfo tracingInfo = new TracingInfo(false, 0, configuration); - assertEquals("RequestType=Startup", tracingInfo.getValue(false)); - assertEquals("RequestType=Watch", tracingInfo.getValue(true)); + assertEquals("RequestType=Startup", tracingInfo.getValue(false, false)); + assertEquals("RequestType=Watch", tracingInfo.getValue(true, false)); tracingInfo = new TracingInfo(true, 0, configuration); - assertEquals("RequestType=Startup,UsesKeyVault", tracingInfo.getValue(false)); + assertEquals("RequestType=Startup,UsesKeyVault", tracingInfo.getValue(false, false)); tracingInfo = new TracingInfo(false, 1, configuration); - assertEquals("RequestType=Startup,ReplicaCount=1", tracingInfo.getValue(false)); + assertEquals("RequestType=Startup,ReplicaCount=1", tracingInfo.getValue(false, false)); tracingInfo = new TracingInfo(false, 0, configuration); tracingInfo.getFeatureFlagTracing().updateFeatureFilterTelemetry("Random"); - assertEquals("RequestType=Startup,Filter=CSTM", tracingInfo.getValue(false)); + assertEquals("RequestType=Startup,Filter=CSTM", tracingInfo.getValue(false, false)); + + tracingInfo = new TracingInfo(true, 0, configuration); + assertEquals("RequestType=Startup,UsesKeyVault,PushRefresh", tracingInfo.getValue(false, true)); + } @Test public void disableTracingTest() { TracingInfo tracingInfo = new TracingInfo(false, 0, getConfiguration(null)); - assertNotEquals("", tracingInfo.getValue(false)); + assertNotEquals("", tracingInfo.getValue(false, false)); tracingInfo = new TracingInfo(false, 0, getConfiguration("")); - assertNotEquals("", tracingInfo.getValue(false)); + assertNotEquals("", tracingInfo.getValue(false, false)); tracingInfo = new TracingInfo(false, 0, getConfiguration("true")); - assertEquals("", tracingInfo.getValue(false)); + assertEquals("", tracingInfo.getValue(false, false)); tracingInfo = new TracingInfo(false, 0, getConfiguration("false")); - assertNotEquals("", tracingInfo.getValue(false)); + assertNotEquals("", tracingInfo.getValue(false, false)); tracingInfo = new TracingInfo(false, 0, getConfiguration("random string")); - assertNotEquals("", tracingInfo.getValue(false)); + assertNotEquals("", tracingInfo.getValue(false, false)); } private static final ConfigurationSource EMPTY_SOURCE = new ConfigurationSource() { From cb2c36954b58dc0f4bd71a910327ff2cbf1c2228 Mon Sep 17 00:00:00 2001 From: Matthew Metcalf Date: Thu, 13 Feb 2025 11:16:24 -0800 Subject: [PATCH 06/41] Update to use Evaluation Event (#44070) --- .../FeatureManagementConfiguration.java | 18 ++++-- .../feature/management/FeatureManager.java | 45 +++++++++----- .../filters/ContextualFeatureFilter.java | 1 + .../filters/ContextualFeatureFilterAsync.java | 1 + .../management/filters/TargetingFilter.java | 7 +-- .../implementation/FeatureFilterUtils.java | 1 + .../FeatureManagementConstants.java | 4 +- .../feature/management/models/Conditions.java | 5 ++ .../management/models/EvaluationEvent.java | 62 +++++++++++++++++++ .../feature/management/models/Feature.java | 13 ++-- .../models/FeatureManagementException.java | 2 +- .../filters/TargetingFilterTest.java | 36 ++++++----- .../TargetingFilterUtilsTest.java | 6 +- 13 files changed, 154 insertions(+), 47 deletions(-) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/EvaluationEvent.java diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java index 4b7ca99db962..97eb31aed40f 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java @@ -2,10 +2,13 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.management; +import org.springframework.beans.BeansException; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; import com.azure.spring.cloud.feature.management.implementation.FeatureManagementConfigProperties; import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties; @@ -15,7 +18,9 @@ */ @Configuration @EnableConfigurationProperties({ FeatureManagementConfigProperties.class, FeatureManagementProperties.class }) -class FeatureManagementConfiguration { +class FeatureManagementConfiguration implements ApplicationContextAware { + + private ApplicationContext appContext; /** * Creates Feature Manager @@ -26,8 +31,13 @@ class FeatureManagementConfiguration { * @return FeatureManager */ @Bean - FeatureManager featureManager(ApplicationContext context, - FeatureManagementProperties featureManagementConfigurations, FeatureManagementConfigProperties properties) { - return new FeatureManager(context, featureManagementConfigurations, properties); + FeatureManager featureManager(FeatureManagementProperties featureManagementConfigurations, + FeatureManagementConfigProperties properties) { + return new FeatureManager(appContext, featureManagementConfigurations, properties); + } + + @Override + public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException { + this.appContext = applicationContext; } } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java index 97f864d0060a..1e13cdc543e4 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java @@ -23,6 +23,7 @@ import com.azure.spring.cloud.feature.management.implementation.FeatureManagementConfigProperties; import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties; import com.azure.spring.cloud.feature.management.models.Conditions; +import com.azure.spring.cloud.feature.management.models.EvaluationEvent; import com.azure.spring.cloud.feature.management.models.Feature; import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext; import com.azure.spring.cloud.feature.management.models.FilterNotFoundException; @@ -42,8 +43,8 @@ public class FeatureManager { private final FeatureManagementProperties featureManagementConfigurations; private transient FeatureManagementConfigProperties properties; - - private static final Duration DEFAULT_REQUEST_TIMEOUT = Duration.ofSeconds(100); + + private static final Duration DEFAULT_BLOCK_TIMEOUT = Duration.ofSeconds(100); /** * Can be called to check if a feature is enabled or disabled. @@ -69,7 +70,7 @@ public class FeatureManager { * @throws FilterNotFoundException file not found */ public Mono isEnabledAsync(String feature) { - return checkFeature(feature, null); + return checkFeature(feature, null).map(event -> event.isEnabled()); } /** @@ -82,7 +83,7 @@ public Mono isEnabledAsync(String feature) { * @throws FilterNotFoundException file not found */ public Boolean isEnabled(String feature) throws FilterNotFoundException { - return checkFeature(feature, null).block(DEFAULT_REQUEST_TIMEOUT); + return checkFeature(feature, null).map(event -> event.isEnabled()).block(DEFAULT_BLOCK_TIMEOUT); } /** @@ -96,7 +97,7 @@ public Boolean isEnabled(String feature) throws FilterNotFoundException { * @throws FilterNotFoundException file not found */ public Mono isEnabledAsync(String feature, Object featureContext) { - return checkFeature(feature, featureContext); + return checkFeature(feature, featureContext).map(event -> event.isEnabled()); } /** @@ -110,30 +111,40 @@ public Mono isEnabledAsync(String feature, Object featureContext) { * @throws FilterNotFoundException file not found */ public Boolean isEnabled(String feature, Object featureContext) throws FilterNotFoundException { - return checkFeature(feature, featureContext).block(DEFAULT_REQUEST_TIMEOUT); + return checkFeature(feature, featureContext).map(event -> event.isEnabled()).block(DEFAULT_BLOCK_TIMEOUT); } - private Mono checkFeature(String featureName, Object featureContext) throws FilterNotFoundException { + private Mono checkFeature(String featureName, Object featureContext) + throws FilterNotFoundException { Feature featureFlag = featureManagementConfigurations.getFeatureFlags().stream() .filter(feature -> feature.getId().equals(featureName)).findAny().orElse(null); + EvaluationEvent event = new EvaluationEvent(featureFlag); + if (featureFlag == null) { - return Mono.just(false); + LOGGER.warn("Feature flag %s not found", featureName); + return Mono.just(event); } - if (featureFlag.getConditions().getClientFilters().size() == 0) { - return Mono.just(featureFlag.isEnabled()); + if (!featureFlag.isEnabled()) { + // If a feature flag is disabled and override can't enable it + return Mono.just(event.setEnabled(false)); } - return checkFeatureFilters(featureFlag, featureContext); + Mono result = this.checkFeatureFilters(event, featureContext); + + return result; } - private Mono checkFeatureFilters(Feature featureFlag, Object featureContext) { + private Mono checkFeatureFilters(EvaluationEvent event, Object featureContext) { + Feature featureFlag = event.getFeature(); Conditions conditions = featureFlag.getConditions(); List featureFilters = conditions.getClientFilters(); if (featureFilters.size() == 0) { - return Mono.just(true); + return Mono.just(event.setEnabled(true)); + } else { + event.setEnabled(conditions.getRequirementType().equals(ALL_REQUIREMENT_TYPE)); } List> filterResults = new ArrayList>(); @@ -165,10 +176,14 @@ private Mono checkFeatureFilters(Feature featureFlag, Object featureCon } if (ALL_REQUIREMENT_TYPE.equals(featureFlag.getConditions().getRequirementType())) { - return Flux.merge(filterResults).reduce((a, b) -> a && b).single(); + return Flux.merge(filterResults).reduce((a, b) -> { + return a && b; + }).single().map(result -> { + return event.setEnabled(result); + }); } // Any Filter must be true - return Flux.merge(filterResults).reduce((a, b) -> a || b).single(); + return Flux.merge(filterResults).reduce((a, b) -> a || b).single().map(result -> event.setEnabled(result)); } /** diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilter.java index 8432c1fbc3a9..81fd09d0fe11 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilter.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilter.java @@ -8,6 +8,7 @@ * A Filter for Feature Management that is attached to Features. The filter needs to have @Component set to be found by * feature management. As a Contextual feature filter any context that is passed in to the feature request will be * passed along to the filter(s). + * @since 6.0.0 */ @FunctionalInterface public interface ContextualFeatureFilter { diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilterAsync.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilterAsync.java index 6725e88ca6b7..c22930312978 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilterAsync.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilterAsync.java @@ -10,6 +10,7 @@ * A Filter for Feature Management that is attached to Features. The filter needs to have @Component set to be found by * feature management. As a Contextual feature filter any context that is passed in to the feature request will be * passed along to the filter(s). + * @since 6.0.0 */ @FunctionalInterface public interface ContextualFeatureFilterAsync { diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java index fcaa357aa947..5d1285aaee06 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java @@ -52,7 +52,7 @@ public class TargetingFilter implements FeatureFilter, ContextualFeatureFilter { * Audience that always returns false */ private static final String EXCLUSION_CAMEL = "Exclusion"; - protected static final String EXCLUSION = "Exclusion"; + /** * Error message for when the total Audience value is greater than 100 percent. */ @@ -111,7 +111,6 @@ public boolean evaluate(FeatureFilterEvaluationContext context, Object appContex } TargetingContext targetingContext = new TargetingFilterContext(); - if (appContext != null && appContext instanceof TargetingContext) { // Use this if, there is an appContext + the contextualAccessor, or there is no contextAccessor. targetingContext = (TargetingContext) appContext; @@ -151,10 +150,10 @@ public boolean evaluate(FeatureFilterEvaluationContext context, Object appContex if (exclusionMap == null) { exclusionMap = new HashMap<>(); } - + Object users = exclusionMap.get(exclusionUserValue); Object groups = exclusionMap.get(exclusionGroupsValue); - + Map exclusion = new HashMap<>(); if (users instanceof Map) { diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java index cb25caac6203..5f8fe1cf9925 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java @@ -6,6 +6,7 @@ import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.Collection; import java.util.Map; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementConstants.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementConstants.java index 64165c5ddedd..605d45ea1dfe 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementConstants.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementConstants.java @@ -2,10 +2,10 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.management.implementation; -public class FeatureManagementConstants { +public final class FeatureManagementConstants { public static final String DEFAULT_REQUIREMENT_TYPE = "Any"; - + public static final String ALL_REQUIREMENT_TYPE = "All"; } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Conditions.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Conditions.java index a6504e0f219d..069c6bbafab7 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Conditions.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Conditions.java @@ -11,6 +11,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +/** + * Conditions for evaluating a feature flag. + */ @JsonIgnoreProperties(ignoreUnknown = true) public class Conditions { @JsonProperty("client_filters") @@ -28,6 +31,7 @@ public String getRequirementType() { /** * @param requirementType the requirementType to set + * @return Conditions */ public Conditions setRequirementType(String requirementType) { this.requirementType = requirementType; @@ -43,6 +47,7 @@ public List getClientFilters() { /** * @param clientFilters the clientFilters to set + * @return Conditions */ public Conditions setClientFilters(List clientFilters) { this.clientFilters = clientFilters; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/EvaluationEvent.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/EvaluationEvent.java new file mode 100644 index 000000000000..786269d9ed6b --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/EvaluationEvent.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.management.models; + +/** + * Event tracking the evaluation of a feature flag + */ +public class EvaluationEvent { + + private final Feature feature; + + private String user = ""; + + private boolean enabled = false; + + /** + * Creates an Evaluation Event for the given feature + * @param feature Feature + */ + public EvaluationEvent(Feature feature) { + this.feature = feature; + } + + /** + * @return the feature + */ + public Feature getFeature() { + return feature; + } + + /** + * @return the user + */ + public String getUser() { + return user; + } + + /** + * @param user the user to set + * @return EvaluationEvent + */ + public EvaluationEvent setUser(String user) { + this.user = user; + return this; + } + + /** + * @return the enabled + */ + public boolean isEnabled() { + return enabled; + } + + /** + * @param enabled the enabled to set + * @return EvaluationEvent + */ + public EvaluationEvent setEnabled(boolean enabled) { + this.enabled = enabled; + return this; + } +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java index e3b43b0013c4..e25f75b77278 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java @@ -35,6 +35,7 @@ public String getId() { /** * @param id the id to set + * @return Feature */ public Feature setId(String id) { this.id = id; @@ -50,6 +51,7 @@ public boolean isEnabled() { /** * @param enabled the enabled to set + * @return Feature */ public Feature setEnabled(boolean enabled) { this.enabled = enabled; @@ -58,14 +60,15 @@ public Feature setEnabled(boolean enabled) { /** * @return the description - * */ + */ public String getDescription() { return description; } /** * @param description the description to set - * */ + * @return Feature + */ public Feature setDescription(String description) { this.description = description; return this; @@ -73,17 +76,17 @@ public Feature setDescription(String description) { /** * @return the conditions - * */ + */ public Conditions getConditions() { return conditions; } /** * @param conditions the conditions to set - * */ + * @return Feature + */ public Feature setConditions(Conditions conditions) { this.conditions = conditions; return this; } - } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FeatureManagementException.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FeatureManagementException.java index 8204b27cabfa..30ce0305f987 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FeatureManagementException.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FeatureManagementException.java @@ -20,7 +20,7 @@ public final class FeatureManagementException extends RuntimeException { * * @param message the error message. */ - FeatureManagementException(String message) { + public FeatureManagementException(String message) { super(message); this.message = message; } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java index 660e2b774fab..26115986d3fe 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java @@ -62,23 +62,25 @@ public void targetedUser() { } @Test - public void targetedUserLower() { + public void targetedUserAudience() { FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext(); - Map parameters = new LinkedHashMap(); + Map parameters = new LinkedHashMap<>(); + Map audience = new LinkedHashMap<>(); - Map users = new LinkedHashMap(); + Map users = new LinkedHashMap<>(); users.put("0", "Doe"); - parameters.put(USERS.toLowerCase(), users); - parameters.put(GROUPS.toLowerCase(), new LinkedHashMap()); - parameters.put(DEFAULT_ROLLOUT_PERCENTAGE, 0); - parameters.put("Exclusion", emptyExclusion()); + audience.put(USERS, users); + audience.put(GROUPS, new LinkedHashMap()); + audience.put(DEFAULT_ROLLOUT_PERCENTAGE, 0); + audience.put("Exclusion", emptyExclusion()); Map excludes = new LinkedHashMap<>(); Map excludedGroups = new LinkedHashMap<>(); excludes.put(GROUPS, excludedGroups); + parameters.put("Audience", audience); context.setParameters(parameters); context.setFeatureName("testFeature"); @@ -86,6 +88,9 @@ public void targetedUserLower() { TargetingFilter filter = new TargetingFilter(new TargetingFilterTestContextAccessor("Doe", null)); assertTrue(filter.evaluate(context)); + + filter = new TargetingFilter(new TargetingFilterTestContextAccessor("Doe", null)); + assertTrue(filter.evaluate(context)); } @Test @@ -139,10 +144,11 @@ public void targetedGroup() { } @Test - public void targetedGroupLower() { + public void targetedGroupAudience() { FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext(); - Map parameters = new LinkedHashMap(); + Map parameters = new LinkedHashMap<>(); + Map audience = new LinkedHashMap<>(); Map groups = new LinkedHashMap(); Map g1 = new LinkedHashMap(); @@ -150,10 +156,12 @@ public void targetedGroupLower() { g1.put("rolloutPercentage", "100"); groups.put("0", g1); - parameters.put(USERS.toLowerCase(), new LinkedHashMap()); - parameters.put(GROUPS.toLowerCase(), groups); - parameters.put(DEFAULT_ROLLOUT_PERCENTAGE, 0); - parameters.put("Exclusion", emptyExclusion()); + audience.put(USERS, new LinkedHashMap()); + audience.put(GROUPS, groups); + audience.put(DEFAULT_ROLLOUT_PERCENTAGE, 0); + audience.put("Exclusion", emptyExclusion()); + + parameters.put("Audience", audience); context.setParameters(parameters); context.setFeatureName("testFeature"); @@ -247,7 +255,7 @@ public void targetedGroupFiftyFalse() { TargetingFilter filter = new TargetingFilter(new TargetingFilterTestContextAccessor("Doe", targetedGroups)); - assertTrue(filter.evaluate(context)); + assertFalse(filter.evaluate(context)); } @Test diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implemenation/TargetingFilterUtilsTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implemenation/TargetingFilterUtilsTest.java index ab85d8a7c543..759cf5873635 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implemenation/TargetingFilterUtilsTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implemenation/TargetingFilterUtilsTest.java @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. package com.azure.spring.cloud.feature.management.implemenation; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -13,8 +15,8 @@ public void isTargetedPercentageTest() { assertEquals(FeatureFilterUtils.isTargetedPercentage(null), 9.875071074318855); assertEquals(FeatureFilterUtils.isTargetedPercentage(""), 26.0813765987012); assertEquals(FeatureFilterUtils.isTargetedPercentage("Alice"), 38.306839656621875); - assertEquals(FeatureFilterUtils.isTargetedPercentage("Quinn\nDeb"), 38.306839656621875); - assertEquals(FeatureFilterUtils.isTargetedPercentage("\nProd"), 79.98622464481421); + assertEquals(FeatureFilterUtils.isTargetedPercentage("Quinn\nDeb"), 79.98622464481421); + assertEquals(FeatureFilterUtils.isTargetedPercentage("\nProd"), 73.47059517015484); } } From 8ed490167febc8c4ff13fb0e96330285134ce18b Mon Sep 17 00:00:00 2001 From: Matthew Metcalf Date: Fri, 21 Feb 2025 12:14:19 -0800 Subject: [PATCH 07/41] Adding Default Filters (#43935) * Adding Default Filters * Fixing Feature flag loading after both redesigns * Fixing issues after just 2.0 schema and properties * Clearing up feature return * fixing usage of default min backoff * Removed dead code + tests * Update TargetingContextAccessorTestConfiguration.java * Update FeatureManagementConfiguration.java --- ...ppConfigurationWatchAutoConfiguration.java | 10 +- ...rationFeatureManagementPropertySource.java | 15 +- ...AppConfigurationKeyVaultClientFactory.java | 7 +- .../AppConfigurationPullRefresh.java | 8 +- ...AppConfigurationReplicaClientsBuilder.java | 5 +- .../AzureAppConfigBoostrapRegistrar.java | 37 +++-- .../AzureAppConfigDataLoader.java | 10 +- .../AzureAppConfigDataLocationResolver.java | 36 +---- .../AzureAppConfigDataResource.java | 16 +- .../implementation/FeatureFlagClient.java | 4 +- .../AppConfigurationProviderProperties.java | 145 ------------------ .../AppConfigurationSecretClientManager.java | 10 +- ...tionSettingPropertySourceSnapshotTest.java | 2 +- ...onFeatureManagementPropertySourceTest.java | 23 +-- .../AppConfigurationPullRefreshTest.java | 4 +- ...ConfigurationReplicaClientBuilderTest.java | 24 +-- .../implementation/FeatureFlagClientTest.java | 19 ++- .../stores/KeyVaultClientTest.java | 6 +- .../FeatureManagementConfiguration.java | 28 ++++ .../FeatureManagementProperties.java | 12 +- .../FeatureManagementConfigurationTest.java | 21 +++ ...etingContextAccessorTestConfiguration.java | 31 ++++ .../FeatureManagementPropertiesTest.java | 32 ++++ 23 files changed, 225 insertions(+), 280 deletions(-) delete mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationProviderProperties.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/TargetingContextAccessorTestConfiguration.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementPropertiesTest.java diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationWatchAutoConfiguration.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationWatchAutoConfiguration.java index d2d7262e439e..9b0d4be6c0a0 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationWatchAutoConfiguration.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationWatchAutoConfiguration.java @@ -17,14 +17,13 @@ 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. */ @EnableAsync @ConditionalOnProperty(prefix = AppConfigurationProperties.CONFIG_PREFIX, name = "enabled", matchIfMissing = true) -@EnableConfigurationProperties({ AppConfigurationProperties.class, AppConfigurationProviderProperties.class }) +@EnableConfigurationProperties({ AppConfigurationProperties.class }) @AutoConfiguration @ConditionalOnClass(RefreshEndpoint.class) public class AppConfigurationWatchAutoConfiguration { @@ -37,13 +36,12 @@ public AppConfigurationWatchAutoConfiguration() { @Bean @ConditionalOnMissingBean - AppConfigurationRefresh appConfigurationRefresh(AppConfigurationProperties properties, - AppConfigurationProviderProperties appProperties, BootstrapContext context) { + AppConfigurationRefresh appConfigurationRefresh(AppConfigurationProperties properties, BootstrapContext context) { AppConfigurationReplicaClientFactory clientFactory = context .get(AppConfigurationReplicaClientFactory.class); ReplicaLookUp replicaLookUp = context.get(ReplicaLookUp.class); - 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/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 6a27f783285f..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 @@ -23,19 +23,16 @@ class AppConfigurationKeyVaultClientFactory { private final boolean credentialsConfigured; private final boolean isConfigured; - - private final int timeout; 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; } AppConfigurationSecretClientManager getClient(String host) { @@ -43,7 +40,7 @@ AppConfigurationSecretClientManager getClient(String host) { // 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/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/AppConfigurationReplicaClientsBuilder.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientsBuilder.java index 4f28d5297f28..b9868060bc26 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 @@ -78,11 +78,10 @@ public class AppConfigurationReplicaClientsBuilder { private final boolean credentialConfigured; - private final int defaultMaxRetries; + private final int defaultMaxRetries = 2; - AppConfigurationReplicaClientsBuilder(int defaultMaxRetries, ConfigurationClientBuilderFactory clientFactory, + AppConfigurationReplicaClientsBuilder(ConfigurationClientBuilderFactory clientFactory, ConfigurationClientCustomizer clientCustomizer, boolean credentialConfigured, boolean isKeyVaultConfigured) { - this.defaultMaxRetries = defaultMaxRetries; this.credentialConfigured = credentialConfigured; this.clientFactory = clientFactory; this.clientCustomizer = clientCustomizer; 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 index be7d90bb5e06..fb0a090f0910 100644 --- 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 @@ -4,7 +4,6 @@ import org.springframework.boot.BootstrapRegistry.InstanceSupplier; import org.springframework.boot.context.config.ConfigDataLocationResolverContext; -import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.util.StringUtils; @@ -15,9 +14,9 @@ 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.appconfiguration.config.implementation.properties.AppConfigurationProviderProperties; 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; @@ -26,12 +25,10 @@ import com.azure.spring.cloud.service.implementation.appconfiguration.ConfigurationClientBuilderFactory; import com.azure.spring.cloud.service.implementation.keyvault.secrets.SecretClientBuilderFactory; -@EnableConfigurationProperties(AppConfigurationProviderProperties.class) class AzureAppConfigurationBootstrapRegistrar { static void register(ConfigDataLocationResolverContext context, Binder binder, - AppConfigurationProperties properties, AppConfigurationProviderProperties appProperties, - ReplicaLookUp replicaLookup) { + AppConfigurationProperties properties, ReplicaLookUp replicaLookup) { AzureGlobalProperties globalProperties = binder .bind(AzureGlobalProperties.PREFIX, Bindable.of(AzureGlobalProperties.class)) @@ -46,9 +43,9 @@ static void register(ConfigDataLocationResolverContext context, Binder binder, boolean isCredentialConfigured = isCredentialConfigured(loadedProperties); AppConfigurationKeyVaultClientFactory keyVaultClientFactory = appConfigurationKeyVaultClientFactory(context, - isCredentialConfigured, appProperties.getMaxRetryTime()); - AppConfigurationReplicaClientsBuilder replicaClientsBuilder = replicaClientBuilder(context, binder, - keyVaultClientFactory, loadedProperties, isCredentialConfigured, appProperties.getMaxRetries()); + binder, isCredentialConfigured); + AppConfigurationReplicaClientsBuilder replicaClientsBuilder = replicaClientBuilder(context, + keyVaultClientFactory, loadedProperties, isCredentialConfigured); context.getBootstrapContext().registerIfAbsent(AppConfigurationKeyVaultClientFactory.class, InstanceSupplier.from(() -> keyVaultClientFactory)); @@ -57,17 +54,23 @@ static void register(ConfigDataLocationResolverContext context, Binder binder, } private static AppConfigurationKeyVaultClientFactory appConfigurationKeyVaultClientFactory( - ConfigDataLocationResolverContext context, boolean isCredentialConfigured, Integer maxRetryTime) + ConfigDataLocationResolverContext context, Binder binder, boolean isCredentialConfigured) throws IllegalArgumentException { SecretClientCustomizer customizer = context.getBootstrapContext().getOrElse(SecretClientCustomizer.class, null); KeyVaultSecretProvider secretProvider = context.getBootstrapContext().getOrElse(KeyVaultSecretProvider.class, null); - SecretClientBuilderFactory secretClientFactory = context.getBootstrapContext() - .getOrElse(SecretClientBuilderFactory.class, null); - return new AppConfigurationKeyVaultClientFactory(customizer, secretProvider, secretClientFactory, - isCredentialConfigured, maxRetryTime); + 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( @@ -78,8 +81,8 @@ private static AppConfigurationReplicaClientFactory buildClientFactory( @SuppressWarnings("unchecked") private static AppConfigurationReplicaClientsBuilder replicaClientBuilder(ConfigDataLocationResolverContext context, - Binder binder, AppConfigurationKeyVaultClientFactory keyVaultClientFactory, - AzureAppConfigurationProperties properties, boolean isCredentialConfigured, Integer maxRetries) { + AppConfigurationKeyVaultClientFactory keyVaultClientFactory, AzureAppConfigurationProperties properties, + boolean isCredentialConfigured) { InstanceSupplier> customizer = context .getBootstrapContext() @@ -108,8 +111,8 @@ private static AppConfigurationReplicaClientsBuilder replicaClientBuilder(Config clientCustomizer = configurationClientCustomizer.get(context.getBootstrapContext()); } - return new AppConfigurationReplicaClientsBuilder(maxRetries, clientFactory, clientCustomizer, - isCredentialConfigured, keyVaultClientFactory.isConfigured()); + return new AppConfigurationReplicaClientsBuilder(clientFactory, clientCustomizer, isCredentialConfigured, + keyVaultClientFactory.isConfigured()); } private static boolean isCredentialConfigured(AbstractAzureHttpConfigurationProperties properties) { diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java index 82bbd918900c..96abbe9ff923 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java @@ -14,7 +14,6 @@ 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.context.properties.EnableConfigurationProperties; import org.springframework.boot.logging.DeferredLog; import org.springframework.boot.logging.DeferredLogFactory; import org.springframework.core.env.EnumerablePropertySource; @@ -24,12 +23,10 @@ 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.AppConfigurationProviderProperties; 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; -@EnableConfigurationProperties(AppConfigurationProviderProperties.class) public class AzureAppConfigDataLoader implements ConfigDataLoader { private static Log logger = new DeferredLog(); @@ -46,6 +43,10 @@ public class AzureAppConfigDataLoader implements ConfigDataLoader createFeatureFlags(AppConfigurationReplicaClient clie private void delayException() { Instant currentDate = Instant.now(); - Instant preKillTIme = resource.getAppProperties().getStartDate() - .plusSeconds(resource.getAppProperties().getPrekillTime()); + Instant preKillTIme = START_DATE.plusSeconds(PREKILL_TIME); if (currentDate.isBefore(preKillTIme)) { long diffInMillies = Math.abs(preKillTIme.toEpochMilli() - currentDate.toEpochMilli()); try { 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 index 77ec7057d854..8c5d1cabd38e 100644 --- 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 @@ -10,7 +10,6 @@ import javax.naming.NamingException; import org.apache.commons.logging.Log; -import org.springframework.beans.BeanUtils; import org.springframework.boot.BootstrapRegistry.InstanceSupplier; import org.springframework.boot.context.config.ConfigDataLocation; import org.springframework.boot.context.config.ConfigDataLocationNotFoundException; @@ -18,7 +17,6 @@ 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.EnableConfigurationProperties; import org.springframework.boot.context.properties.bind.BindHandler; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; @@ -27,10 +25,8 @@ 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; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore; -@EnableConfigurationProperties(AppConfigurationProviderProperties.class) public class AzureAppConfigDataLocationResolver implements ConfigDataLocationResolver { @@ -73,32 +69,23 @@ public List resolveProfileSpecific( ConfigDataLocationResolverContext resolverContext, ConfigDataLocation location, Profiles profiles) throws ConfigDataLocationNotFoundException { - Holder holder = loadProperties(resolverContext); + AppConfigurationProperties properties = loadProperties(resolverContext); List locations = new ArrayList<>(); - for (ConfigStore store : holder.properties.getStores()) { + for (ConfigStore store : properties.getStores()) { locations.add( - new AzureAppConfigDataResource(store, profiles, holder.appProperties, START_UP.get(), - holder.properties.getRefreshInterval())); + new AzureAppConfigDataResource(store, profiles, START_UP.get(), properties.getRefreshInterval())); } START_UP.set(false); return locations; } - protected Holder loadProperties(ConfigDataLocationResolverContext context) { + protected AppConfigurationProperties loadProperties(ConfigDataLocationResolverContext context) { Binder binder = context.getBinder(); BindHandler bindHandler = getBindHandler(context); - AppConfigurationProperties properties; - AppConfigurationProviderProperties appProperties; - Holder holder = new Holder(); - - properties = binder.bind(AppConfigurationProperties.CONFIG_PREFIX, + AppConfigurationProperties properties = binder.bind(AppConfigurationProperties.CONFIG_PREFIX, Bindable.of(AppConfigurationProperties.class), bindHandler).get(); - appProperties = binder.bind(AppConfigurationProviderProperties.CONFIG_PREFIX, - Bindable.of(AppConfigurationProviderProperties.class), bindHandler) - .orElseGet(AppConfigurationProviderProperties::new); - properties.validateAndInit(); ReplicaLookUp replicaLookup = null; try { @@ -108,22 +95,13 @@ protected Holder loadProperties(ConfigDataLocationResolverContext context) { LOGGER.info("Failed to find DNS Entry for config store while looking for replicas."); } - AzureAppConfigurationBootstrapRegistrar.register(context, binder, properties, appProperties, replicaLookup); - - holder.properties = properties; - holder.appProperties = appProperties; + AzureAppConfigurationBootstrapRegistrar.register(context, binder, properties, replicaLookup); - return holder; + return properties; } private BindHandler getBindHandler(ConfigDataLocationResolverContext context) { return context.getBootstrapContext().getOrElse(BindHandler.class, null); } - private class Holder { - AppConfigurationProperties properties; - - AppConfigurationProviderProperties appProperties; - } - } 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 index 13f5f1a87ad8..3f31ee7fd926 100644 --- 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 @@ -10,7 +10,6 @@ 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.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; @@ -31,14 +30,12 @@ public class AzureAppConfigDataResource extends ConfigDataResource { private final AppConfigurationStoreMonitoring monitoring; - private final AppConfigurationProviderProperties appProperties; - private final boolean isRefresh; private Duration refreshInterval; - AzureAppConfigDataResource(ConfigStore configStore, Profiles profiles, - AppConfigurationProviderProperties appProperties, boolean isRefresh, Duration refreshInterval) { + AzureAppConfigDataResource(ConfigStore configStore, Profiles profiles, boolean isRefresh, + Duration refreshInterval) { this.configStoreEnabled = configStore.isEnabled(); this.endpoint = configStore.getEndpoint(); this.selects = configStore.getSelects(); @@ -46,7 +43,6 @@ public class AzureAppConfigDataResource extends ConfigDataResource { this.trimKeyPrefix = configStore.getTrimKeyPrefix(); this.monitoring = configStore.getMonitoring(); this.profiles = profiles; - this.appProperties = appProperties; this.isRefresh = isRefresh; this.refreshInterval = refreshInterval; } @@ -128,13 +124,6 @@ public boolean isRefresh() { return isRefresh; } - /** - * @return the appProperties - */ - public AppConfigurationProviderProperties getAppProperties() { - return appProperties; - } - /** * @return the refreshInterval */ @@ -148,5 +137,4 @@ public Duration getRefreshInterval() { 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/FeatureFlagClient.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClient.java index 047dee654f29..b227d70b4747 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 @@ -167,8 +167,8 @@ private static String calculateFeatureFlagId(String key, String label) { /** * @return the properties */ - public Map getProperties() { - return properties; + public List getFeatureFlags() { + return properties.values().stream().toList(); } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationProviderProperties.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationProviderProperties.java deleted file mode 100644 index c0b05c03881e..000000000000 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationProviderProperties.java +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.appconfiguration.config.implementation.properties; - -import java.time.Instant; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.util.Assert; - -import jakarta.annotation.PostConstruct; - -/** - * Properties defining connection to Azure App Configuration. - */ -@ConfigurationProperties(prefix = AppConfigurationProviderProperties.CONFIG_PREFIX) -public class AppConfigurationProviderProperties { - - /** - * Prefix for the libraries internal configurations. - */ - public static final String CONFIG_PREFIX = "spring.cloud.appconfiguration"; - - private static final Instant START_DATE = Instant.now(); - - @Value("${version:1.0}") - private String version; - - @Value("${maxRetries:2}") - private int maxRetries; - - @Value("${maxRetryTime:60}") - private int maxRetryTime; - - @Value("${prekillTime:5}") - private int prekillTime; - - @Value("${defaultMinBackoff:30}") - private Long defaultMinBackoff; - - @Value("${defaultMaxBackoff:600}") - private Long defaultMaxBackoff; - - /** - * @return the apiVersion - */ - public String getVersion() { - return version; - } - - /** - * @param apiVersion the apiVersion to set - */ - public void setVersion(String apiVersion) { - this.version = apiVersion; - } - - /** - * @return the maxRetries - */ - public int getMaxRetries() { - return maxRetries; - } - - /** - * @param maxRetries the maxRetries to set - */ - public void setMaxRetries(int maxRetries) { - this.maxRetries = maxRetries; - } - - /** - * @return the maxRetryTime - */ - public int getMaxRetryTime() { - return maxRetryTime; - } - - /** - * @param maxRetryTime the maxRetryTime to set - */ - public void setMaxRetryTime(int maxRetryTime) { - this.maxRetryTime = maxRetryTime; - } - - /** - * @return the prekillTime - */ - public int getPrekillTime() { - return prekillTime; - } - - /** - * @param prekillTime the prekillTime to set - */ - public void setPrekillTime(int prekillTime) { - this.prekillTime = prekillTime; - } - - /** - * @return the startDate - */ - public Instant getStartDate() { - return START_DATE; - } - - /** - * @return the defaultMinBackoff - */ - public Long getDefaultMinBackoff() { - return defaultMinBackoff; - } - - /** - * @param defaultMinBackoff the defaultMinBackoff to set - */ - public void setDefaultMinBackoff(Long defaultMinBackoff) { - this.defaultMinBackoff = defaultMinBackoff; - } - - /** - * @return the defaultMaxBackoff - */ - public Long getDefaultMaxBackoff() { - return defaultMaxBackoff; - } - - /** - * @param defaultMaxBackoff the defaultMaxBackoff to set - */ - public void setDefaultMaxBackoff(Long defaultMaxBackoff) { - this.defaultMaxBackoff = defaultMaxBackoff; - } - - @PostConstruct - public void validateAndInit() { - Assert.hasLength(version, "A version of app configuration should be set."); - Assert.notNull(maxRetries, "A number of max retries has to be configured."); - Assert.notNull(maxRetryTime, "A max retry value needs to be configured"); - Assert.notNull(prekillTime, "A preKill time value needs to be configured."); - Assert.notNull(defaultMinBackoff, "A default minimum backoff time value needs to be set."); - Assert.notNull(defaultMaxBackoff, "A default max backoff time value needs to be set."); - } - -} diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/stores/AppConfigurationSecretClientManager.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/stores/AppConfigurationSecretClientManager.java index 2c086d3c5dab..9a8437d7bfd2 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/stores/AppConfigurationSecretClientManager.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/stores/AppConfigurationSecretClientManager.java @@ -7,7 +7,7 @@ import org.springframework.util.StringUtils; -import com.azure.identity.ManagedIdentityCredentialBuilder; +import com.azure.identity.DefaultAzureCredentialBuilder; import com.azure.security.keyvault.secrets.SecretAsyncClient; import com.azure.security.keyvault.secrets.SecretClientBuilder; import com.azure.security.keyvault.secrets.models.KeyVaultSecret; @@ -32,7 +32,7 @@ public final class AppConfigurationSecretClientManager { private final boolean credentialConfigured; - private final int timeout; + private final int timeout = 30; /** * Creates a Client for connecting to Key Vault @@ -41,17 +41,15 @@ public final class AppConfigurationSecretClientManager { * @param keyVaultSecretProvider optional provider for providing Secrets instead of connecting to Key Vault * @param secretClientFactory Factory for building clients to Key Vault * @param credentialConfigured Is a credential configured with Global Configurations or Service Configurations - * @param timeout How long the connection to key vault is kept open without a response. */ public AppConfigurationSecretClientManager(String endpoint, SecretClientCustomizer keyVaultClientProvider, KeyVaultSecretProvider keyVaultSecretProvider, SecretClientBuilderFactory secretClientFactory, - boolean credentialConfigured, int timeout) { + boolean credentialConfigured) { this.endpoint = endpoint; this.keyVaultClientProvider = keyVaultClientProvider; this.keyVaultSecretProvider = keyVaultSecretProvider; this.secretClientFactory = secretClientFactory; this.credentialConfigured = credentialConfigured; - this.timeout = timeout; } AppConfigurationSecretClientManager build() { @@ -59,7 +57,7 @@ AppConfigurationSecretClientManager build() { if (!credentialConfigured) { // System Assigned Identity. - builder.credential(new ManagedIdentityCredentialBuilder().build()); + builder.credential(new DefaultAzureCredentialBuilder().build()); } builder.vaultUrl(endpoint); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceSnapshotTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceSnapshotTest.java index c4c9c95c025a..b9735b0232f3 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceSnapshotTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceSnapshotTest.java @@ -150,7 +150,7 @@ public void testPropCanBeInitAndQueried() throws IOException { assertThat(propertySource.getProperty(TEST_KEY_2)).isEqualTo(TEST_VALUE_2); assertThat(propertySource.getProperty(TEST_KEY_3)).isEqualTo(TEST_VALUE_3); assertThat(propertySource.getProperty(".bar.test_key_4")).isEqualTo("test_value_4"); - assertEquals(1, featureFlagLoader.getProperties().size()); + assertEquals(1, featureFlagLoader.getFeatureFlags().size()); } @Test diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationFeatureManagementPropertySourceTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationFeatureManagementPropertySourceTest.java index 804ef5b7c830..457ff57d8d99 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationFeatureManagementPropertySourceTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationFeatureManagementPropertySourceTest.java @@ -8,7 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; -import java.util.HashMap; +import java.util.List; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -25,9 +25,9 @@ public class AppConfigurationFeatureManagementPropertySourceTest { @Mock private FeatureFlagClient featureFlagLoaderMock; - + private static final String FEATURE_FLAG_KEY = "feature_management.feature_flags"; - + private MockitoSession session; @BeforeEach @@ -35,7 +35,7 @@ public void init() { session = Mockito.mockitoSession().initMocks(this).strictness(Strictness.STRICT_STUBS).startMocking(); MockitoAnnotations.openMocks(this); } - + @AfterEach public void cleanup() throws Exception { MockitoAnnotations.openMocks(this).close(); @@ -46,21 +46,24 @@ public void cleanup() throws Exception { public void getPropertyNamesTest() { AppConfigurationFeatureManagementPropertySource featureManagementPropertySource = new AppConfigurationFeatureManagementPropertySource( featureFlagLoaderMock); - + String[] names = featureManagementPropertySource.getPropertyNames(); - assertTrue(names.length == 1); + assertTrue(names.length == 0); + + when(featureFlagLoaderMock.getFeatureFlags()).thenReturn(List.of(new Feature())); + names = featureManagementPropertySource.getPropertyNames(); assertEquals(FEATURE_FLAG_KEY, names[0]); } - + @Test public void getPropertyTest() { AppConfigurationFeatureManagementPropertySource featureManagementPropertySource = new AppConfigurationFeatureManagementPropertySource( featureFlagLoaderMock); - + assertNull(featureManagementPropertySource.getProperty("NotFeatureFlagProperty")); - when(featureFlagLoaderMock.getProperties()).thenReturn(new HashMap()); + when(featureFlagLoaderMock.getFeatureFlags()).thenReturn(List.of()); assertNotNull(featureManagementPropertySource.getProperty(FEATURE_FLAG_KEY)); - + } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefreshTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefreshTest.java index d6163c972b7b..72bb068ecb71 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefreshTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefreshTest.java @@ -63,7 +63,7 @@ public void refreshNoChange() throws InterruptedException, ExecutionException { when(refreshUtilMock.refreshStoresCheck(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(eventDataMock); AppConfigurationPullRefresh refresh = new AppConfigurationPullRefresh(clientFactoryMock, refreshInterval, - (long) 0, replicaLookUpMock, refreshUtilMock); + replicaLookUpMock, refreshUtilMock); assertFalse(refresh.refreshConfigurations().block()); } @@ -75,7 +75,7 @@ public void refreshUpdate() throws InterruptedException, ExecutionException { when(refreshUtilMock.refreshStoresCheck(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(eventDataMock); AppConfigurationPullRefresh refresh = new AppConfigurationPullRefresh(clientFactoryMock, refreshInterval, - (long) 0, replicaLookUpMock, refreshUtilMock); + replicaLookUpMock, refreshUtilMock); refresh.setApplicationEventPublisher(publisher); assertTrue(refresh.refreshConfigurations().block()); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientBuilderTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientBuilderTest.java index c35010596f36..23f20ad69b0b 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientBuilderTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientBuilderTest.java @@ -78,7 +78,7 @@ public void cleanup() throws Exception { @Test public void buildClientFromEndpointTest() { - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); + clientBuilder = new AppConfigurationReplicaClientsBuilder(clientFactoryMock, null, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); when(clientFactoryMock.build()).thenReturn(builderMock); @@ -100,7 +100,7 @@ public void buildClientFromConnectionStringTest() { configStore.setConnectionString(TEST_CONN_STRING); configStore.validateAndInit(); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); + clientBuilder = new AppConfigurationReplicaClientsBuilder(clientFactoryMock, null, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); when(clientFactoryMock.build()).thenReturn(builderMock); @@ -117,7 +117,7 @@ public void buildClientFromConnectionStringTest() { @Test public void modifyClientTest() { - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, modifierMock, false, false); + clientBuilder = new AppConfigurationReplicaClientsBuilder(clientFactoryMock, modifierMock, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); @@ -148,7 +148,7 @@ public void buildClientsFromMultipleEndpointsTest() { configStore.validateAndInit(); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); + clientBuilder = new AppConfigurationReplicaClientsBuilder(clientFactoryMock, null, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); @@ -174,7 +174,7 @@ public void buildClientsFromMultipleConnectionStringsTest() { configStore.validateAndInit(); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); + clientBuilder = new AppConfigurationReplicaClientsBuilder(clientFactoryMock, null, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); @@ -198,7 +198,7 @@ public void endpointAndConnectionString() { configStore.setConnectionString(TEST_CONN_STRING); configStore.validateAndInit(); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); + clientBuilder = new AppConfigurationReplicaClientsBuilder(clientFactoryMock, null, false, false); String message = assertThrows(IllegalArgumentException.class, () -> clientBuilder.buildClients(configStore).get(0)).getMessage(); @@ -208,7 +208,7 @@ public void endpointAndConnectionString() { @Test public void buildClientTest() { - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); + clientBuilder = new AppConfigurationReplicaClientsBuilder(clientFactoryMock, null, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); when(clientFactoryMock.build()).thenReturn(builderMock); @@ -227,7 +227,7 @@ public void buildClientTest() { @Test public void buildClientConnectionStringTest() { configStore.setConnectionString(TEST_CONN_STRING); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); + clientBuilder = new AppConfigurationReplicaClientsBuilder(clientFactoryMock, null, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); when(clientFactoryMock.build()).thenReturn(builderMock); @@ -245,7 +245,7 @@ public void buildClientConnectionStringTest() { @Test public void buildClientConnectionStringsTest() { configStore.setConnectionStrings(List.of(TEST_CONN_STRING, TEST_CONN_STRING_GEO)); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); + clientBuilder = new AppConfigurationReplicaClientsBuilder(clientFactoryMock, null, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); when(builderMock.addPolicy(Mockito.any())).thenReturn(builderMock); @@ -263,7 +263,7 @@ public void buildClientConnectionStringsTest() { @Test public void buildClientConnectionStringInvalidTest() { configStore.setConnectionString(TEST_CONN_STRING); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); + clientBuilder = new AppConfigurationReplicaClientsBuilder(clientFactoryMock, null, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, @@ -275,7 +275,7 @@ public void buildClientConnectionStringInvalidTest() { @Test public void buildClientConnectionStringInvalid2Test() { configStore.setConnectionString("Not A Connection String"); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); + clientBuilder = new AppConfigurationReplicaClientsBuilder(clientFactoryMock, null, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, @@ -287,7 +287,7 @@ public void buildClientConnectionStringInvalid2Test() { @Test public void buildClientConnectionStringInvalid3Test() { configStore.setConnectionString("Not;A;Connection String"); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, null, false, false); + clientBuilder = new AppConfigurationReplicaClientsBuilder(clientFactoryMock, null, false, false); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java index 92704d28e88b..4615b389cbaf 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java @@ -22,7 +22,6 @@ import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -86,7 +85,7 @@ public void loadFeatureFlagsTestNoFeatureFlags() { assertEquals(1, featureFlagsList.size()); assertEquals(featureFlags, featureFlagsList.get(0)); assertEquals("FakeKey", featureFlagsList.get(0).getFeatureFlags().get(0).getKey()); - assertEquals(0, featureFlagClient.getProperties().size()); + assertEquals(0, featureFlagClient.getFeatureFlags().size()); } @Test @@ -101,7 +100,7 @@ public void loadFeatureFlagsTestFeatureFlags() { assertEquals(featureFlags, featureFlagsList.get(0)); assertEquals(".appconfig.featureflag/Alpha", featureFlagsList.get(0).getFeatureFlags().get(0).getKey()); assertEquals(".appconfig.featureflag/Beta", featureFlagsList.get(0).getFeatureFlags().get(1).getKey()); - assertEquals(2, featureFlagClient.getProperties().size()); + assertEquals(2, featureFlagClient.getFeatureFlags().size()); } @Test @@ -116,7 +115,7 @@ public void loadFeatureFlagsTestMultipleLoads() { assertEquals(featureFlags, featureFlagsList.get(0)); assertEquals(".appconfig.featureflag/Alpha", featureFlagsList.get(0).getFeatureFlags().get(0).getKey()); assertEquals(".appconfig.featureflag/Beta", featureFlagsList.get(0).getFeatureFlags().get(1).getKey()); - assertEquals(2, featureFlagClient.getProperties().size()); + assertEquals(2, featureFlagClient.getFeatureFlags().size()); List settings2 = List.of(new FeatureFlagConfigurationSetting("Alpha", true), new FeatureFlagConfigurationSetting("Gamma", false)); @@ -128,11 +127,11 @@ public void loadFeatureFlagsTestMultipleLoads() { assertEquals(featureFlags, featureFlagsList.get(0)); assertEquals(".appconfig.featureflag/Alpha", featureFlagsList.get(0).getFeatureFlags().get(0).getKey()); assertEquals(".appconfig.featureflag/Gamma", featureFlagsList.get(0).getFeatureFlags().get(1).getKey()); - assertEquals(3, featureFlagClient.getProperties().size()); - Map features = featureFlagClient.getProperties(); - assertTrue(features.get(".appconfig.featureflag/Alpha").isEnabled()); - assertTrue(features.get(".appconfig.featureflag/Beta").isEnabled()); - assertFalse(features.get(".appconfig.featureflag/Gamma").isEnabled()); + assertEquals(3, featureFlagClient.getFeatureFlags().size()); + List features = featureFlagClient.getFeatureFlags(); + assertTrue(features.get(0).isEnabled()); + assertTrue(features.get(1).isEnabled()); + assertFalse(features.get(2).isEnabled()); } @Test @@ -173,7 +172,7 @@ public void loadFeatureFlagsTestTargetingFilter() { assertEquals(1, featureFlagsList.size()); assertEquals(featureFlags, featureFlagsList.get(0)); assertEquals(".appconfig.featureflag/TargetingTest", featureFlagsList.get(0).getFeatureFlags().get(0).getKey()); - assertEquals(1, featureFlagClient.getProperties().size()); + assertEquals(1, featureFlagClient.getFeatureFlags().size()); } @Test diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/stores/KeyVaultClientTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/stores/KeyVaultClientTest.java index 98f2c3ec9bc8..d3ff2c487311 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/stores/KeyVaultClientTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/stores/KeyVaultClientTest.java @@ -59,7 +59,7 @@ public void configProviderAuth() throws URISyntaxException { String keyVaultUri = "https://keyvault.vault.azure.net"; clientStore = new AppConfigurationSecretClientManager(keyVaultUri, null, null, secretClientBuilderFactoryMock, - false, 60); + false); AppConfigurationSecretClientManager test = Mockito.spy(clientStore); when(secretClientBuilderFactoryMock.build()).thenReturn(builderMock); @@ -82,7 +82,7 @@ public void systemAssignedCredentials() throws URISyntaxException { String keyVaultUri = "https://keyvault.vault.azure.net/secrets/mySecret"; clientStore = new AppConfigurationSecretClientManager(keyVaultUri, null, null, secretClientBuilderFactoryMock, - false, 60); + false); AppConfigurationSecretClientManager test = Mockito.spy(clientStore); when(secretClientBuilderFactoryMock.build()).thenReturn(builderMock); @@ -105,7 +105,7 @@ public void secretResolverTest() throws URISyntaxException { String keyVaultUri = "https://keyvault.vault.azure.net/secrets/mySecret"; clientStore = new AppConfigurationSecretClientManager(keyVaultUri, null, new TestSecretResolver(), - secretClientBuilderFactoryMock, false, 60); + secretClientBuilderFactoryMock, false); AppConfigurationSecretClientManager test = Mockito.spy(clientStore); when(secretClientBuilderFactoryMock.build()).thenReturn(builderMock); diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java index 97eb31aed40f..313030cd66dd 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java @@ -3,15 +3,23 @@ package com.azure.spring.cloud.feature.management; import org.springframework.beans.BeansException; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; import org.springframework.lang.NonNull; +import com.azure.spring.cloud.feature.management.filters.PercentageFilter; +import com.azure.spring.cloud.feature.management.filters.TargetingFilter; +import com.azure.spring.cloud.feature.management.filters.TimeWindowFilter; import com.azure.spring.cloud.feature.management.implementation.FeatureManagementConfigProperties; import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties; +import com.azure.spring.cloud.feature.management.targeting.TargetingContextAccessor; +import com.azure.spring.cloud.feature.management.targeting.TargetingEvaluationOptions; /** * Configuration for setting up FeatureManager @@ -40,4 +48,24 @@ FeatureManager featureManager(FeatureManagementProperties featureManagementConfi public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException { this.appContext = applicationContext; } + + @Bean(name = "Microsoft.TimeWindow") + @ConditionalOnMissingBean(TimeWindowFilter.class) + public TimeWindowFilter timeWindowFilter() { + return new TimeWindowFilter(); + } + + @Bean(name = "Microsoft.Percentage") + @ConditionalOnMissingBean(PercentageFilter.class) + public PercentageFilter percentageFilter() { + return new PercentageFilter(); + } + + @Bean(name = "Microsoft.Targeting") + @Scope("request") + @ConditionalOnMissingBean(TargetingFilter.class) + @ConditionalOnBean(TargetingContextAccessor.class) + public TargetingFilter targettingFilter(TargetingContextAccessor context) { + return new TargetingFilter(context, new TargetingEvaluationOptions().setIgnoreCase(true)); + } } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementProperties.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementProperties.java index c9a1da8a834a..73f9c01d891a 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementProperties.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementProperties.java @@ -2,18 +2,23 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.management.implementation; +import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; import com.azure.spring.cloud.feature.management.models.Feature; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; /** * Configuration Properties for Feature Management. Processes the configurations to be usable by Feature Management. */ @ConfigurationProperties(prefix = "feature-management") public class FeatureManagementProperties { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @JsonProperty("feature-flags") private List featureFlags; @@ -22,8 +27,11 @@ public List getFeatureFlags() { return featureFlags; } - public void setFeatureFlags(List featureFlags) { - this.featureFlags = featureFlags; + public void setFeatureFlags(List> featureFlags) { + this.featureFlags = new ArrayList<>(); + for (Map featureFlag: featureFlags) { + this.featureFlags.add(OBJECT_MAPPER.convertValue(featureFlag, Feature.class)); + } } } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagementConfigurationTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagementConfigurationTest.java index 5bb4dc631560..388a03aca3ab 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagementConfigurationTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagementConfigurationTest.java @@ -8,6 +8,10 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import com.azure.spring.cloud.feature.management.filters.PercentageFilter; +import com.azure.spring.cloud.feature.management.filters.TargetingFilter; +import com.azure.spring.cloud.feature.management.filters.TimeWindowFilter; +import com.azure.spring.cloud.feature.management.targeting.TargetingContextAccessor; import com.azure.spring.cloud.feature.management.targeting.TargetingEvaluationOptions; public class FeatureManagementConfigurationTest { @@ -24,6 +28,9 @@ public void featureManagementTest() { CONTEXT_RUNNER.run(context -> { assertThat(context).hasSingleBean(FeatureManager.class); assertThat(context).doesNotHaveBean(TargetingEvaluationOptions.class); + assertThat(context).hasSingleBean(TimeWindowFilter.class); + assertThat(context).hasSingleBean(PercentageFilter.class); + assertThat(context).doesNotHaveBean(TargetingFilter.class); }); } @@ -32,7 +39,21 @@ public void featureManagementWithEvaluationOptionsTest() { CONTEXT_RUNNER_OPTIONS.run(context -> { assertThat(context).hasSingleBean(TargetingEvaluationOptions.class); assertThat(context).hasSingleBean(FeatureManager.class); + assertThat(context).hasSingleBean(TimeWindowFilter.class); + assertThat(context).hasSingleBean(PercentageFilter.class); + assertThat(context).doesNotHaveBean(TargetingFilter.class); }); } + @Test + public void featureManagementWithAccessorTest() { + CONTEXT_RUNNER.withUserConfiguration(TargetingContextAccessorTestConfiguration.class) + .run(context -> { + assertThat(context).hasSingleBean(FeatureManager.class); + assertThat(context).hasSingleBean(TimeWindowFilter.class); + assertThat(context).hasSingleBean(PercentageFilter.class); + assertThat(context).hasSingleBean(TargetingContextAccessor.class); + assertThat(context).hasSingleBean(TargetingFilter.class); + }); + } } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/TargetingContextAccessorTestConfiguration.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/TargetingContextAccessorTestConfiguration.java new file mode 100644 index 000000000000..882abb74c509 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/TargetingContextAccessorTestConfiguration.java @@ -0,0 +1,31 @@ +package com.azure.spring.cloud.feature.management; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.azure.spring.cloud.feature.management.implementation.FeatureManagementConfigProperties; +import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties; +import com.azure.spring.cloud.feature.management.targeting.TargetingContext; +import com.azure.spring.cloud.feature.management.targeting.TargetingContextAccessor; + +@Configuration +@EnableConfigurationProperties({ FeatureManagementConfigProperties.class, FeatureManagementProperties.class }) +public class TargetingContextAccessorTestConfiguration { + + @Bean + public TargetingContextAccessor targetingAccessor() { + return new TestAccessor(); + } + + + + class TestAccessor implements TargetingContextAccessor { + + @Override + public void configureTargetingContext(TargetingContext context) { + } + + } + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementPropertiesTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementPropertiesTest.java new file mode 100644 index 000000000000..6e23c9168338 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementPropertiesTest.java @@ -0,0 +1,32 @@ +package com.azure.spring.cloud.feature.management.implementation; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +public class FeatureManagementPropertiesTest { + + + @Test + public void setFeatureFlagsTest() { + FeatureManagementProperties properties = new FeatureManagementProperties(); + List> featureFlags = new ArrayList<>(); + properties.setFeatureFlags(featureFlags); + assertEquals(0, properties.getFeatureFlags().size()); + + Map alphaFeatureFlag = new HashMap(); + alphaFeatureFlag.put("id", "alpha"); + alphaFeatureFlag.put("enabled", true); + featureFlags.add(alphaFeatureFlag); + properties.setFeatureFlags(featureFlags); + assertEquals(1, properties.getFeatureFlags().size()); + assertEquals("alpha", properties.getFeatureFlags().get(0).getId()); + assertEquals(true, properties.getFeatureFlags().get(0).isEnabled()); + } + +} From 736d9f5e0754dbf4c0822aec2c1de39725a58cd8 Mon Sep 17 00:00:00 2001 From: Matthew Metcalf Date: Mon, 24 Feb 2025 15:15:46 -0800 Subject: [PATCH 08/41] Variants (#42853) * variants * Fixing Targeting Filter * off by one * Fixing Variant assignment * review comments * Default Timeout * Update sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilterAsync.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Review comments * Fixing Merge Issues * Update TargetingFilterTest.java * Fixing doc comments and removed ContextualAccessor * fixed missing headers * Removed dead code * Update FeatureManager.java * Update FeatureManager.java * Update FeatureManager.java --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../FeatureManagementConfiguration.java | 13 +- .../feature/management/FeatureManager.java | 232 ++++++++++++- .../management/filters/PercentageFilter.java | 5 +- .../management/filters/TimeWindowFilter.java | 19 +- .../implementation/FeatureFilterUtils.java | 1 - .../models/RecurrencePattern.java | 6 +- .../models/RecurrenceRange.java | 6 +- .../timewindow/TimeWindowFilterSettings.java | 4 +- .../timewindow/TimeWindowUtils.java | 7 +- .../recurrence/RecurrenceEvaluator.java | 10 +- .../recurrence/RecurrenceValidator.java | 18 +- .../feature/management/models/Allocation.java | 127 +++++++ .../management/models/EvaluationEvent.java | 36 ++ .../feature/management/models/Feature.java | 39 +++ .../models/FeatureManagementException.java | 2 +- .../management/models/GroupAllocation.java | 44 +++ .../models/PercentileAllocation.java | 61 ++++ .../management/models/UserAllocation.java | 44 +++ .../feature/management/models/Variant.java | 52 +++ .../models/VariantAssignmentReason.java | 37 +++ .../management/models/VariantReference.java | 67 ++++ .../management/FeatureManagerTest.java | 2 +- .../management/FeatureManagerVariantTest.java | 312 ++++++++++++++++++ .../recurrence/RecurrenceEvaluatorTest.java | 15 +- .../recurrence/RecurrenceValidatorTest.java | 23 +- 25 files changed, 1121 insertions(+), 61 deletions(-) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Allocation.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/GroupAllocation.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/PercentileAllocation.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/UserAllocation.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Variant.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/VariantAssignmentReason.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/VariantReference.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerVariantTest.java diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java index 313030cd66dd..106001a42214 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java @@ -3,6 +3,7 @@ package com.azure.spring.cloud.feature.management; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -40,8 +41,16 @@ class FeatureManagementConfiguration implements ApplicationContextAware { */ @Bean FeatureManager featureManager(FeatureManagementProperties featureManagementConfigurations, - FeatureManagementConfigProperties properties) { - return new FeatureManager(appContext, featureManagementConfigurations, properties); + FeatureManagementConfigProperties properties, + ObjectProvider contextAccessorProvider, + ObjectProvider evaluationOptionsProvider) { + + TargetingContextAccessor contextAccessor = contextAccessorProvider.getIfAvailable(); + TargetingEvaluationOptions evaluationOptions = evaluationOptionsProvider + .getIfAvailable(() -> new TargetingEvaluationOptions()); + + return new FeatureManager(appContext, featureManagementConfigurations, properties, contextAccessor, + evaluationOptions); } @Override diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java index 1e13cdc543e4..800bb014e2a3 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java @@ -15,18 +15,32 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; import com.azure.spring.cloud.feature.management.filters.ContextualFeatureFilter; import com.azure.spring.cloud.feature.management.filters.ContextualFeatureFilterAsync; import com.azure.spring.cloud.feature.management.filters.FeatureFilter; import com.azure.spring.cloud.feature.management.filters.FeatureFilterAsync; +import com.azure.spring.cloud.feature.management.implementation.FeatureFilterUtils; import com.azure.spring.cloud.feature.management.implementation.FeatureManagementConfigProperties; import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties; +import com.azure.spring.cloud.feature.management.models.Allocation; import com.azure.spring.cloud.feature.management.models.Conditions; import com.azure.spring.cloud.feature.management.models.EvaluationEvent; import com.azure.spring.cloud.feature.management.models.Feature; import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext; +import com.azure.spring.cloud.feature.management.models.FeatureManagementException; import com.azure.spring.cloud.feature.management.models.FilterNotFoundException; +import com.azure.spring.cloud.feature.management.models.GroupAllocation; +import com.azure.spring.cloud.feature.management.models.PercentileAllocation; +import com.azure.spring.cloud.feature.management.models.UserAllocation; +import com.azure.spring.cloud.feature.management.models.Variant; +import com.azure.spring.cloud.feature.management.models.VariantAssignmentReason; +import com.azure.spring.cloud.feature.management.models.VariantReference; +import com.azure.spring.cloud.feature.management.targeting.TargetingContext; +import com.azure.spring.cloud.feature.management.targeting.TargetingContextAccessor; +import com.azure.spring.cloud.feature.management.targeting.TargetingEvaluationOptions; +import com.azure.spring.cloud.feature.management.targeting.TargetingFilterContext; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -46,6 +60,10 @@ public class FeatureManager { private static final Duration DEFAULT_BLOCK_TIMEOUT = Duration.ofSeconds(100); + private final TargetingContextAccessor contextAccessor; + + private final TargetingEvaluationOptions evaluationOptions; + /** * Can be called to check if a feature is enabled or disabled. * @@ -54,10 +72,13 @@ public class FeatureManager { * @param properties FeatureManagementConfigProperties */ FeatureManager(ApplicationContext context, FeatureManagementProperties featureManagementConfigurations, - FeatureManagementConfigProperties properties) { + FeatureManagementConfigProperties properties, TargetingContextAccessor contextAccessor, + TargetingEvaluationOptions evaluationOptions) { this.context = context; this.featureManagementConfigurations = featureManagementConfigurations; this.properties = properties; + this.contextAccessor = contextAccessor; + this.evaluationOptions = evaluationOptions; } /** @@ -101,7 +122,7 @@ public Mono isEnabledAsync(String feature, Object featureContext) { } /** - * Checks to see if the feature is enabled. If enabled it checks each filter, once a single filter returns true it + * Checks to see if the feature is enabled. If enabled it check each filter, once a single filter returns true it * returns true. If no filter returns true, it returns false. If there are no filters, it returns true. If feature * isn't found it returns false. * @@ -114,6 +135,48 @@ public Boolean isEnabled(String feature, Object featureContext) throws FilterNot return checkFeature(feature, featureContext).map(event -> event.isEnabled()).block(DEFAULT_BLOCK_TIMEOUT); } + /** + * Returns the variant assigned to the current context. + * + * @param feature Feature being checked. + * @return Assigned Variant + */ + public Variant getVariant(String feature) { + return checkFeature(feature, null).block().getVariant(); + } + + /** + * Returns the variant assigned to the current context. + * + * @param feature Feature being checked. + * @param featureContext Local context + * @return Assigned Variant + */ + public Variant getVariant(String feature, Object featureContext) { + return checkFeature(feature, featureContext).block().getVariant(); + } + + /** + * Returns the variant assigned to the current context. + * + * @param feature Feature being checked. + * @return Assigned Variant + */ + public Mono getVariantAsync(String feature) { + return checkFeature(feature, null).map(event -> event.getVariant()); + } + + /** + * Returns the variant assigned to the current context. + * + * @param feature Feature being checked. + * @param featureContext Local context + * @return Assigned Variant + */ + public Mono getVariantAsync(String feature, Object featureContext) { + return checkFeature(feature, featureContext).map(event -> event.getVariant()); + } + private Mono checkFeature(String featureName, Object featureContext) throws FilterNotFoundException { Feature featureFlag = featureManagementConfigurations.getFeatureFlags().stream() @@ -127,15 +190,161 @@ private Mono checkFeature(String featureName, Object featureCon } if (!featureFlag.isEnabled()) { + this.assignDefaultDisabledReason(event); + // If a feature flag is disabled and override can't enable it return Mono.just(event.setEnabled(false)); } Mono result = this.checkFeatureFilters(event, featureContext); + result = assignAllocation(result); return result; } + private Mono assignAllocation(Mono monoEvent) { + + return monoEvent.map(event -> { + Feature featureFlag = event.getFeature(); + + if (featureFlag.getVariants() == null || featureFlag.getAllocation() == null) { + return event; + } + + if (!event.isEnabled()) { + this.assignDefaultDisabledReason(event); + return event; + } + this.assignVariant(event); + return event; + }); + } + + private void assignDefaultDisabledReason(EvaluationEvent event) { + Feature featureFlag = event.getFeature(); + event.setReason(VariantAssignmentReason.DEFAULT_WHEN_DISABLED); + if (event.getFeature().getAllocation() == null) { + return; + } + this.assignVariantOverride(event.getFeature().getVariants(), + event.getFeature().getAllocation().getDefaultWhenDisabled(), false, event); + + if (featureFlag.getAllocation() != null) { + String variantName = featureFlag.getAllocation().getDefaultWhenDisabled(); + event.setVariant(this.variantNameToVariant(featureFlag, variantName)); + } + } + + private void assignDefaultEnabledVariant(EvaluationEvent event) { + event.setReason(VariantAssignmentReason.DEFAULT_WHEN_ENABLED); + if (event.getFeature().getAllocation() == null) { + return; + } + this.assignVariantOverride(event.getFeature().getVariants(), + event.getFeature().getAllocation().getDefaultWhenEnabled(), true, event); + Feature featureFlag = event.getFeature(); + + if (featureFlag.getAllocation() != null) { + event.setVariant( + this.variantNameToVariant(featureFlag, featureFlag.getAllocation().getDefaultWhenEnabled())); + return; + } + } + + private void assignVariant(EvaluationEvent event) { + Feature featureFlag = event.getFeature(); + if (featureFlag.getVariants().size() == 0 || featureFlag.getAllocation() == null) { + return; + } + + Allocation allocation = featureFlag.getAllocation(); + + TargetingContext targetingContext = buildContext(); + + List groups = targetingContext.getGroups(); + String variantName = null; + + if (StringUtils.hasText(targetingContext.getUserId())) { + // Loop through all user allocations + for (UserAllocation userAllocation : allocation.getUser()) { + if (!evaluationOptions.isIgnoreCase() + && userAllocation.getUsers().contains(targetingContext.getUserId())) { + event.setReason(VariantAssignmentReason.USER); + variantName = userAllocation.getVariant(); + break; + } else if (evaluationOptions.isIgnoreCase() + && userAllocation.getUsers().stream().anyMatch(targetingContext.getUserId()::equalsIgnoreCase)) { + event.setReason(VariantAssignmentReason.USER); + variantName = userAllocation.getVariant(); + break; + } + } + } + if (variantName == null) { + for (GroupAllocation groupAllocation : allocation.getGroup()) { + for (String allocationGroup : groupAllocation.getGroups()) { + if (!evaluationOptions.isIgnoreCase() && groups.contains(allocationGroup)) { + event.setReason(VariantAssignmentReason.GROUP); + variantName = groupAllocation.getVariant(); + break; + } else if (evaluationOptions.isIgnoreCase() + && groups.stream().anyMatch(allocationGroup::equalsIgnoreCase)) { + event.setReason(VariantAssignmentReason.GROUP); + variantName = groupAllocation.getVariant(); + break; + } + } + if (variantName != null) { + break; + } + } + } + + if (variantName == null) { + String seed = allocation.getSeed(); + if (!StringUtils.hasText(seed)) { + seed = "allocation\n" + featureFlag.getId(); + } + String contextId = targetingContext.getUserId() + "\n" + seed; + double box = FeatureFilterUtils.isTargetedPercentage(contextId); + for (PercentileAllocation percentileAllocation : allocation.getPercentile()) { + Double to = percentileAllocation.getTo(); + if ((box == 100 && to == 100) || (percentileAllocation.getFrom() <= box && box < to)) { + event.setReason(VariantAssignmentReason.PERCENTILE); + variantName = percentileAllocation.getVariant(); + break; + } + } + } + + if (variantName == null) { + this.assignDefaultEnabledVariant(event); + } + + event.setVariant(variantNameToVariant(featureFlag, variantName)); + assignVariantOverride(featureFlag.getVariants(), variantName, true, event); + } + + private void assignVariantOverride(List variants, String defaultVariantName, boolean status, + EvaluationEvent event) { + if (variants.size() == 0 || !StringUtils.hasText(defaultVariantName)) { + return; + } + for (VariantReference variant : variants) { + if (variant.getName().equals(defaultVariantName)) { + if ("Enabled".equals(variant.getStatusOverride())) { + event.setEnabled(true); + return; + } + if ("Disabled".equals(variant.getStatusOverride())) { + event.setEnabled(false); + return; + } + } + } + event.setEnabled(status); + } + private Mono checkFeatureFilters(EvaluationEvent event, Object featureContext) { Feature featureFlag = event.getFeature(); Conditions conditions = featureFlag.getConditions(); @@ -186,6 +395,25 @@ private Mono checkFeatureFilters(EvaluationEvent event, Object return Flux.merge(filterResults).reduce((a, b) -> a || b).single().map(result -> event.setEnabled(result)); } + private Variant variantNameToVariant(Feature featureFlag, String variantName) { + for (VariantReference variant : featureFlag.getVariants()) { + if (variant.getName().equals(variantName)) { + return new Variant(variantName, variant.getConfigurationValue()); + } + } + return null; + } + + private TargetingFilterContext buildContext() { + TargetingFilterContext targetingContext = new TargetingFilterContext(); + if (contextAccessor != null) { + // If this is the only one provided just use it. + contextAccessor.configureTargetingContext(targetingContext); + return targetingContext; + } + throw new FeatureManagementException("No Targeting Filter Context found to assign variant."); + } + /** * Returns the names of all features flags * diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/PercentageFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/PercentageFilter.java index 256ce9b537fd..6418ddc14d03 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/PercentageFilter.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/PercentageFilter.java @@ -2,11 +2,12 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.management.filters; -import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext; +import static com.azure.spring.cloud.feature.management.models.FilterParameters.PERCENTAGE_FILTER_SETTING; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static com.azure.spring.cloud.feature.management.models.FilterParameters.PERCENTAGE_FILTER_SETTING; +import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext; /** * A feature filter that can be used to activate a feature based on a random percentage. diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TimeWindowFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TimeWindowFilter.java index b58efddce703..a25274677a09 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TimeWindowFilter.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TimeWindowFilter.java @@ -2,6 +2,16 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.management.filters; +import static com.azure.spring.cloud.feature.management.models.FilterParameters.TIME_WINDOW_FILTER_SETTING_END; +import static com.azure.spring.cloud.feature.management.models.FilterParameters.TIME_WINDOW_FILTER_SETTING_RECURRENCE; +import static com.azure.spring.cloud.feature.management.models.FilterParameters.TIME_WINDOW_FILTER_SETTING_START; + +import java.time.ZonedDateTime; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.azure.spring.cloud.feature.management.implementation.FeatureFilterUtils; import com.azure.spring.cloud.feature.management.implementation.timewindow.TimeWindowFilterSettings; import com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence.RecurrenceConstants; @@ -10,15 +20,6 @@ import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.time.ZonedDateTime; -import java.util.Map; - -import static com.azure.spring.cloud.feature.management.models.FilterParameters.TIME_WINDOW_FILTER_SETTING_END; -import static com.azure.spring.cloud.feature.management.models.FilterParameters.TIME_WINDOW_FILTER_SETTING_RECURRENCE; -import static com.azure.spring.cloud.feature.management.models.FilterParameters.TIME_WINDOW_FILTER_SETTING_START; /** * A feature filter that can be used at activate a feature based on a time window. diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java index 5f8fe1cf9925..fd52e85912ee 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java @@ -83,5 +83,4 @@ public static BigInteger bigEndianToLittleEndian(byte[] bigEndian) { } return new BigInteger(1, reversedBytes); } - } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/RecurrencePattern.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/RecurrencePattern.java index 330a89d82763..6addcea494f6 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/RecurrencePattern.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/RecurrencePattern.java @@ -3,14 +3,14 @@ package com.azure.spring.cloud.feature.management.implementation.models; -import com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence.RecurrenceConstants; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - import java.time.DayOfWeek; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence.RecurrenceConstants; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + /** * The recurrence pattern specifying how often the time window repeats * */ diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/RecurrenceRange.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/RecurrenceRange.java index a9e833aed7bb..0a6bad1d3dc5 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/RecurrenceRange.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/models/RecurrenceRange.java @@ -3,13 +3,13 @@ package com.azure.spring.cloud.feature.management.implementation.models; +import java.time.ZonedDateTime; +import java.util.Arrays; + import com.azure.spring.cloud.feature.management.implementation.timewindow.TimeWindowUtils; import com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence.RecurrenceConstants; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import java.time.ZonedDateTime; -import java.util.Arrays; - /** * The recurrence range specifying how long the recurrence pattern repeats * */ diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/TimeWindowFilterSettings.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/TimeWindowFilterSettings.java index 5496bac40603..54b6675be16c 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/TimeWindowFilterSettings.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/TimeWindowFilterSettings.java @@ -2,10 +2,10 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.management.implementation.timewindow; -import com.azure.spring.cloud.feature.management.implementation.models.Recurrence; - import java.time.ZonedDateTime; +import com.azure.spring.cloud.feature.management.implementation.models.Recurrence; + public class TimeWindowFilterSettings { /** * An optional start time used to determine when a feature should be enabled. diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/TimeWindowUtils.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/TimeWindowUtils.java index 3ee3fd0883cd..11d1deee7bef 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/TimeWindowUtils.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/TimeWindowUtils.java @@ -3,9 +3,6 @@ package com.azure.spring.cloud.feature.management.implementation.timewindow; -import com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence.RecurrenceConstants; -import org.springframework.util.StringUtils; - import java.time.DayOfWeek; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @@ -15,6 +12,10 @@ import java.util.Comparator; import java.util.List; +import org.springframework.util.StringUtils; + +import com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence.RecurrenceConstants; + public class TimeWindowUtils { public static ZonedDateTime convertStringToDate(String timeStr) { if (!StringUtils.hasText(timeStr)) { diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/recurrence/RecurrenceEvaluator.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/recurrence/RecurrenceEvaluator.java index 17621fc0842b..a49837a0fe46 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/recurrence/RecurrenceEvaluator.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/recurrence/RecurrenceEvaluator.java @@ -3,6 +3,11 @@ package com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence; +import java.time.DayOfWeek; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.List; + import com.azure.spring.cloud.feature.management.implementation.models.RecurrencePattern; import com.azure.spring.cloud.feature.management.implementation.models.RecurrencePatternType; import com.azure.spring.cloud.feature.management.implementation.models.RecurrenceRange; @@ -10,11 +15,6 @@ import com.azure.spring.cloud.feature.management.implementation.timewindow.TimeWindowFilterSettings; import com.azure.spring.cloud.feature.management.implementation.timewindow.TimeWindowUtils; -import java.time.DayOfWeek; -import java.time.Duration; -import java.time.ZonedDateTime; -import java.util.List; - public class RecurrenceEvaluator { /** * Checks if a provided timestamp is within any recurring time window specified diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/recurrence/RecurrenceValidator.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/recurrence/RecurrenceValidator.java index a09f16c3c432..366e36394da5 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/recurrence/RecurrenceValidator.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/timewindow/recurrence/RecurrenceValidator.java @@ -3,12 +3,9 @@ package com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence; -import com.azure.spring.cloud.feature.management.implementation.models.Recurrence; -import com.azure.spring.cloud.feature.management.implementation.models.RecurrencePattern; -import com.azure.spring.cloud.feature.management.implementation.models.RecurrencePatternType; -import com.azure.spring.cloud.feature.management.implementation.models.RecurrenceRangeType; -import com.azure.spring.cloud.feature.management.implementation.timewindow.TimeWindowFilterSettings; -import com.azure.spring.cloud.feature.management.implementation.timewindow.TimeWindowUtils; +import static com.azure.spring.cloud.feature.management.models.FilterParameters.TIME_WINDOW_FILTER_SETTING_END; +import static com.azure.spring.cloud.feature.management.models.FilterParameters.TIME_WINDOW_FILTER_SETTING_RECURRENCE; +import static com.azure.spring.cloud.feature.management.models.FilterParameters.TIME_WINDOW_FILTER_SETTING_START; import java.time.DayOfWeek; import java.time.Duration; @@ -16,9 +13,12 @@ import java.time.temporal.ChronoUnit; import java.util.List; -import static com.azure.spring.cloud.feature.management.models.FilterParameters.TIME_WINDOW_FILTER_SETTING_END; -import static com.azure.spring.cloud.feature.management.models.FilterParameters.TIME_WINDOW_FILTER_SETTING_RECURRENCE; -import static com.azure.spring.cloud.feature.management.models.FilterParameters.TIME_WINDOW_FILTER_SETTING_START; +import com.azure.spring.cloud.feature.management.implementation.models.Recurrence; +import com.azure.spring.cloud.feature.management.implementation.models.RecurrencePattern; +import com.azure.spring.cloud.feature.management.implementation.models.RecurrencePatternType; +import com.azure.spring.cloud.feature.management.implementation.models.RecurrenceRangeType; +import com.azure.spring.cloud.feature.management.implementation.timewindow.TimeWindowFilterSettings; +import com.azure.spring.cloud.feature.management.implementation.timewindow.TimeWindowUtils; public class RecurrenceValidator { public static void validateSettings(TimeWindowFilterSettings settings) { diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Allocation.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Allocation.java new file mode 100644 index 000000000000..6d428fe38682 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Allocation.java @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.management.models; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Allocation of a feature flag to variants. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Allocation { + + @JsonProperty("default_when_enabled") + private String defaultWhenEnabled; + + @JsonProperty("default_when_disabled") + private String defaultWhenDisabled; + + private List user = new ArrayList<>(); + + private List group = new ArrayList<>(); + + private List percentile = new ArrayList<>(); + + private String seed; + + /** + * @return the defaultWhenEnabled + */ + public String getDefaultWhenEnabled() { + return defaultWhenEnabled; + } + + /** + * @param defaultWhenEnabled the defaultWhenEnabled to set + * @return Allocation + */ + public Allocation setDefaultWhenEnabled(String defaultWhenEnabled) { + this.defaultWhenEnabled = defaultWhenEnabled; + return this; + } + + /** + * @return the defaultWhenDisabled + */ + public String getDefaultWhenDisabled() { + return defaultWhenDisabled; + } + + /** + * @param defaultWhenDisabled the defaultWhenDisabled to set + * @return Allocation + */ + public Allocation setDefaultWhenDisabled(String defaultWhenDisabled) { + this.defaultWhenDisabled = defaultWhenDisabled; + return this; + } + + /** + * @return the users + */ + public List getUser() { + return user; + } + + /** + * @param user the users to set + * @return Allocation + */ + public Allocation setUser(List user) { + this.user = user; + return this; + } + + /** + * @return the groups + */ + public List getGroup() { + return group; + } + + /** + * @param group the groups to set + * @return Allocation + */ + public Allocation setGroups(List group) { + this.group = group; + return this; + } + + /** + * @return the percentile + */ + public List getPercentile() { + return percentile; + } + + /** + * @param percentile the percentile to set + * @return Allocation + */ + public Allocation setPercentile(List percentile) { + this.percentile = percentile; + return this; + } + + /** + * @return the seed + */ + public String getSeed() { + return seed; + } + + /** + * @param seed the seed to set + * @return Allocation + */ + public Allocation setSeed(String seed) { + this.seed = seed; + return this; + } + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/EvaluationEvent.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/EvaluationEvent.java index 786269d9ed6b..446a5d5aa911 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/EvaluationEvent.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/EvaluationEvent.java @@ -13,6 +13,10 @@ public class EvaluationEvent { private boolean enabled = false; + private Variant variant; + + private VariantAssignmentReason reason = VariantAssignmentReason.NONE; + /** * Creates an Evaluation Event for the given feature * @param feature Feature @@ -59,4 +63,36 @@ public EvaluationEvent setEnabled(boolean enabled) { this.enabled = enabled; return this; } + + /** + * @return the variant + */ + public Variant getVariant() { + return variant; + } + + /** + * @param variant the variant to set + * @return EvaluationEvent + */ + public EvaluationEvent setVariant(Variant variant) { + this.variant = variant; + return this; + } + + /** + * @return the reason + */ + public VariantAssignmentReason getReason() { + return reason; + } + + /** + * @param reason the reason to set + * @return EvaluationEvent + */ + public EvaluationEvent setReason(VariantAssignmentReason reason) { + this.reason = reason; + return this; + } } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java index e25f75b77278..f292fa6e6969 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java @@ -3,6 +3,7 @@ package com.azure.spring.cloud.feature.management.models; +import java.util.List; import org.springframework.lang.NonNull; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -26,6 +27,12 @@ public class Feature { @NonNull private Conditions conditions = new Conditions(); + @JsonProperty("allocation") + private Allocation allocation; + + @JsonProperty("variants") + private List variants; + /** * @return the id */ @@ -89,4 +96,36 @@ public Feature setConditions(Conditions conditions) { this.conditions = conditions; return this; } + + /** + * @return the allocation + */ + public Allocation getAllocation() { + return allocation; + } + + /** + * @param allocation the allocation to set + * @return Feature + */ + public Feature setAllocation(Allocation allocation) { + this.allocation = allocation; + return this; + } + + /** + * @return the variants + */ + public List getVariants() { + return variants; + } + + /** + * @param variants the variants to set + * @return Feature + */ + public Feature setVariants(List variants) { + this.variants = variants; + return this; + } } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FeatureManagementException.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FeatureManagementException.java index 30ce0305f987..c4312595e9f0 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FeatureManagementException.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FeatureManagementException.java @@ -31,7 +31,7 @@ public FeatureManagementException(String message) { * @param message the error message. * @param cause the original error thrown, typically of NoSuchBeanDefinitionException type. */ - FeatureManagementException(String message, Throwable cause) { + public FeatureManagementException(String message, Throwable cause) { super(message, cause); this.message = message; } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/GroupAllocation.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/GroupAllocation.java new file mode 100644 index 000000000000..36c2f845322a --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/GroupAllocation.java @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.management.models; + +import java.util.List; + +/** + * Group allocation of a variant. Contains a variant and a list of groups assigned to the variant. + */ +public class GroupAllocation { + + private String variant; + + private List groups; + + /** + * @return the variant + */ + public String getVariant() { + return variant; + } + + /** + * @param variant the variant to set + */ + public void setVariant(String variant) { + this.variant = variant; + } + + /** + * @return the groups + */ + public List getGroups() { + return groups; + } + + /** + * @param groups the groups to set + */ + public void setGroups(List groups) { + this.groups = groups; + } + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/PercentileAllocation.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/PercentileAllocation.java new file mode 100644 index 000000000000..dde6abbffe02 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/PercentileAllocation.java @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.management.models; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Percentile allocation of a variant. Contains a variant and a range of users assigned to the variant. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class PercentileAllocation { + + private String variant; + + private Double from; + + private Double to; + + /** + * @return the variant + */ + public String getVariant() { + return variant; + } + + /** + * @param variant the variant to set + */ + public void setVariant(String variant) { + this.variant = variant; + } + + /** + * @return the from + */ + public Double getFrom() { + return from; + } + + /** + * @param from the from to set + */ + public void setFrom(Double from) { + this.from = from; + } + + /** + * @return the to + */ + public Double getTo() { + return to; + } + + /** + * @param to the to to set + */ + public void setTo(Double to) { + this.to = to; + } + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/UserAllocation.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/UserAllocation.java new file mode 100644 index 000000000000..c63d9261fd62 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/UserAllocation.java @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.management.models; + +import java.util.List; + +/** + * User allocation of a variant. Contains a variant and a list of users assigned to the variant. + */ +public class UserAllocation { + + private String variant; + + private List users; + + /** + * @return the variant + */ + public String getVariant() { + return variant; + } + + /** + * @param variant the variant to set + */ + public void setVariant(String variant) { + this.variant = variant; + } + + /** + * @return the users + */ + public List getUsers() { + return users; + } + + /** + * @param users the users to set + */ + public void setUsers(List users) { + this.users = users; + } + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Variant.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Variant.java new file mode 100644 index 000000000000..f811e69ad7c4 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Variant.java @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.management.models; + +/** + * The return object of getVariant that has the name of the variant and the instance value of the variant. + */ +public class Variant { + + private String name; + + private Object value; + + /** + * Variant + * @param name Name of the Variant + * @param value Instance of the Variant + */ + public Variant(String name, Object value) { + this.name = name; + this.value = value; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the value + */ + public Object getValue() { + return value; + } + + /** + * @param value the value to set + */ + public void setValue(Object value) { + this.value = value; + } + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/VariantAssignmentReason.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/VariantAssignmentReason.java new file mode 100644 index 000000000000..334d402ccc03 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/VariantAssignmentReason.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.feature.management.models; + +/** + * The reason why a given boolean/variant was returned when calling isEnabled/getVariant. + */ +public enum VariantAssignmentReason { + + /** None */ + NONE("None"), + /** Default when Disabled */ + DEFAULT_WHEN_DISABLED("DefaultWhenDisabled"), + /** Default when Enabled */ + DEFAULT_WHEN_ENABLED("DefaultWhenEnabled"), + /** User */ + USER("User"), + /** Group */ + GROUP("Group"), + /** Percentile */ + PERCENTILE("Percentile"); + + private final String type; + + VariantAssignmentReason(final String type) { + this.type = type; + } + + /** + * @return the type + */ + public String getType() { + return type; + } + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/VariantReference.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/VariantReference.java new file mode 100644 index 000000000000..7fe054cfcda8 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/VariantReference.java @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.management.models; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Reference to a Variant containing the Variant name, configuration value, and possible status override. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class VariantReference { + + private String name; + + private Object configurationValue; + + private String statusOverride; + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name the name to set + * @return VariantReference + */ + public VariantReference setName(String name) { + this.name = name; + return this; + } + + /** + * @return the configurationValue + */ + public Object getConfigurationValue() { + return configurationValue; + } + + /** + * @param configurationValue the configurationValue to set + * @return VariantReference + */ + public VariantReference setConfigurationValue(Object configurationValue) { + this.configurationValue = configurationValue; + return this; + } + + /** + * @return the statusOverride + */ + public String getStatusOverride() { + return statusOverride; + } + + /** + * @param statusOverride the statusOverride to set + * @return VariantReference + */ + public VariantReference setStatusOverride(String statusOverride) { + this.statusOverride = statusOverride; + return this; + } + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java index e119279cfb9b..bceb7de20fae 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java @@ -56,7 +56,7 @@ public void setup() { MockitoAnnotations.openMocks(this); when(properties.isFailFast()).thenReturn(true); - featureManager = new FeatureManager(context, featureManagementPropertiesMock, properties); + featureManager = new FeatureManager(context, featureManagementPropertiesMock, properties, null, null); } @AfterEach diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerVariantTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerVariantTest.java new file mode 100644 index 000000000000..7638d141bd55 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerVariantTest.java @@ -0,0 +1,312 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.management; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; + +import com.azure.spring.cloud.feature.management.filters.FeatureFilter; +import com.azure.spring.cloud.feature.management.implementation.FeatureManagementConfigProperties; +import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties; +import com.azure.spring.cloud.feature.management.models.Allocation; +import com.azure.spring.cloud.feature.management.models.Conditions; +import com.azure.spring.cloud.feature.management.models.Feature; +import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext; +import com.azure.spring.cloud.feature.management.models.Variant; +import com.azure.spring.cloud.feature.management.models.VariantReference; +import com.azure.spring.cloud.feature.management.targeting.TargetingContextAccessor; + +/** + * Unit tests for FeatureManager. + */ +@SpringBootTest(classes = { FeatureManagementTestConfigurations.class, SpringBootTest.class }) +public class FeatureManagerVariantTest { + + private FeatureManager featureManager; + + @Mock + private ApplicationContext context; + + @Mock + private FeatureManagementConfigProperties properties; + + @Mock + private FeatureManagementProperties featureManagementPropertiesMock; + + @Mock + private TargetingContextAccessor contextAccessorMock; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + when(properties.isFailFast()).thenReturn(true); + + featureManager = new FeatureManager(context, featureManagementPropertiesMock, properties, contextAccessorMock, + null); + } + + @AfterEach + public void cleanup() throws Exception { + MockitoAnnotations.openMocks(this).close(); + } + + @Test + public void isEnabledFeatureNotFound() { + assertNull(featureManager.getVariant("Unknown Banner")); + } + + @Test + public void noAssignedVariants() { + List features = List.of(new Feature().setId("No Variants")); + + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + assertNull(featureManager.getVariant("No Variants")); + } + + @Test + public void noAssigner() { + featureManager = new FeatureManager(context, featureManagementPropertiesMock, properties, null, null); + List features = List.of(new Feature().setId("No Assigner").setVariants(createVariants())); + + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + assertNull(featureManager.getVariant("No Assigner")); + } + + @Test + public void noAssignmentNoDefault() { + List features = List + .of(new Feature().setId("No Assigner").setVariants(createVariants()).setAllocation(new Allocation())); + + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + assertNull(featureManager.getVariant("No Assigner")); + } + + @Test + public void noAssignmentDefaultEnabled() { + List features = List + .of(new Feature().setId("No Assigner").setVariants(createVariants()) + .setAllocation(new Allocation().setDefaultWhenEnabled("small")).setEnabled(true)); + + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + Variant result = featureManager.getVariant("No Assigner"); + assertEquals(result.getName(), "small"); + assertEquals(result.getValue(), 1); + } + + @Test + public void noAssignmentDefaultDisabled() { + List features = List + .of(new Feature().setId("No Assigner").setVariants(createVariants()) + .setAllocation(new Allocation().setDefaultWhenDisabled("large"))); + + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + Variant result = featureManager.getVariant("No Assigner"); + assertEquals(result.getName(), "large"); + assertEquals(result.getValue(), 9); + } + + @Test + public void disabledNoDefaultDisabled() { + List features = List + .of(new Feature().setId("No Assigner").setVariants(createVariants()) + .setAllocation(new Allocation())); + + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + Variant result = featureManager.getVariant("No Assigner"); + assertNull(result); + } + + @Test + public void enabledFilterDefault() { + List features = List + .of(new Feature().setId("No Assigner").setVariants(createVariants()) + .setConditions(new Conditions().setClientFilters( + List.of(new FeatureFilterEvaluationContext().setFeatureName("No Assigner").setName("AlwaysOn")))) + .setAllocation(new Allocation().setDefaultWhenEnabled("small")).setEnabled(true)); + + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + when(context.getBean(Mockito.eq("AlwaysOn"))).thenReturn(new AlwaysOnFilter()); + Variant result = featureManager.getVariant("No Assigner"); + assertEquals(result.getName(), "small"); + assertEquals(result.getValue(), 1); + + result = featureManager.getVariantAsync("No Assigner").block(); + assertEquals(result.getName(), "small"); + assertEquals(result.getValue(), 1); + } + + @Test + public void enabledFilterDefaultAnyTrue() { + List features = List + .of(new Feature().setId("No Assigner").setVariants(createVariants()) + .setConditions(new Conditions().setClientFilters( + List.of(new FeatureFilterEvaluationContext().setFeatureName("No Assigner").setName("AlwaysOn"), + new FeatureFilterEvaluationContext().setFeatureName("No Assigner").setName("AlwaysOn")))) + .setAllocation(new Allocation().setDefaultWhenEnabled("small")).setEnabled(true)); + + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + when(context.getBean(Mockito.eq("AlwaysOn"))).thenReturn(new AlwaysOnFilter()); + Variant result = featureManager.getVariant("No Assigner"); + assertEquals(result.getName(), "small"); + assertEquals(result.getValue(), 1); + } + + @Test + public void enabledFilterDefaultAnyTrueJustOne() { + List features = List + .of(new Feature().setId("No Assigner").setVariants(createVariants()) + .setConditions(new Conditions().setClientFilters( + List.of(new FeatureFilterEvaluationContext().setFeatureName("No Assigner").setName("AlwaysOn"), + new FeatureFilterEvaluationContext().setFeatureName("No Assigner").setName("AlwaysOff")))) + .setAllocation(new Allocation().setDefaultWhenEnabled("small")).setEnabled(true)); + + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + when(context.getBean(Mockito.eq("AlwaysOn"))).thenReturn(new AlwaysOnFilter()); + Variant result = featureManager.getVariant("No Assigner"); + assertEquals(result.getName(), "small"); + assertEquals(result.getValue(), 1); + } + + @Test + public void enabledFilterDefaultAllTrueJustOne() { + List features = List + .of(new Feature().setId("No Assigner").setVariants(createVariants()) + .setConditions(new Conditions().setClientFilters( + List.of(new FeatureFilterEvaluationContext().setFeatureName("No Assigner").setName("AlwaysOn"), + new FeatureFilterEvaluationContext().setFeatureName("No Assigner").setName("AlwaysOff"))) + .setRequirementType("All")) + .setAllocation(new Allocation().setDefaultWhenEnabled("small").setDefaultWhenDisabled("large")) + .setEnabled(true)); + + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + when(context.getBean(Mockito.eq("AlwaysOn"))).thenReturn(new AlwaysOnFilter()); + when(context.getBean(Mockito.eq("AlwaysOff"))).thenReturn(new AlwaysOffFilter()); + Variant result = featureManager.getVariant("No Assigner"); + assertEquals(result.getName(), "large"); + assertEquals(result.getValue(), 9); + } + + @Test + public void enabledFilterDefaultAllTrueJustOneNoDefault() { + List features = List + .of(new Feature().setId("No Assigner").setVariants(createVariants()) + .setConditions(new Conditions().setClientFilters( + List.of(new FeatureFilterEvaluationContext().setFeatureName("No Assigner").setName("AlwaysOn"), + new FeatureFilterEvaluationContext().setFeatureName("No Assigner").setName("AlwaysOn"))) + .setRequirementType("All")) + .setEnabled(true)); + + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + when(context.getBean(Mockito.eq("AlwaysOn"))).thenReturn(new AlwaysOnFilter()); + Variant result = featureManager.getVariant("No Assigner"); + assertNull(result); + } + + @Test + public void allOnVariantOverride() { + List features = List + .of(new Feature().setId("On").setVariants(createVariants()) + .setConditions(new Conditions().setRequirementType("All").setClientFilters( + List.of(new FeatureFilterEvaluationContext().setFeatureName("No Assigner").setName("AlwaysOn"), + new FeatureFilterEvaluationContext().setFeatureName("No Assigner").setName("AlwaysOn")))) + .setAllocation(new Allocation().setDefaultWhenEnabled("large")).setEnabled(true)); + + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + + when(context.getBean(Mockito.matches("AlwaysOn"))).thenReturn(new AlwaysOnFilter()) + .thenReturn(new AlwaysOnFilter()); + + assertFalse(featureManager.isEnabledAsync("On").block()); + } + + @Test + public void allOnVariantOverrideNoDefault() { + List features = List + .of(new Feature().setId("On").setVariants(createVariants()) + .setConditions(new Conditions().setClientFilters( + List.of(new FeatureFilterEvaluationContext().setFeatureName("No Assigner").setName("AlwaysOn"), + new FeatureFilterEvaluationContext().setFeatureName("No Assigner").setName("AlwaysOn"))) + .setRequirementType("All")) + .setAllocation(new Allocation()).setEnabled(true)); + + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + + when(context.getBean(Mockito.matches("AlwaysOn"))).thenReturn(new AlwaysOnFilter()) + .thenReturn(new AlwaysOnFilter()); + + assertTrue(featureManager.isEnabledAsync("On").block()); + } + + @Test + public void allOnVariantOverrideInvalidVariant() { + List features = List.of(new Feature().setId("On").setVariants(createVariants()) + .setConditions(new Conditions() + .setClientFilters( + List.of(new FeatureFilterEvaluationContext().setFeatureName("No Assigner").setName("AlwaysOn"), + new FeatureFilterEvaluationContext().setFeatureName("No Assigner").setName("AlwaysOn"))) + .setRequirementType("All")) + .setAllocation(new Allocation()).setEnabled(true)); + + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + + when(context.getBean(Mockito.matches("AlwaysOn"))).thenReturn(new AlwaysOnFilter()) + .thenReturn(new AlwaysOnFilter()); + + assertTrue(featureManager.isEnabledAsync("On").block()); + } + + @Test + public void noFiltersButVariants() { + List features = List + .of(new Feature().setId("On").setVariants(createVariants()) + .setConditions(new Conditions().setRequirementType("All")) + .setAllocation(new Allocation().setDefaultWhenEnabled("small")).setEnabled(true)); + + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + + when(context.getBean(Mockito.matches("AlwaysOn"))).thenReturn(new AlwaysOnFilter()) + .thenReturn(new AlwaysOnFilter()); + + assertTrue(featureManager.isEnabledAsync("On").block()); + } + + private List createVariants() { + List variants = new ArrayList<>(); + variants.add(new VariantReference().setName("small").setConfigurationValue(1).setStatusOverride("Enabled")); + variants.add(new VariantReference().setName("large").setConfigurationValue(9).setStatusOverride("Disabled")); + return variants; + } + + class AlwaysOnFilter implements FeatureFilter { + + @Override + public boolean evaluate(FeatureFilterEvaluationContext context) { + return true; + } + + } + + class AlwaysOffFilter implements FeatureFilter { + + @Override + public boolean evaluate(FeatureFilterEvaluationContext context) { + return false; + } + + } +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/recurrence/RecurrenceEvaluatorTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/recurrence/RecurrenceEvaluatorTest.java index 12d9fd6c6308..94cfb06870a7 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/recurrence/RecurrenceEvaluatorTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/recurrence/RecurrenceEvaluatorTest.java @@ -3,18 +3,19 @@ package com.azure.spring.cloud.feature.management.filters.recurrence; -import com.azure.spring.cloud.feature.management.implementation.timewindow.TimeWindowFilterSettings; -import com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence.RecurrenceEvaluator; -import com.azure.spring.cloud.feature.management.implementation.models.Recurrence; -import com.azure.spring.cloud.feature.management.implementation.models.RecurrencePattern; -import com.azure.spring.cloud.feature.management.implementation.models.RecurrenceRange; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; + +import com.azure.spring.cloud.feature.management.implementation.models.Recurrence; +import com.azure.spring.cloud.feature.management.implementation.models.RecurrencePattern; +import com.azure.spring.cloud.feature.management.implementation.models.RecurrenceRange; +import com.azure.spring.cloud.feature.management.implementation.timewindow.TimeWindowFilterSettings; +import com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence.RecurrenceEvaluator; public class RecurrenceEvaluatorTest { diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/recurrence/RecurrenceValidatorTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/recurrence/RecurrenceValidatorTest.java index a4122ca06a63..34301162e3fa 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/recurrence/RecurrenceValidatorTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/recurrence/RecurrenceValidatorTest.java @@ -3,15 +3,8 @@ package com.azure.spring.cloud.feature.management.filters.recurrence; -import com.azure.spring.cloud.feature.management.filters.TimeWindowFilter; -import com.azure.spring.cloud.feature.management.implementation.timewindow.TimeWindowFilterSettings; -import com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence.RecurrenceConstants; -import com.azure.spring.cloud.feature.management.implementation.models.Recurrence; -import com.azure.spring.cloud.feature.management.implementation.models.RecurrencePattern; -import com.azure.spring.cloud.feature.management.implementation.models.RecurrenceRange; -import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext; -import com.azure.spring.cloud.feature.management.models.FilterParameters; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @@ -20,8 +13,16 @@ import java.util.List; import java.util.Map; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +import com.azure.spring.cloud.feature.management.filters.TimeWindowFilter; +import com.azure.spring.cloud.feature.management.implementation.models.Recurrence; +import com.azure.spring.cloud.feature.management.implementation.models.RecurrencePattern; +import com.azure.spring.cloud.feature.management.implementation.models.RecurrenceRange; +import com.azure.spring.cloud.feature.management.implementation.timewindow.TimeWindowFilterSettings; +import com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence.RecurrenceConstants; +import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext; +import com.azure.spring.cloud.feature.management.models.FilterParameters; public class RecurrenceValidatorTest { private final String recurrencePatter = "Recurrence.Pattern"; From c394d7f471fce3773aca9e3bd38c817552c7e418 Mon Sep 17 00:00:00 2001 From: Matthew Metcalf Date: Mon, 31 Mar 2025 14:17:06 -0700 Subject: [PATCH 09/41] Removed feature flags from configuration (#44558) --- ...ationApplicationSettingPropertySource.java | 3 +- ...tionSettingPropertySourceSnapshotTest.java | 2 +- ...nApplicationSettingPropertySourceTest.java | 7 ++- .../config/implementation/TestUtils.java | 51 +++++++++++-------- 4 files changed, 38 insertions(+), 25 deletions(-) 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 7302de70d4a2..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 @@ -127,7 +127,8 @@ private void handleKeyVaultReference(String key, SecretReferenceConfigurationSet 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; } private void handleJson(ConfigurationSetting setting, List keyPrefixTrimValues) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceSnapshotTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceSnapshotTest.java index b9735b0232f3..1bad01307099 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceSnapshotTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceSnapshotTest.java @@ -72,7 +72,7 @@ public class AppConfigurationApplicationSettingPropertySourceSnapshotTest { createItem("/bar/", "test_key_4", "test_value_4", "test_label_4", EMPTY_CONTENT_TYPE); private static final FeatureFlagConfigurationSetting FEATURE_ITEM = createItemFeatureFlag(".appconfig.featureflag/", - "Alpha", FEATURE_VALUE, FEATURE_LABEL, FEATURE_FLAG_CONTENT_TYPE); + "Alpha", FEATURE_VALUE, FEATURE_LABEL, FEATURE_FLAG_CONTENT_TYPE, "fake-etag"); private static final ConfigurationSetting ITEM_NULL = createItem(KEY_FILTER, TEST_KEY_3, TEST_VALUE_3, TEST_LABEL_3, null); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceTest.java index 6aac4f284cb8..af99492615e5 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySourceTest.java @@ -16,6 +16,7 @@ import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_VALUE_2; import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.TEST_VALUE_3; import static com.azure.spring.cloud.appconfiguration.config.implementation.TestUtils.createItem; +import static com.azure.spring.cloud.appconfiguration.config.implementation.TestUtils.createItemFeatureFlag; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.when; @@ -38,6 +39,7 @@ 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.properties.AppConfigurationProperties; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; @@ -67,6 +69,8 @@ public class AppConfigurationApplicationSettingPropertySourceTest { private static final ConfigurationSetting ITEM_INVALID_JSON = createItem(KEY_FILTER, TEST_KEY_3, TEST_VALUE_3, TEST_LABEL_3, JSON_CONTENT_TYPE); + + private static final FeatureFlagConfigurationSetting FEATURE_FLAG = createItemFeatureFlag("Beta", "/0"); private static final ObjectMapper MAPPER = new ObjectMapper(); @@ -104,6 +108,7 @@ public void init() { testItems.add(ITEM_1); testItems.add(ITEM_2); testItems.add(ITEM_3); + testItems.add(FEATURE_FLAG); String[] labelFilter = { "\0" }; @@ -126,7 +131,7 @@ public void testPropCanBeInitAndQueried() throws IOException { propertySource.initProperties(null, contextMock); String[] keyNames = propertySource.getPropertyNames(); - String[] expectedKeyNames = testItems.stream() + String[] expectedKeyNames = testItems.stream().filter(config -> !(config instanceof FeatureFlagConfigurationSetting)) .map(t -> t.getKey().substring(KEY_FILTER.length())).toArray(String[]::new); assertThat(keyNames).containsExactlyInAnyOrder(expectedKeyNames); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/TestUtils.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/TestUtils.java index 78b9bd6bd6ba..0f36bc5b6294 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/TestUtils.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/TestUtils.java @@ -2,6 +2,8 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation; +import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_CONTENT_TYPE; + import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -47,42 +49,47 @@ static ConfigurationSetting createItem(String keyFilter, String key, String valu return item; } - static FeatureFlagConfigurationSetting createItemFeatureFlag(String prefix, String key, String value, String label, - String contentType) { - return createItemFeatureFlag(prefix, key, value, label, contentType, null); + static FeatureFlagConfigurationSetting createItemFeatureFlag(String key, String label) { + return createItemFeatureFlag(".appconfig.featureflag/", key, null, label, FEATURE_FLAG_CONTENT_TYPE, null); } static FeatureFlagConfigurationSetting createItemFeatureFlag(String prefix, String key, String value, String label, String contentType, String eTag) { FeatureFlagConfigurationSetting item = new FeatureFlagConfigurationSetting(key, true); - item.setValue(value); + if (value != null) { + item.setValue(value); + } item.setClientFilters(new ArrayList<>()); - item.setKey(prefix + key); + if (prefix != null) { + item.setKey(prefix + key); + } item.setLabel(label); item.setContentType(contentType); item.setETag(eTag); try { - JsonNode node = MAPPER.readTree(value).get("conditions").get("client_filters"); - - for (int i = 0; i < node.size(); i++) { - JsonNode nodeFilter = node.get(i); - FeatureFlagFilter filter = new FeatureFlagFilter(nodeFilter.get("Name").asText()); - - JsonNode nodeParams = nodeFilter.get("Parameters"); - if (nodeParams != null) { - for (int j = 0; j < nodeParams.size(); j++) { - // JsonNode param = nodeParams. - Map result = MAPPER.convertValue(nodeParams, - new TypeReference>() { - }); - Set parameters = result.keySet(); - for (String paramKey : parameters) { - filter.addParameter(paramKey, result.get(paramKey)); + if (value != null) { + JsonNode node = MAPPER.readTree(value).get("conditions").get("client_filters"); + + for (int i = 0; i < node.size(); i++) { + JsonNode nodeFilter = node.get(i); + FeatureFlagFilter filter = new FeatureFlagFilter(nodeFilter.get("Name").asText()); + + JsonNode nodeParams = nodeFilter.get("Parameters"); + if (nodeParams != null) { + for (int j = 0; j < nodeParams.size(); j++) { + // JsonNode param = nodeParams. + Map result = MAPPER.convertValue(nodeParams, + new TypeReference>() { + }); + Set parameters = result.keySet(); + for (String paramKey : parameters) { + filter.addParameter(paramKey, result.get(paramKey)); + } } } + item.addClientFilter(filter); } - item.addClientFilter(filter); } } catch (JsonProcessingException e) { LOGGER.log(LogLevel.VERBOSE, () -> "Failed to create FeatureFlagConfigurationSetting.", e); From 6ac0c5e9c520d4ce83b5332ede449d9b5bb7de68 Mon Sep 17 00:00:00 2001 From: Matthew Metcalf Date: Tue, 22 Apr 2025 10:05:58 -0700 Subject: [PATCH 10/41] Feature management Snapshot (#44621) * Feature Snapshot support * Removing dead code * Missing copyright * Adding back Feature Filter telemetry * Adding tests and simplifying code * Update URI usage * Update AzureAppConfigDataLoader.java --- ...AppConfigurationReplicaClientsBuilder.java | 10 +- .../AzureAppConfigDataLoader.java | 17 ++- .../implementation/BackoffTimeCalculator.java | 10 -- .../implementation/FeatureFlagClient.java | 22 +++ .../config/implementation/StateHolder.java | 24 +--- .../feature/FeatureFlagState.java | 29 ---- .../policy/BaseAppConfigurationPolicy.java | 3 +- .../http/policy/FeatureFlagTracing.java | 6 +- .../http/policy/TracingInfo.java | 18 +-- ...ConfigurationReplicaClientBuilderTest.java | 2 +- .../BackoffTimeCalculatorTest.java | 1 - .../implementation/StateHolderTest.java | 1 - .../BaseAppConfigurationPolicyTest.java | 10 +- .../http/policy/TracingInfoTest.java | 46 ++++--- .../web/FeatureManagerSnapshot.java | 109 ++++++++++++++- .../web/FeatureManagerSnapshotTest.java | 125 +++++++++++++++++- .../feature/management/FeatureManager.java | 7 +- ...etingContextAccessorTestConfiguration.java | 2 + .../FeatureManagementPropertiesTest.java | 2 + 19 files changed, 320 insertions(+), 124 deletions(-) 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 b9868060bc26..40fdf1e5340a 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; @@ -307,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()); @@ -329,9 +333,11 @@ private static class ConnectionString { 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/AzureAppConfigDataLoader.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java index 96abbe9ff923..9386a2bab524 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java @@ -3,6 +3,7 @@ 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; @@ -40,7 +41,7 @@ public class AzureAppConfigDataLoader implements ConfigDataLoader this.featureFlagClient)); + InstanceSupplier.from(() -> featureFlagClient)); } + // Reset telemetry usage for refresh + featureFlagClient.resetTelemetry(); List> sourceList = new ArrayList<>(); @@ -80,8 +83,10 @@ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResour 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()))) { + 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, 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/FeatureFlagClient.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClient.java index b227d70b4747..82e759e1f73e 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 @@ -29,10 +29,12 @@ 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; @@ -51,6 +53,8 @@ class FeatureFlagClient { private static final ObjectMapper CASE_INSENSITIVE_MAPPER = JsonMapper.builder() .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build(); + + private FeatureFlagTracing tracing = new FeatureFlagTracing(); /** *

@@ -79,6 +83,7 @@ List loadFeatureFlags(AppConfigurationReplicaClient replicaClient, for (String label : labels) { SettingSelector settingSelector = new SettingSelector().setKeyFilter(keyFilter).setLabelFilter(label); + context.addData("FeatureFlagTracing", tracing); FeatureFlags features = replicaClient.listFeatureFlags(settingSelector, context); loadedFeatureFlags.addAll(proccessFeatureFlags(features, keyFilter)); @@ -95,6 +100,7 @@ List proccessFeatureFlags(FeatureFlags features, String endpoint) if (setting instanceof FeatureFlagConfigurationSetting && FEATURE_FLAG_CONTENT_TYPE.equals(setting.getContentType())) { FeatureFlagConfigurationSetting featureFlag = (FeatureFlagConfigurationSetting) setting; + updateTelemetry(featureFlag); properties.put(featureFlag.getKey(), createFeature(featureFlag, endpoint)); } } @@ -170,5 +176,21 @@ private static String calculateFeatureFlagId(String key, String label) { public List getFeatureFlags() { return properties.values().stream().toList(); } + + public void resetTelemetry() { + tracing.resetFeatureFilterTelemetry(); + } + + /** + * Looks at each filter used in a Feature Flag to check what types it is using. + * + * @param featureFlag FeatureFlagConfigurationSetting + * @param tracing The TracingInfo for this store. + */ + private void updateTelemetry(FeatureFlagConfigurationSetting featureFlag) { + for (FeatureFlagFilter filter : featureFlag.getClientFilters()) { + tracing.updateFeatureFilterTelemetry(filter.getName()); + } + } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/StateHolder.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/StateHolder.java index 817ee27d1c6c..8d63ce85ac92 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/StateHolder.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/StateHolder.java @@ -21,7 +21,7 @@ final class StateHolder { private static StateHolder currentState; private final Map state = new ConcurrentHashMap<>(); - + private final Map featureFlagState = new ConcurrentHashMap<>(); private final Map loadState = new ConcurrentHashMap<>(); @@ -53,7 +53,7 @@ static State getState(String originEndpoint) { private Map getFullState() { return state; } - + private Map getFullFeatureFlagState() { return featureFlagState; } @@ -86,23 +86,15 @@ void setState(String originEndpoint, List watchKeys, Durat */ void setStateFeatureFlag(String originEndpoint, List watchKeys, Duration duration) { - featureFlagState.put(originEndpoint, new FeatureFlagState(watchKeys, Math.toIntExact(duration.getSeconds()), originEndpoint)); - } - - /** - * @param state previous state to base off - * @param duration nextRefreshPeriod - */ - void setState(State state, Duration duration) { - this.state.put(state.getOriginEndpoint(), - new State(state, Instant.now().plusSeconds(Math.toIntExact(duration.getSeconds())))); + featureFlagState.put(originEndpoint, + new FeatureFlagState(watchKeys, Math.toIntExact(duration.getSeconds()), originEndpoint)); } void updateStateRefresh(State state, Duration duration) { this.state.put(state.getOriginEndpoint(), new State(state, Instant.now().plusSeconds(Math.toIntExact(duration.getSeconds())))); } - + void updateFeatureFlagStateRefresh(FeatureFlagState state, Duration duration) { this.featureFlagState.put(state.getOriginEndpoint(), new FeatureFlagState(state, Instant.now().plusSeconds(Math.toIntExact(duration.getSeconds())))); @@ -125,10 +117,6 @@ static boolean getLoadState(String originEndpoint) { return currentState.getFullLoadState().getOrDefault(originEndpoint, false); } - Map getLoadState() { - return loadState; - } - /** * @param originEndpoint the configuration store connected to. * @param loaded true if the configuration store was loaded. @@ -158,7 +146,7 @@ public void setNextForcedRefresh(Duration refreshPeriod) { * Sets a minimum value until the next refresh. If a refresh interval has passed or is smaller than the calculated * backoff time, the refresh interval is set to the backoff time. * @param refreshInterval period between refresh checks. - * @param defaultMinBackoff min backoff between checks + * @param defaultMinBackoff min backoff between checks */ void updateNextRefreshTime(Duration refreshInterval, Long defaultMinBackoff) { if (refreshInterval != null) { diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/FeatureFlagState.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/FeatureFlagState.java index e63cfb80be7f..ddca37b32140 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/FeatureFlagState.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/FeatureFlagState.java @@ -13,24 +13,16 @@ public class FeatureFlagState { private final String originEndpoint; - private Integer refreshAttempt; - - private final int refreshInterval; - public FeatureFlagState(List watchKeys, int refreshInterval, String originEndpoint) { this.watchKeys = watchKeys; - this.refreshInterval = refreshInterval; nextRefreshCheck = Instant.now().plusSeconds(refreshInterval); this.originEndpoint = originEndpoint; - this.refreshAttempt = 1; } public FeatureFlagState(FeatureFlagState oldState, Instant newRefresh) { this.watchKeys = oldState.getWatchKeys(); - this.refreshInterval = oldState.getRefreshInterval(); this.nextRefreshCheck = newRefresh; this.originEndpoint = oldState.getOriginEndpoint(); - this.refreshAttempt = oldState.getRefreshAttempt(); } /** @@ -54,25 +46,4 @@ public String getOriginEndpoint() { return originEndpoint; } - /** - * @return the refreshAttempt - */ - public Integer getRefreshAttempt() { - return refreshAttempt; - } - - /** - * Adds 1 to the number of refresh attempts - */ - public void incrementRefreshAttempt() { - this.refreshAttempt += 1; - } - - /** - * @return the refreshInterval - */ - public int getRefreshInterval() { - return refreshInterval; - } - } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/BaseAppConfigurationPolicy.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/BaseAppConfigurationPolicy.java index e46a413fac23..9f9ae1d174d1 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/BaseAppConfigurationPolicy.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/BaseAppConfigurationPolicy.java @@ -46,11 +46,12 @@ public BaseAppConfigurationPolicy(TracingInfo tracingInfo) { public Mono process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) { Boolean watchRequests = (Boolean) context.getData("refresh").orElse(false); Boolean pushRefresh = (Boolean) context.getData(PUSH_REFRESH).orElse(false); + FeatureFlagTracing ffTracing = (FeatureFlagTracing) context.getData("FeatureFlagTracing").orElse(null); HttpHeaders headers = context.getHttpRequest().getHeaders(); String sdkUserAgent = headers.get(HttpHeaderName.USER_AGENT).getValue(); headers.set(HttpHeaderName.USER_AGENT, USER_AGENT + " " + sdkUserAgent); headers.set(HttpHeaderName.fromString(AppConfigurationConstants.CORRELATION_CONTEXT), - tracingInfo.getValue(watchRequests, pushRefresh)); + tracingInfo.getValue(watchRequests, pushRefresh, ffTracing)); return next.process(); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/FeatureFlagTracing.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/FeatureFlagTracing.java index 24b62193cc55..fd35c83c1387 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/FeatureFlagTracing.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/FeatureFlagTracing.java @@ -5,7 +5,7 @@ import java.util.Arrays; import java.util.List; -class FeatureFlagTracing { +public class FeatureFlagTracing { private static final String CUSTOM_FILTER = "CSTM"; @@ -38,14 +38,14 @@ boolean usesAnyFilter() { return usesCustomFilter || usesPercentageFilter || usesTimeWindowFilter || usesTargetingFilter; } - void resetFeatureFilterTelemetry() { + public void resetFeatureFilterTelemetry() { usesCustomFilter = false; usesPercentageFilter = false; usesTimeWindowFilter = false; usesTargetingFilter = false; } - void updateFeatureFilterTelemetry(String filterName) { + public void updateFeatureFilterTelemetry(String filterName) { if (PERCENTAGE_FILTER_NAMES.stream().anyMatch(name -> name.equalsIgnoreCase(filterName))) { usesPercentageFilter = true; } else if (TIME_WINDOW_FILTER_NAMES.stream().anyMatch(name -> name.equalsIgnoreCase(filterName))) { diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfo.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfo.java index 22f1b74118ff..b1b7f4479e7b 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfo.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfo.java @@ -18,7 +18,7 @@ public class TracingInfo { private int replicaCount; - private final FeatureFlagTracing featureFlagTracing; + private FeatureFlagTracing featureFlagTracing; private final Configuration configuration; @@ -29,7 +29,10 @@ public TracingInfo(boolean isKeyVaultConfigured, int replicaCount, Configuration this.configuration = configuration; } - String getValue(boolean watchRequests, boolean pushRefresh) { + String getValue(boolean watchRequests, boolean pushRefresh, FeatureFlagTracing featureFlagTracing) { + if (featureFlagTracing != null) { + this.featureFlagTracing = featureFlagTracing; + } String track = configuration.get(RequestTracingConstants.REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE.toString()); if (track != null && Boolean.valueOf(track)) { return ""; @@ -40,8 +43,8 @@ String getValue(boolean watchRequests, boolean pushRefresh) { sb.append(RequestTracingConstants.REQUEST_TYPE_KEY).append("=" + requestTypeValue); - if (featureFlagTracing != null && featureFlagTracing.usesAnyFilter()) { - sb.append(",Filter=").append(featureFlagTracing.toString()); + if (this.featureFlagTracing.usesAnyFilter()) { + sb.append(",Filter=").append(this.featureFlagTracing.toString()); } String hostType = getHostType(); @@ -97,11 +100,4 @@ private static StringBuilder getFeatureManagementUsage(StringBuilder sb) { return sb; } - /** - * @return the featureFlagTracing - */ - public FeatureFlagTracing getFeatureFlagTracing() { - return featureFlagTracing; - } - } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientBuilderTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientBuilderTest.java index 23f20ad69b0b..10e10c33787b 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientBuilderTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientBuilderTest.java @@ -269,7 +269,7 @@ public void buildClientConnectionStringInvalidTest() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> spy.buildClient("fake.test.config.io", configStore)); - assertEquals("java.net.MalformedURLException: no protocol: fake.test.config.io", exception.getMessage()); + assertEquals("URI is not absolute", exception.getMessage()); } @Test diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/BackoffTimeCalculatorTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/BackoffTimeCalculatorTest.java index a91d013e63b1..eb633cba1cd3 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/BackoffTimeCalculatorTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/BackoffTimeCalculatorTest.java @@ -16,7 +16,6 @@ public class BackoffTimeCalculatorTest { public void testCalculate() { int testTime = 10; - BackoffTimeCalculator.setDefaults((long) 600, (long) 30); Long testDate = BackoffTimeCalculator.calculateBackoff(1); assertNotNull(testDate); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/StateHolderTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/StateHolderTest.java index 3b10d736f8ec..683de01f730b 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/StateHolderTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/StateHolderTest.java @@ -182,7 +182,6 @@ private void loadStateTest(TestInfo testInfo) { StateHolder testStateHolder = new StateHolder(); testStateHolder.setLoadState(endpoint, true); StateHolder.updateState(testStateHolder); - assertEquals(testStateHolder.getLoadState().get(endpoint), StateHolder.getLoadState(endpoint)); assertEquals(testStateHolder, StateHolder.getCurrentState()); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/BaseAppConfigurationPolicyTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/BaseAppConfigurationPolicyTest.java index d3bfdea27cef..e449fd6357fa 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/BaseAppConfigurationPolicyTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/BaseAppConfigurationPolicyTest.java @@ -7,6 +7,8 @@ import static org.mockito.Mockito.when; import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.util.Optional; @@ -55,8 +57,8 @@ public void cleanup() throws Exception { } @Test - public void startupThenWatchUpdateTest() throws MalformedURLException { - URL url = new URL("https://www.test.url/kv"); + public void startupThenWatchUpdateTest() throws MalformedURLException, URISyntaxException { + URL url = new URI("https://www.test.url/kv").toURL(); HttpRequest request = new HttpRequest(HttpMethod.GET, url); request.setHeader(HttpHeaderName.USER_AGENT, "PreExistingUserAgent"); BaseAppConfigurationPolicy policy = new BaseAppConfigurationPolicy( @@ -104,11 +106,11 @@ public void startupThenWatchUpdateTest() throws MalformedURLException { } @Test - public void keyVaultIsConfigured() throws MalformedURLException { + public void keyVaultIsConfigured() throws MalformedURLException, URISyntaxException { BaseAppConfigurationPolicy policy = new BaseAppConfigurationPolicy( new TracingInfo(true, 0, Configuration.getGlobalConfiguration())); - URL url = new URL("https://www.test.url/kv"); + URL url = new URI("https://www.test.url/kv").toURL(); HttpRequest request = new HttpRequest(HttpMethod.GET, url); request.setHeader(HttpHeaderName.USER_AGENT, "PreExistingUserAgent"); when(contextMock.getHttpRequest()).thenReturn(request); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfoTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfoTest.java index f93ffa0c63e6..4ad16ca71d67 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfoTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/http/policy/TracingInfoTest.java @@ -24,41 +24,42 @@ public void getValueTest() { Configuration configuration = getConfiguration("false"); TracingInfo tracingInfo = new TracingInfo(false, 0, configuration); - assertEquals("RequestType=Startup", tracingInfo.getValue(false, false)); - assertEquals("RequestType=Watch", tracingInfo.getValue(true, false)); + assertEquals("RequestType=Startup", tracingInfo.getValue(false, false, null)); + assertEquals("RequestType=Watch", tracingInfo.getValue(true, false, null)); tracingInfo = new TracingInfo(true, 0, configuration); - assertEquals("RequestType=Startup,UsesKeyVault", tracingInfo.getValue(false, false)); + assertEquals("RequestType=Startup,UsesKeyVault", tracingInfo.getValue(false, false, null)); tracingInfo = new TracingInfo(false, 1, configuration); - assertEquals("RequestType=Startup,ReplicaCount=1", tracingInfo.getValue(false, false)); + assertEquals("RequestType=Startup,ReplicaCount=1", tracingInfo.getValue(false, false, null)); - tracingInfo = new TracingInfo(false, 0, configuration); - - tracingInfo.getFeatureFlagTracing().updateFeatureFilterTelemetry("Random"); - assertEquals("RequestType=Startup,Filter=CSTM", tracingInfo.getValue(false, false)); - tracingInfo = new TracingInfo(true, 0, configuration); - assertEquals("RequestType=Startup,UsesKeyVault,PushRefresh", tracingInfo.getValue(false, true)); - + + FeatureFlagTracing ffTracing = new FeatureFlagTracing(); + + ffTracing.updateFeatureFilterTelemetry("Random"); + assertEquals("RequestType=Startup,Filter=CSTM,UsesKeyVault", tracingInfo.getValue(false, false, ffTracing)); + + assertEquals("RequestType=Startup,Filter=CSTM,UsesKeyVault,PushRefresh", tracingInfo.getValue(false, true, null)); + } @Test public void disableTracingTest() { TracingInfo tracingInfo = new TracingInfo(false, 0, getConfiguration(null)); - assertNotEquals("", tracingInfo.getValue(false, false)); - + assertNotEquals("", tracingInfo.getValue(false, false, null)); + tracingInfo = new TracingInfo(false, 0, getConfiguration("")); - assertNotEquals("", tracingInfo.getValue(false, false)); - + assertNotEquals("", tracingInfo.getValue(false, false, null)); + tracingInfo = new TracingInfo(false, 0, getConfiguration("true")); - assertEquals("", tracingInfo.getValue(false, false)); - + assertEquals("", tracingInfo.getValue(false, false, null)); + tracingInfo = new TracingInfo(false, 0, getConfiguration("false")); - assertNotEquals("", tracingInfo.getValue(false, false)); - + assertNotEquals("", tracingInfo.getValue(false, false, null)); + tracingInfo = new TracingInfo(false, 0, getConfiguration("random string")); - assertNotEquals("", tracingInfo.getValue(false, false)); + assertNotEquals("", tracingInfo.getValue(false, false, null)); } private static final ConfigurationSource EMPTY_SOURCE = new ConfigurationSource() { @@ -69,9 +70,10 @@ public Map getProperties(String source) { }; private Configuration getConfiguration(String value) { - return new ConfigurationBuilder(EMPTY_SOURCE, EMPTY_SOURCE, new TestConfigurationSource().put(RequestTracingConstants.REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE.toString(), value)).build(); + return new ConfigurationBuilder(EMPTY_SOURCE, EMPTY_SOURCE, new TestConfigurationSource() + .put(RequestTracingConstants.REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE.toString(), value)).build(); } - + private final class TestConfigurationSource implements ConfigurationSource { private final Map testData; diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/management/web/FeatureManagerSnapshot.java b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/management/web/FeatureManagerSnapshot.java index f908b76c4041..6d3da2a2e20f 100644 --- a/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/management/web/FeatureManagerSnapshot.java +++ b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/management/web/FeatureManagerSnapshot.java @@ -2,9 +2,12 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.management.web; +import java.time.Duration; import java.util.HashMap; +import java.util.Map; import com.azure.spring.cloud.feature.management.FeatureManager; +import com.azure.spring.cloud.feature.management.models.Variant; import reactor.core.publisher.Mono; @@ -16,7 +19,11 @@ public class FeatureManagerSnapshot { private final FeatureManager featureManager; - private final HashMap requestMap; + private final Map requestMap; + + private final Map variantMap; + + private static final Duration DEFAULT_BLOCK_TIMEOUT = Duration.ofSeconds(100); /** * Used to evaluate whether a feature is enabled or disabled. When setup with the @RequestScope it will @@ -26,6 +33,7 @@ public class FeatureManagerSnapshot { FeatureManagerSnapshot(FeatureManager featureManager) { this.featureManager = featureManager; this.requestMap = new HashMap<>(); + this.variantMap = new HashMap<>(); } /** @@ -40,12 +48,29 @@ public class FeatureManagerSnapshot { * @return state of the feature */ public Mono isEnabledAsync(String feature) { + return isEnabledAsync(feature, null); + } + + /** + * Checks to see if the feature is enabled. If enabled it checks each filter, once a single filter returns true it + * returns true. If no filter returns true, it returns false. If there are no filters, it returns true. If feature + * isn't found it returns false. + *

+ * If isEnabled has already been called on this feature in this request, it will return the same value as it did + * before. + * + * @param feature Feature being checked. + * @param featureContext Local context + * @return state of the feature + */ + public Mono isEnabledAsync(String feature, Object featureContext) { Boolean featureValue = requestMap.get(feature); if (featureValue != null) { return Mono.just(featureValue); } - return featureManager.isEnabledAsync(feature).doOnSuccess((enabled) -> requestMap.put(feature, enabled)); + return featureManager.isEnabledAsync(feature, featureContext) + .doOnSuccess((enabled) -> requestMap.put(feature, enabled)); } /** @@ -60,12 +85,82 @@ public Mono isEnabledAsync(String feature) { * @return state of the feature */ public Boolean isEnabled(String feature) { - Boolean featureValue = requestMap.get(feature); - if (featureValue != null) { - return featureValue; + return isEnabled(feature, null); + } + + /** + * Checks to see if the feature is enabled. If enabled it checks each filter, once a single filter returns true it + * returns true. If no filter returns true, it returns false. If there are no filters, it returns true. If feature + * isn't found it returns false. + *

+ * If isEnabled has already been called on this feature in this request, it will return the same value as it did + * before. + * + * @param feature Feature being checked. + * @param featureContext Local context + * @return state of the feature + */ + public Boolean isEnabled(String feature, Object featureContext) { + return isEnabledAsync(feature, featureContext).block(DEFAULT_BLOCK_TIMEOUT); + } + + /** + * Returns the variant assigned to the current context. + *

+ * If getVariantAsync has already been called on this feature in this request, it will return the same value as it + * did before. + * + * @param feature Feature being checked. + * @return state of the feature + */ + public Mono getVariantAsync(String feature) { + return getVariantAsync(feature, null); + } + + /** + * Returns the variant assigned to the current context. + *

+ * If getVariantAsync has already been called on this feature in this request, it will return the same value as it + * did before. + * + * @param feature Feature being checked. + * @param featureContext Local context + * @return state of the feature + */ + public Mono getVariantAsync(String feature, Object featureContext) { + Variant featureVariant = variantMap.get(feature); + if (featureVariant != null) { + return Mono.just(featureVariant); } - return featureManager.isEnabledAsync(feature).doOnSuccess((enabled) -> requestMap.put(feature, enabled)) - .block(); + return featureManager.getVariantAsync(feature, featureContext) + .doOnSuccess((variant) -> variantMap.put(feature, variant)); + } + + /** + * Returns the variant assigned to the current context. + *

+ * If getVariant has already been called on this feature in this request, it will return the same value as it did + * before. + * + * @param feature Feature being checked. + * @return state of the feature + */ + public Variant getVariant(String feature) { + return getVariant(feature, null); + } + + /** + * Returns the variant assigned to the current context. + *

+ * If getVariant has already been called on this feature in this request, it will return the same value as it did + * before. + * + * @param feature Feature being checked. + * @param featureContext Local context + * @return state of the feature + */ + public Variant getVariant(String feature, Object featureContext) { + return getVariantAsync(feature, featureContext).block(DEFAULT_BLOCK_TIMEOUT); } } diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/src/test/java/com/azure/spring/cloud/feature/management/web/FeatureManagerSnapshotTest.java b/sdk/spring/spring-cloud-azure-feature-management-web/src/test/java/com/azure/spring/cloud/feature/management/web/FeatureManagerSnapshotTest.java index 8e9dbd7a0af5..f8c207eb8784 100644 --- a/sdk/spring/spring-cloud-azure-feature-management-web/src/test/java/com/azure/spring/cloud/feature/management/web/FeatureManagerSnapshotTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management-web/src/test/java/com/azure/spring/cloud/feature/management/web/FeatureManagerSnapshotTest.java @@ -2,7 +2,9 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.management.web; - +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -19,6 +21,7 @@ import org.mockito.MockitoAnnotations; import com.azure.spring.cloud.feature.management.FeatureManager; +import com.azure.spring.cloud.feature.management.models.Variant; import jakarta.servlet.http.HttpServletRequest; import reactor.core.publisher.Mono; @@ -41,22 +44,132 @@ public void setup(TestInfo testInfo) { @Test public void setAttribute() throws InterruptedException, ExecutionException { - when(featureManager.isEnabledAsync(Mockito.matches("setAttribute"))).thenReturn(Mono.just(true)); + when(featureManager.isEnabledAsync(Mockito.matches("setAttribute"), Mockito.isNull())) + .thenReturn(Mono.just(true)); assertTrue(featureManagerSnapshot.isEnabledAsync("setAttribute").block()); - verify(featureManager, times(1)).isEnabledAsync("setAttribute"); + verify(featureManager, times(1)).isEnabledAsync("setAttribute", null); } @Test public void setSavedValue() throws InterruptedException, ExecutionException { - when(featureManager.isEnabledAsync(Mockito.matches("setAttribute"))).thenReturn(Mono.just(true)); + when(featureManager.isEnabledAsync(Mockito.matches("setAttribute"), Mockito.isNull())) + .thenReturn(Mono.just(true)); assertTrue(featureManagerSnapshot.isEnabledAsync("setAttribute").block()); - verify(featureManager, times(1)).isEnabledAsync("setAttribute"); + verify(featureManager, times(1)).isEnabledAsync("setAttribute", null); // The second time should return the same value, but not increase the non-snapshot count. assertTrue(featureManagerSnapshot.isEnabledAsync("setAttribute").block()); - verify(featureManager, times(1)).isEnabledAsync("setAttribute"); + verify(featureManager, times(1)).isEnabledAsync("setAttribute", null); + } + + @Test + public void featureDisabled() throws InterruptedException, ExecutionException { + when(featureManager.isEnabledAsync(Mockito.matches("featureDisabled"), Mockito.isNull())) + .thenReturn(Mono.just(false)); + + assertFalse(featureManagerSnapshot.isEnabledAsync("featureDisabled").block()); + verify(featureManager, times(1)).isEnabledAsync("featureDisabled", null); + } + + @Test + public void featureEnabledWithException() throws InterruptedException, ExecutionException { + when(featureManager.isEnabledAsync(Mockito.matches("featureEnabledWithException"), Mockito.isNull())) + .thenReturn(Mono.error(new RuntimeException("Test Exception"))); + + assertThrows(RuntimeException.class, + () -> featureManagerSnapshot.isEnabledAsync("featureEnabledWithException").block()); + verify(featureManager, times(1)).isEnabledAsync("featureEnabledWithException", null); + } + + @Test + public void featureEnabledMultipleTimes() throws InterruptedException, ExecutionException { + when(featureManager.isEnabledAsync(Mockito.matches("featureEnabledMultipleTimes"), Mockito.isNull())) + .thenReturn(Mono.just(true)); + + assertTrue(featureManagerSnapshot.isEnabledAsync("featureEnabledMultipleTimes").block()); + verify(featureManager, times(1)).isEnabledAsync("featureEnabledMultipleTimes", null); + + assertTrue(featureManagerSnapshot.isEnabledAsync("featureEnabledMultipleTimes").block()); + verify(featureManager, times(1)).isEnabledAsync("featureEnabledMultipleTimes", null); + + assertTrue(featureManagerSnapshot.isEnabledAsync("featureEnabledMultipleTimes", null).block()); + } + + @Test + public void featureDisabledMultipleTimes() throws InterruptedException, ExecutionException { + when(featureManager.isEnabledAsync(Mockito.matches("featureDisabledMultipleTimes"), Mockito.isNull())) + .thenReturn(Mono.just(false)); + when(featureManager.isEnabledAsync(Mockito.matches("featureDisabledMultipleTimes"), + Mockito.matches("my Context"))) + .thenReturn(Mono.just(false)); + + assertFalse(featureManagerSnapshot.isEnabled("featureDisabledMultipleTimes")); + verify(featureManager, times(1)).isEnabledAsync("featureDisabledMultipleTimes", null); + + assertFalse(featureManagerSnapshot.isEnabled("featureDisabledMultipleTimes")); + verify(featureManager, times(1)).isEnabledAsync("featureDisabledMultipleTimes", null); + + assertFalse(featureManagerSnapshot.isEnabled("featureDisabledMultipleTimes", "my Context")); + verify(featureManager, times(1)).isEnabledAsync("featureDisabledMultipleTimes", null); + } + + @Test + public void getVariantAsync() throws InterruptedException, ExecutionException { + Variant variant = new Variant("variant1", true); + when(featureManager.getVariantAsync(Mockito.matches("featureWithVariant"), Mockito.matches("my Context"))) + .thenReturn(Mono.just(variant)); + + assertEquals(variant, featureManagerSnapshot.getVariantAsync("featureWithVariant", "my Context").block()); + verify(featureManager, times(1)).getVariantAsync("featureWithVariant", "my Context"); + } + + @Test + public void getVariantAsyncWithException() throws InterruptedException, ExecutionException { + when(featureManager.getVariantAsync(Mockito.matches("featureWithVariantException"), Mockito.isNull())) + .thenReturn(Mono.error(new RuntimeException("Test Exception"))); + + assertThrows(RuntimeException.class, + () -> featureManagerSnapshot.getVariantAsync("featureWithVariantException").block()); + verify(featureManager, times(1)).getVariantAsync("featureWithVariantException", null); + } + + @Test + public void getVariant() { + Variant variant = new Variant("variant1", true); + when(featureManager.getVariantAsync(Mockito.matches("featureWithVariant"), Mockito.isNull())) + .thenReturn(Mono.just(variant)); + + assertEquals(variant, featureManagerSnapshot.getVariant("featureWithVariant")); + verify(featureManager, times(1)).getVariantAsync("featureWithVariant", null); + } + + @Test + public void getVariantWithException() { + when(featureManager.getVariant(Mockito.matches("featureWithVariantException"), Mockito.isNull())) + .thenThrow(new RuntimeException("Test Exception")); + + assertThrows(RuntimeException.class, () -> featureManagerSnapshot.getVariant("featureWithVariantException")); + verify(featureManager, times(1)).getVariantAsync("featureWithVariantException", null); + } + + @Test + public void getVariantMultipleTimes() { + Variant variant = new Variant("variant1", true); + when(featureManager.getVariantAsync(Mockito.matches("featureWithVariantMultipleTimes"), Mockito.isNull())) + .thenReturn(Mono.just(variant)); + when(featureManager.getVariantAsync(Mockito.matches("featureWithVariantMultipleTimes"), + Mockito.matches("my Context"))).thenReturn(Mono.just(variant)); + + assertEquals(variant, featureManagerSnapshot.getVariant("featureWithVariantMultipleTimes")); + verify(featureManager, times(1)).getVariantAsync("featureWithVariantMultipleTimes", null); + + assertEquals(variant, featureManagerSnapshot.getVariant("featureWithVariantMultipleTimes")); + verify(featureManager, times(1)).getVariantAsync("featureWithVariantMultipleTimes", null); + + assertEquals(variant, featureManagerSnapshot.getVariant("featureWithVariantMultipleTimes", "my Context")); + verify(featureManager, times(1)).getVariantAsync("featureWithVariantMultipleTimes", null); } } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java index 800bb014e2a3..d30e844c39eb 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java @@ -46,7 +46,7 @@ import reactor.core.publisher.Mono; /** - * Holds information on Feature Management properties and can check if a given feature is enabled. + * Used to evaluate the enabled state of a feature and/or get the assigned variant of a feature, if any. */ public class FeatureManager { @@ -65,11 +65,13 @@ public class FeatureManager { private final TargetingEvaluationOptions evaluationOptions; /** - * Can be called to check if a feature is enabled or disabled. + * Used to evaluate the enabled state of a feature and/or get the assigned variant of a feature, if any. * * @param context ApplicationContext * @param featureManagementConfigurations Configuration Properties for Feature Flags * @param properties FeatureManagementConfigProperties + * @param contextAccessor TargetingContextAccessor + * @param evaluationOptions TargetingE */ FeatureManager(ApplicationContext context, FeatureManagementProperties featureManagementConfigurations, FeatureManagementConfigProperties properties, TargetingContextAccessor contextAccessor, @@ -319,6 +321,7 @@ private void assignVariant(EvaluationEvent event) { if (variantName == null) { this.assignDefaultEnabledVariant(event); + return; } event.setVariant(variantNameToVariant(featureFlag, variantName)); diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/TargetingContextAccessorTestConfiguration.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/TargetingContextAccessorTestConfiguration.java index 882abb74c509..1b61d3453805 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/TargetingContextAccessorTestConfiguration.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/TargetingContextAccessorTestConfiguration.java @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. package com.azure.spring.cloud.feature.management; import org.springframework.boot.context.properties.EnableConfigurationProperties; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementPropertiesTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementPropertiesTest.java index 6e23c9168338..9077a8968abb 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementPropertiesTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementPropertiesTest.java @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. package com.azure.spring.cloud.feature.management.implementation; import static org.junit.jupiter.api.Assertions.assertEquals; From d59dafa568404c7b562dcf26219a8ee346cb2299 Mon Sep 17 00:00:00 2001 From: Matthew Metcalf Date: Thu, 1 May 2025 13:06:00 -0700 Subject: [PATCH 11/41] Adding missing java docs (#45169) --- .../feature/management/models/Allocation.java | 66 ++++++++--- .../feature/management/models/Conditions.java | 51 ++++++-- .../management/models/EvaluationEvent.java | 88 +++++++++++--- .../feature/management/models/Feature.java | 110 +++++++++++++++--- .../management/models/GroupAllocation.java | 41 ++++++- .../models/PercentileAllocation.java | 80 ++++++++++--- .../management/models/UserAllocation.java | 41 ++++++- .../feature/management/models/Variant.java | 37 ++++-- .../models/VariantAssignmentReason.java | 43 +++++-- .../management/models/VariantReference.java | 51 ++++++-- 10 files changed, 496 insertions(+), 112 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Allocation.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Allocation.java index 6d428fe38682..ca2add71313b 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Allocation.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Allocation.java @@ -14,6 +14,12 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class Allocation { + /** + * Creates a new instance of the Allocation class. + */ + public Allocation() { + } + @JsonProperty("default_when_enabled") private String defaultWhenEnabled; @@ -29,15 +35,19 @@ public class Allocation { private String seed; /** - * @return the defaultWhenEnabled + * Gets the variant to use when the feature flag is enabled and no specific allocation matches. + * + * @return the default variant when the feature flag is enabled */ public String getDefaultWhenEnabled() { return defaultWhenEnabled; } /** - * @param defaultWhenEnabled the defaultWhenEnabled to set - * @return Allocation + * Sets the variant to use when the feature flag is enabled and no specific allocation matches. + * + * @param defaultWhenEnabled the default variant when enabled + * @return the updated Allocation object */ public Allocation setDefaultWhenEnabled(String defaultWhenEnabled) { this.defaultWhenEnabled = defaultWhenEnabled; @@ -45,15 +55,19 @@ public Allocation setDefaultWhenEnabled(String defaultWhenEnabled) { } /** - * @return the defaultWhenDisabled + * Gets the variant to use when the feature flag is disabled and no specific allocation matches. + * + * @return the default variant when the feature flag is disabled */ public String getDefaultWhenDisabled() { return defaultWhenDisabled; } /** - * @param defaultWhenDisabled the defaultWhenDisabled to set - * @return Allocation + * Sets the variant to use when the feature flag is disabled and no specific allocation matches. + * + * @param defaultWhenDisabled the default variant when disabled + * @return the updated Allocation object */ public Allocation setDefaultWhenDisabled(String defaultWhenDisabled) { this.defaultWhenDisabled = defaultWhenDisabled; @@ -61,15 +75,19 @@ public Allocation setDefaultWhenDisabled(String defaultWhenDisabled) { } /** - * @return the users + * Gets the list of user-specific allocations for the feature flag. + * + * @return the list of user allocations */ public List getUser() { return user; } /** - * @param user the users to set - * @return Allocation + * Sets the list of user-specific allocations for the feature flag. + * + * @param user the list of user allocations + * @return the updated Allocation object */ public Allocation setUser(List user) { this.user = user; @@ -77,15 +95,19 @@ public Allocation setUser(List user) { } /** - * @return the groups + * Gets the list of group-specific allocations for the feature flag. + * + * @return the list of group allocations */ public List getGroup() { return group; } /** - * @param group the groups to set - * @return Allocation + * Sets the list of group-specific allocations for the feature flag. + * + * @param group the list of group allocations + * @return the updated Allocation object */ public Allocation setGroups(List group) { this.group = group; @@ -93,15 +115,19 @@ public Allocation setGroups(List group) { } /** - * @return the percentile + * Gets the list of percentile-based allocations for the feature flag. + * + * @return the list of percentile allocations */ public List getPercentile() { return percentile; } /** - * @param percentile the percentile to set - * @return Allocation + * Sets the list of percentile-based allocations for the feature flag. + * + * @param percentile the list of percentile allocations + * @return the updated Allocation object */ public Allocation setPercentile(List percentile) { this.percentile = percentile; @@ -109,15 +135,19 @@ public Allocation setPercentile(List percentile) { } /** - * @return the seed + * Gets the seed value used for randomization in allocation calculations. + * + * @return the seed value for allocation */ public String getSeed() { return seed; } /** - * @param seed the seed to set - * @return Allocation + * Sets the seed value used for randomization in allocation calculations. + * + * @param seed the seed value for allocation + * @return the updated Allocation object */ public Allocation setSeed(String seed) { this.seed = seed; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Conditions.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Conditions.java index 069c6bbafab7..8ec90d82c05b 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Conditions.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Conditions.java @@ -12,26 +12,55 @@ import com.fasterxml.jackson.annotation.JsonProperty; /** - * Conditions for evaluating a feature flag. +* Conditions for evaluating a feature flag. This class defines how feature filters +* should be evaluated to determine if a feature flag is enabled for the current request. +* It specifies both the filters to check and how their results should be combined +* (e.g., if all filters must pass or if only one needs to pass). */ @JsonIgnoreProperties(ignoreUnknown = true) public class Conditions { + + /** + * Creates a new instance of the Conditions class. + */ + public Conditions() { + } + + /** + * List of client-side feature filters to evaluate for determining if + * a feature flag is enabled. Each filter context contains parameters + * needed for filter evaluation. + */ @JsonProperty("client_filters") private List clientFilters = new ArrayList<>(); + /** + * Requirement type that determines the logic for combining filter results. + * Default is typically "All" which means all filters must pass for the + * feature to be enabled (logical AND). + */ @JsonProperty("requirement_type") private String requirementType = DEFAULT_REQUIREMENT_TYPE; /** - * @return the requirementType + * Gets the requirement type that determines how feature filters are evaluated. + * The requirement type specifies whether all filters must evaluate to true (AND logic) + * or if only one filter needs to evaluate to true (OR logic). + * + * @return the requirement type for filter evaluation */ public String getRequirementType() { return requirementType; } /** - * @param requirementType the requirementType to set - * @return Conditions + * Sets the requirement type that determines how feature filters are evaluated. + * Valid values are typically "All" (AND logic) or "Any" (OR logic), + * where "All" requires all filters to evaluate to true, and "Any" requires + * only one filter to evaluate to true. + * + * @param requirementType the requirement type to set for filter evaluation + * @return the updated Conditions object */ public Conditions setRequirementType(String requirementType) { this.requirementType = requirementType; @@ -39,15 +68,23 @@ public Conditions setRequirementType(String requirementType) { } /** - * @return the clientFilters + * Gets the list of client-side feature filters that should be evaluated + * to determine if a feature flag is enabled. + * Each filter contains its own parameters and evaluation context. + * + * @return the list of client-side feature filters */ public List getClientFilters() { return clientFilters; } /** - * @param clientFilters the clientFilters to set - * @return Conditions + * Sets the list of client-side feature filters to be evaluated + * to determine if a feature flag is enabled. + * Each filter should contain its necessary parameters and context for evaluation. + * + * @param clientFilters the list of client-side feature filters to set + * @return the updated Conditions object */ public Conditions setClientFilters(List clientFilters) { this.clientFilters = clientFilters; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/EvaluationEvent.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/EvaluationEvent.java index 446a5d5aa911..e6a6596ed2ba 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/EvaluationEvent.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/EvaluationEvent.java @@ -3,45 +3,79 @@ package com.azure.spring.cloud.feature.management.models; /** - * Event tracking the evaluation of a feature flag + * Event tracking the evaluation of a feature flag. This class captures information about + * a feature flag evaluation, including which user evaluated it, whether it was enabled, + * which variant was returned (if any), and the reason for the variant assignment. */ public class EvaluationEvent { + /** + * The feature flag that was evaluated. This contains information such as the + * feature flag name, its conditions, and other associated metadata. + */ private final Feature feature; + /** + * The identifier for the user who evaluated the feature flag. + * This is used for user-targeting scenarios and analytics. + */ private String user = ""; + /** + * Indicates whether the feature flag was determined to be enabled + * for this particular evaluation. + */ private boolean enabled = false; + /** + * The variant that was assigned during this feature flag evaluation. + * This is used for feature flags that support multiple variants beyond + * simply enabled or disabled. + */ private Variant variant; + /** + * The reason why a particular variant was assigned during the evaluation. + * This helps track the decision-making process that led to the variant selection. + */ private VariantAssignmentReason reason = VariantAssignmentReason.NONE; /** - * Creates an Evaluation Event for the given feature - * @param feature Feature + * Creates an Evaluation Event for the given feature. + * This constructor initializes a new evaluation event with the specified feature flag, + * while setting default values for other properties. + * + * @param feature The feature flag that is being evaluated */ public EvaluationEvent(Feature feature) { this.feature = feature; } /** - * @return the feature + * Gets the feature flag that was evaluated. + * + * @return the feature flag associated with this evaluation event */ public Feature getFeature() { return feature; } /** - * @return the user + * Gets the identifier of the user who evaluated the feature flag. + * This is useful for targeting specific users with features and for analytics tracking. + * + * @return the user identifier associated with this evaluation */ public String getUser() { return user; } /** - * @param user the user to set - * @return EvaluationEvent + * Sets the identifier of the user who evaluated the feature flag. + * This allows tracking which user accessed a particular feature. + * + * @param user the user identifier to associate with this evaluation + * @return the updated EvaluationEvent instance for method chaining */ public EvaluationEvent setUser(String user) { this.user = user; @@ -49,15 +83,21 @@ public EvaluationEvent setUser(String user) { } /** - * @return the enabled + * Determines whether the feature flag was enabled for this evaluation. + * This indicates the result of the evaluation process for the feature flag. + * + * @return true if the feature flag was enabled, false otherwise */ public boolean isEnabled() { return enabled; } /** - * @param enabled the enabled to set - * @return EvaluationEvent + * Sets whether the feature flag was enabled for this evaluation. + * This allows recording the result of the feature flag evaluation process. + * + * @param enabled true to mark the feature as enabled, false otherwise + * @return the updated EvaluationEvent instance for method chaining */ public EvaluationEvent setEnabled(boolean enabled) { this.enabled = enabled; @@ -65,15 +105,23 @@ public EvaluationEvent setEnabled(boolean enabled) { } /** - * @return the variant + * Gets the variant that was assigned during this feature flag evaluation. + * This is relevant for feature flags that support multiple variants + * rather than just being enabled or disabled. + * + * @return the variant assigned for this evaluation, or null if no variant was assigned */ public Variant getVariant() { return variant; } /** - * @param variant the variant to set - * @return EvaluationEvent + * Sets the variant that was assigned during this feature flag evaluation. + * This allows recording which specific variant of a feature flag was selected + * when multiple variants are supported. + * + * @param variant the variant to associate with this evaluation + * @return the updated EvaluationEvent instance for method chaining */ public EvaluationEvent setVariant(Variant variant) { this.variant = variant; @@ -81,15 +129,23 @@ public EvaluationEvent setVariant(Variant variant) { } /** - * @return the reason + * Gets the reason why a particular variant was assigned during this evaluation. + * This helps track the decision-making process behind the variant selection + * and can be useful for debugging and analytics. + * + * @return the reason for the variant assignment */ public VariantAssignmentReason getReason() { return reason; } /** - * @param reason the reason to set - * @return EvaluationEvent + * Sets the reason why a particular variant was assigned during this evaluation. + * This documents the logic behind why a specific variant was chosen, which + * can be useful for troubleshooting and analytics purposes. + * + * @param reason the reason for the variant assignment + * @return the updated EvaluationEvent instance for method chaining */ public EvaluationEvent setReason(VariantAssignmentReason reason) { this.reason = reason; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java index f292fa6e6969..3738806af4c4 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java @@ -4,45 +4,84 @@ package com.azure.spring.cloud.feature.management.models; import java.util.List; + import org.springframework.lang.NonNull; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; /** - * App Configuration Feature defines the feature name and a Map of FeatureFilterEvaluationContexts. + * This class represents a complete feature flag definition including its identity, + * description, enabled state, conditions for evaluation, variant allocation, + * and variant references for feature flags that support multiple variations. */ @JsonIgnoreProperties(ignoreUnknown = true) public class Feature { + + /** + * Creates a new instance of the Feature class. + */ + public Feature() { + } + + /** + * The unique identifier for this feature flag. + * This represents the name of the feature as stored in Azure App Configuration. + */ @JsonProperty("id") private String id; + /** + * A human-readable description of the feature flag and its purpose. + * This provides context about what the feature flag controls. + */ @JsonProperty("description") private String description; + /** + * The enabled state of the feature flag. + * When true, the feature is enabled by default, though conditions may still apply. + * When false, the feature is disabled by default. + */ @JsonProperty("enabled") private boolean enabled; + /** + * The set of conditions that determine when this feature flag should be enabled. + * These conditions contain feature filters and their evaluation logic. + */ @JsonProperty("conditions") @NonNull private Conditions conditions = new Conditions(); + /** + * The allocation strategy for this feature flag when using variants. + * Determines how users or requests are assigned to specific variants. + */ @JsonProperty("allocation") private Allocation allocation; + /** + * The list of variant references that define the different variations + * of this feature flag when it supports multiple implementations. + */ @JsonProperty("variants") private List variants; /** - * @return the id + * Gets the unique identifier of this feature flag. + * + * @return the feature flag's identifier */ public String getId() { return id; } /** - * @param id the id to set - * @return Feature + * Sets the unique identifier of this feature flag. + * + * @param id the feature flag identifier to set + * @return the updated Feature instance for method chaining */ public Feature setId(String id) { this.id = id; @@ -50,15 +89,20 @@ public Feature setId(String id) { } /** - * @return the enabled + * Determines whether this feature flag is enabled by default. + * Even when enabled, the flag may still be controlled by conditions and filters. + * + * @return true if the feature flag is enabled, false otherwise */ public boolean isEnabled() { return enabled; } /** - * @param enabled the enabled to set - * @return Feature + * Sets whether this feature flag is enabled by default. + * + * @param enabled true to enable the feature flag, false to disable it + * @return the updated Feature instance for method chaining */ public Feature setEnabled(boolean enabled) { this.enabled = enabled; @@ -66,15 +110,20 @@ public Feature setEnabled(boolean enabled) { } /** - * @return the description + * Gets the human-readable description of this feature flag. + * + * @return the description of the feature flag */ public String getDescription() { return description; } /** - * @param description the description to set - * @return Feature + * Sets the human-readable description of this feature flag. + * This provides context about what the feature flag controls. + * + * @param description the description to set for the feature flag + * @return the updated Feature instance for method chaining */ public Feature setDescription(String description) { this.description = description; @@ -82,15 +131,22 @@ public Feature setDescription(String description) { } /** - * @return the conditions + * Gets the set of conditions that determine when this feature flag should be enabled. + * The conditions contain feature filters and their evaluation logic. + * + * @return the conditions for feature flag evaluation */ public Conditions getConditions() { return conditions; } /** - * @param conditions the conditions to set - * @return Feature + * Sets the conditions that determine when this feature flag should be enabled. + * These conditions define the feature filters and logic for evaluating + * whether the feature should be enabled for a specific request. + * + * @param conditions the conditions to set for feature flag evaluation + * @return the updated Feature instance for method chaining */ public Feature setConditions(Conditions conditions) { this.conditions = conditions; @@ -98,15 +154,23 @@ public Feature setConditions(Conditions conditions) { } /** - * @return the allocation + * Gets the allocation strategy for this feature flag when using variants. + * The allocation defines how users or requests are assigned to specific variants + * through mechanisms like user targeting, percentile rollout, or group assignment. + * + * @return the allocation strategy for variant assignment */ public Allocation getAllocation() { return allocation; } /** - * @param allocation the allocation to set - * @return Feature + * Sets the allocation strategy for this feature flag when using variants. + * The allocation controls how users or requests are assigned to specific + * variants through user targeting, percentile rollout, or group assignment. + * + * @param allocation the allocation strategy to set for variant assignment + * @return the updated Feature instance for method chaining */ public Feature setAllocation(Allocation allocation) { this.allocation = allocation; @@ -114,15 +178,23 @@ public Feature setAllocation(Allocation allocation) { } /** - * @return the variants + * Gets the list of variant references that define the different variations + * of this feature flag. These variants represent different implementations + * or configurations that can be assigned to users when the feature is enabled. + * + * @return the list of variant references for this feature flag */ public List getVariants() { return variants; } /** - * @param variants the variants to set - * @return Feature + * Sets the list of variant references that define the different variations + * of this feature flag. These variants represent different implementations + * or configurations that can be dynamically assigned when the feature is enabled. + * + * @param variants the list of variant references to set for this feature flag + * @return the updated Feature instance for method chaining */ public Feature setVariants(List variants) { this.variants = variants; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/GroupAllocation.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/GroupAllocation.java index 36c2f845322a..221faf3ce65c 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/GroupAllocation.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/GroupAllocation.java @@ -5,37 +5,68 @@ import java.util.List; /** - * Group allocation of a variant. Contains a variant and a list of groups assigned to the variant. + * Group allocation of a variant for feature flag targeting. This class defines how specific + * user groups are assigned to a particular variant of a feature. It contains a reference + * to a variant name and a list of group identifiers that should receive this variant + * when the feature flag is evaluated. */ public class GroupAllocation { + /** + * Creates a new instance of the GroupAllocation class. + */ + public GroupAllocation() { + } + + /** + * The name of the variant that will be assigned to the specified groups. + * This corresponds to a variant defined in the feature flag configuration. + */ private String variant; + /** + * The list of group identifiers that should receive this variant. + * When a user belongs to any of these groups, they will be assigned + * this variant during feature flag evaluation. + */ private List groups; /** - * @return the variant + * Gets the name of the variant that is assigned to the specified groups. + * + * @return the variant name for this group allocation */ public String getVariant() { return variant; } /** - * @param variant the variant to set + * Sets the name of the variant that should be assigned to the specified groups. + * This should match a valid variant name defined in the feature flag configuration. + * + * @param variant the variant name to assign to the groups */ public void setVariant(String variant) { this.variant = variant; } /** - * @return the groups + * Gets the list of group identifiers that should receive this variant. + * When a user belongs to any of these groups, they will be assigned + * this variant during feature flag evaluation. + * + * @return the list of group identifiers for this allocation */ public List getGroups() { return groups; } /** - * @param groups the groups to set + * Sets the list of group identifiers that should receive this variant. + * When a user belongs to any of these groups, they will be assigned + * this variant during feature flag evaluation. + * + * @param groups the list of group identifiers to associate with this variant */ public void setGroups(List groups) { this.groups = groups; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/PercentileAllocation.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/PercentileAllocation.java index dde6abbffe02..38400bef0256 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/PercentileAllocation.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/PercentileAllocation.java @@ -5,54 +5,108 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; /** - * Percentile allocation of a variant. Contains a variant and a range of users assigned to the variant. + * Percentile allocation of a variant for feature flag targeting. This class defines how users + * are assigned to a specific variant based on a percentage range. It allows for gradual rollout + * of features to a specific percentage of users by defining a variant name and a numeric range + * (from-to) that determines which portion of users receive this variant when the feature is + * evaluated. */ @JsonIgnoreProperties(ignoreUnknown = true) public class PercentileAllocation { + /** + * Creates a new instance of the PercentileAllocation class. + */ + public PercentileAllocation() { + } + + /** + * The name of the variant that will be assigned to users within the specified + * percentage range. This corresponds to a variant defined in the feature flag + * configuration. + */ private String variant; + /** + * The lower bound (inclusive) of the percentage range for this variant allocation. + * Users with a computed hash value >= this percentage will be assigned to this + * variant. The value should be between 0.0 and 100.0. + */ private Double from; + /** + * The upper bound of the percentage range for this variant allocation. + *

+ * This value is exclusive (users with computed hash values strictly less than this value will be assigned to this variant), + * except when set to 100, where it becomes inclusive. The value should be between 0.0 and 100.0 and greater than the + * 'from' value. + *

+ */ private Double to; /** - * @return the variant + * Gets the name of the variant that is assigned to users within the specified + * percentage range. + * + * @return the variant name for this percentile allocation */ public String getVariant() { return variant; } /** - * @param variant the variant to set + * Sets the name of the variant that should be assigned to users within the + * specified percentage range. This should match a valid variant name defined + * in the feature flag configuration. + * + * @param variant the variant name to assign for this percentile range */ public void setVariant(String variant) { this.variant = variant; - } - - /** - * @return the from + } /** + * Gets the lower bound of the percentage range for this variant allocation. + * This represents the starting point of the percentile range where users will be + * assigned to this variant. The value is inclusive and typically between 0.0 and 100.0. + * + * @return the lower bound percentage value for this allocation */ public Double getFrom() { return from; } /** - * @param from the from to set + * Sets the lower bound of the percentage range for this variant allocation. + * This value is inclusive (users with computed hash values greater than or equal to this + * value will be assigned to this variant). The value should be between 0.0 and 100.0 + * and less than the 'to' value. + * + * @param from the lower bound percentage value to set for this allocation */ public void setFrom(Double from) { this.from = from; - } - - /** - * @return the to + } /** + * Gets the upper bound of the percentage range for this variant allocation. + *

+ * This value is exclusive (users with computed hash values strictly less than this value will be assigned to this variant), + * except when set to 100, where it becomes inclusive. The value should be between 0.0 and 100.0 and greater than the + * 'from' value. + *

+ * + * @return the upper bound percentage value for this allocation */ public Double getTo() { return to; } /** - * @param to the to to set + * Sets the upper bound of the percentage range for this variant allocation. + *

+ * This value is exclusive (users with computed hash values strictly less than this value will be assigned to this variant), + * except when set to 100, where it becomes inclusive. The value should be between 0.0 and 100.0 and greater than the + * 'from' value. + *

+ * + * @param to the upper bound percentage value to set for this allocation */ public void setTo(Double to) { this.to = to; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/UserAllocation.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/UserAllocation.java index c63d9261fd62..cbd87308cca8 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/UserAllocation.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/UserAllocation.java @@ -5,37 +5,68 @@ import java.util.List; /** - * User allocation of a variant. Contains a variant and a list of users assigned to the variant. + * User allocation of a variant for feature flag targeting. This class defines how specific + * users are assigned to a particular variant of a feature. It contains a reference to + * a variant name and a list of user identifiers that should receive this variant + * when the feature flag is evaluated. This enables targeted feature rollout to specific users. */ public class UserAllocation { + /** + * Creates a new instance of the UserAllocation class. + */ + public UserAllocation() { + } + + /** + * The name of the variant that will be assigned to the specified users. + * This corresponds to a variant defined in the feature flag configuration. + */ private String variant; + /** + * The list of user identifiers that should receive this variant. + * When a user matches any of these identifiers, they will be assigned + * this variant during feature flag evaluation. + */ private List users; /** - * @return the variant + * Gets the name of the variant that is assigned to the specified users. + * + * @return the variant name for this user allocation */ public String getVariant() { return variant; } /** - * @param variant the variant to set + * Sets the name of the variant that should be assigned to the specified users. + * This should match a valid variant name defined in the feature flag configuration. + * + * @param variant the variant name to assign to the users */ public void setVariant(String variant) { this.variant = variant; } /** - * @return the users + * Gets the list of user identifiers that should receive this variant. + * These identifiers are typically usernames, email addresses, or user IDs + * that uniquely identify users in the system. + * + * @return the list of user identifiers for this allocation */ public List getUsers() { return users; } /** - * @param users the users to set + * Sets the list of user identifiers that should receive this variant. + * When a user matches any of these identifiers, they will be assigned + * this variant during feature flag evaluation. + * + * @param users the list of user identifiers to associate with this variant */ public void setUsers(List users) { this.users = users; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Variant.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Variant.java index f811e69ad7c4..ade2c831cfbb 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Variant.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Variant.java @@ -3,18 +3,30 @@ package com.azure.spring.cloud.feature.management.models; /** - * The return object of getVariant that has the name of the variant and the instance value of the variant. + * Represents a feature flag variant in Azure Feature Management. + * This class is the return object of the getVariant method and contains both + * the name of the variant and its corresponding value instance. Variants allow + * for multiple implementations of a feature beyond simple on/off states. */ public class Variant { + /** + * The name of the variant that identifies this specific variation of the feature. + * This is used to match against variant references in feature flag configurations. + */ private String name; + /** + * The actual implementation value of the variant. This can be any type of object + * that represents the variant's behavior or configuration. + */ private Object value; /** - * Variant - * @param name Name of the Variant - * @param value Instance of the Variant + * Creates a new Variant with the specified name and value. + * + * @param name The name that identifies this variant in feature flag configurations + * @param value The implementation value or configuration for this variant */ public Variant(String name, Object value) { this.name = name; @@ -22,28 +34,37 @@ public Variant(String name, Object value) { } /** - * @return the name + * Gets the name of this variant. + * + * @return the name that identifies this variant */ public String getName() { return name; } /** - * @param name the name to set + * Sets the name of this variant. + * + * @param name the name to identify this variant */ public void setName(String name) { this.name = name; } /** - * @return the value + * Gets the implementation value of this variant. + * This can be any object that represents the variant's behavior or configuration. + * + * @return the implementation value for this variant */ public Object getValue() { return value; } /** - * @param value the value to set + * Sets the implementation value of this variant. + * + * @param value the implementation value to set for this variant */ public void setValue(Object value) { this.value = value; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/VariantAssignmentReason.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/VariantAssignmentReason.java index 334d402ccc03..4d1abbc64280 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/VariantAssignmentReason.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/VariantAssignmentReason.java @@ -5,30 +5,53 @@ /** * The reason why a given boolean/variant was returned when calling isEnabled/getVariant. + * This enum represents the different filtering mechanisms that determined the feature flag state. */ public enum VariantAssignmentReason { - /** None */ + /** + * Indicates no specific reason was assigned for the feature flag evaluation. + */ NONE("None"), - /** Default when Disabled */ + + /** + * Indicates the feature flag was evaluated based on the default value when the flag is disabled. + */ DEFAULT_WHEN_DISABLED("DefaultWhenDisabled"), - /** Default when Enabled */ + + /** + * Indicates the feature flag was evaluated based on the default value when the flag is enabled. + */ DEFAULT_WHEN_ENABLED("DefaultWhenEnabled"), - /** User */ + + /** + * Indicates the feature flag was evaluated based on user targeting criteria. + */ USER("User"), - /** Group */ + + /** + * Indicates the feature flag was evaluated based on group targeting criteria. + */ GROUP("Group"), - /** Percentile */ - PERCENTILE("Percentile"); - - private final String type; + + /** + * Indicates the feature flag was evaluated based on percentile targeting criteria. + */ + PERCENTILE("Percentile"); private final String type; + /** + * Creates a new instance of the VariantAssignmentReason enum with the specified type. + * + * @param type The string representation of the variant assignment reason + */ VariantAssignmentReason(final String type) { this.type = type; } /** - * @return the type + * Gets the string representation of this variant assignment reason. + * + * @return the string representation of this variant assignment reason */ public String getType() { return type; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/VariantReference.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/VariantReference.java index 7fe054cfcda8..845f41f16096 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/VariantReference.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/VariantReference.java @@ -6,26 +6,47 @@ /** * Reference to a Variant containing the Variant name, configuration value, and possible status override. + * This class provides a way to reference variants in feature flag configurations and their associated values. */ @JsonIgnoreProperties(ignoreUnknown = true) public class VariantReference { + /** + * The name of the variant reference. + */ private String name; + /** + * The configuration value associated with this variant reference. + * This can be any type of object depending on the feature configuration. + */ private Object configurationValue; + /** + * The status override that can be used to override the default status of a feature flag. + */ private String statusOverride; - + /** - * @return the name + * Creates a new instance of the VariantReference class. + */ + public VariantReference() { + } + + /** + * Gets the name of this variant reference. + * + * @return the name of the variant reference */ public String getName() { return name; } /** - * @param name the name to set - * @return VariantReference + * Sets the name of this variant reference. + * + * @param name the name to set for this variant reference + * @return the updated VariantReference instance for method chaining */ public VariantReference setName(String name) { this.name = name; @@ -33,31 +54,39 @@ public VariantReference setName(String name) { } /** - * @return the configurationValue + * Gets the configuration value associated with this variant reference. + * + * @return the configuration value of this variant reference */ public Object getConfigurationValue() { return configurationValue; } /** - * @param configurationValue the configurationValue to set - * @return VariantReference + * Sets the configuration value for this variant reference. + * + * @param configurationValue the configuration value to set for this variant reference + * @return the updated VariantReference instance for method chaining */ public VariantReference setConfigurationValue(Object configurationValue) { this.configurationValue = configurationValue; return this; } - + /** - * @return the statusOverride + * Gets the status override associated with this variant reference. + * + * @return the status override of this variant reference */ public String getStatusOverride() { return statusOverride; } /** - * @param statusOverride the statusOverride to set - * @return VariantReference + * Sets the status override for this variant reference. + * + * @param statusOverride the status override to set for this variant reference + * @return the updated VariantReference instance for method chaining */ public VariantReference setStatusOverride(String statusOverride) { this.statusOverride = statusOverride; From 57e5cc5eb5905fe5b411ceaa662a140b3d9f855e Mon Sep 17 00:00:00 2001 From: Matthew Metcalf Date: Tue, 6 May 2025 16:29:45 -0700 Subject: [PATCH 12/41] Feature Management Telemetry (#45073) * Feature Snapshot support * Removing dead code * Missing copyright * Adding back Feature Filter telemetry * Adding tests and simplifying code * TelemetryPublisher * Updating tests * Added telemetry object * Fixing tests * Added more tests plus fixes * Added publish telemetry tests * Update sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Adding Constants + Telemetry Package * Fixing endpoint usage + isEnabled * Remove Feature Id * Update FeatureFlagClientTest.java * FeatureId * Removing featureflagid * Adding allocation id * fixed tests * Updating Telemetry usage * Fixing tests * fixing build issues * Update LoggerTelemetryPublisherTest.java * Fix loading --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../AppConfigurationConstants.java | 2 - .../AppConfigurationReplicaClient.java | 12 +- ...AppConfigurationReplicaClientsBuilder.java | 14 +- .../implementation/FeatureFlagClient.java | 162 +++++++--- .../AppConfigurationReplicaClientTest.java | 22 +- .../implementation/FeatureFlagClientTest.java | 44 ++- .../config/implementation/TestConstants.java | 1 + .../config/implementation/TestUtils.java | 35 +-- .../pom.xml | 6 + .../FeatureManagementConfiguration.java | 12 +- .../feature/management/FeatureManager.java | 25 +- .../FeatureManagementProperties.java | 14 +- .../feature/management/models/Feature.java | 35 ++- .../models/PercentileAllocation.java | 21 +- .../feature/management/models/Telemetry.java | 73 +++++ .../telemetry/LoggerTelemetryPublisher.java | 123 ++++++++ .../telemetry/TelemetryConstants.java | 42 +++ .../telemetry/TelemetryPublisher.java | 21 ++ .../management/telemetry/package-info.java | 7 + .../management/FeatureManagerTest.java | 38 ++- .../management/FeatureManagerVariantTest.java | 4 +- .../FeatureManagementPropertiesTest.java | 12 +- .../LoggerTelemetryPublisherTest.java | 286 ++++++++++++++++++ .../src/test/resources/logback-test.xml | 16 + 24 files changed, 915 insertions(+), 112 deletions(-) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Telemetry.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/LoggerTelemetryPublisher.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/TelemetryConstants.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/TelemetryPublisher.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/package-info.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/telemetry/LoggerTelemetryPublisherTest.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/resources/logback-test.xml 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 24e5fba7b6d0..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 @@ -65,8 +65,6 @@ public class AppConfigurationConstants { public static final String REQUIREMENT_TYPE_SERVICE = "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/AppConfigurationReplicaClient.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClient.java index 95e3e90768b1..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 @@ -32,6 +32,8 @@ class AppConfigurationReplicaClient { private final String endpoint; + private final String originClient; + private final ConfigurationClient client; private Instant backoffEndTime; @@ -43,8 +45,9 @@ class AppConfigurationReplicaClient { * @param endpoint client endpoint * @param client Configuration Client to App Configuration store */ - AppConfigurationReplicaClient(String endpoint, ConfigurationClient client) { + 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; @@ -80,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. 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 40fdf1e5340a..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 @@ -156,7 +156,7 @@ 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(); @@ -168,7 +168,7 @@ List buildClients(ConfigStore configStore) { builder.endpoint(endpoint); - clients.add(modifyAndBuildClient(builder, endpoint, endpoints.size() - 1)); + clients.add(modifyAndBuildClient(builder, endpoint, configStore.getEndpoint(), endpoints.size() - 1)); } } return clients; @@ -180,23 +180,23 @@ AppConfigurationReplicaClient buildClient(String failoverEndpoint, ConfigStore c 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) { 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(isKeyVaultConfigured, replicaCount, Configuration.getGlobalConfiguration()); @@ -205,7 +205,7 @@ private AppConfigurationReplicaClient modifyAndBuildClient(ConfigurationClientBu if (clientCustomizer != null) { clientCustomizer.customize(builder, endpoint); } - return new AppConfigurationReplicaClient(endpoint, builder.buildClient()); + return new AppConfigurationReplicaClient(endpoint, originEndpoint, builder.buildClient()); } private ConfigurationClientBuilder createBuilderInstance() { 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 82e759e1f73e..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; @@ -18,10 +17,15 @@ 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.springframework.stereotype.Component; import org.springframework.util.StringUtils; @@ -40,7 +44,6 @@ 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 @@ -86,7 +89,7 @@ List loadFeatureFlags(AppConfigurationReplicaClient replicaClient, context.addData("FeatureFlagTracing", tracing); FeatureFlags features = replicaClient.listFeatureFlags(settingSelector, context); - loadedFeatureFlags.addAll(proccessFeatureFlags(features, keyFilter)); + loadedFeatureFlags.addAll(proccessFeatureFlags(features, replicaClient.getOriginClient())); } return loadedFeatureFlags; } @@ -115,7 +118,8 @@ List proccessFeatureFlags(FeatureFlags features, String endpoint) */ 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); @@ -123,51 +127,32 @@ 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)); } } - } - return feature; - } + } catch (JsonProcessingException e) { - /** - * @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 '=' - */ - private static String calculateFeatureFlagId(String key, String label) { - final String data = String.format("%s\n%s", key, label.isEmpty() ? null : label); - try { - MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); - final String beforeTrim = Base64URL.encode(sha256.digest(data.getBytes(StandardCharsets.UTF_8))) - .toString().replace('+', '-').replace('/', '_'); - final int index = beforeTrim.indexOf('='); - return beforeTrim.substring(0, index > -1 ? index : beforeTrim.length()); - } catch (NoSuchAlgorithmException e) { } - return ""; + return feature; } /** @@ -193,4 +178,103 @@ private void updateTelemetry(FeatureFlagConfigurationSetting featureFlag) { } } + /** + * 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> variantsValue = new ArrayList<>(); + JsonNode variantsNode = featureFlagValue.get("variants"); + if (variantsNode != null && variantsNode.isArray()) { + variantsNode.forEach(variantNode -> { + if (variantNode.isObject()) { + Map variantMap = new LinkedHashMap<>(); + variantNode.fields().forEachRemaining(entry -> variantMap.put(entry.getKey(), entry.getValue())); + variantsValue.add(variantMap); + } + }); + } + if (variantsValue != null && !variantsValue.isEmpty()) { + List> sortedVariants = variantsValue.stream() + .filter(v -> allocatedVariants.contains(v.get("name"))) + .sorted(Comparator.comparing(v -> (String) v.get("name"))) + .collect(Collectors.toList()); + + for (Map variant : sortedVariants) { + allocationId.append(Base64.getEncoder().encodeToString(((String) variant.get("name")).getBytes(StandardCharsets.UTF_8))).append(","); + Object configValue = variant.get("configuration_value"); + if (configValue instanceof Map) { + @SuppressWarnings("unchecked") + Map configMap = (Map) configValue; + allocationId.append(new TreeMap<>(configMap).toString()); + } + allocationId.append(";"); + } + if (!sortedVariants.isEmpty()) { + allocationId.setLength(allocationId.length() - 1); // Remove trailing semicolon + } + } + + // Create a SHA-256 hash of the allocationId + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashBytes = digest.digest(allocationId.toString().getBytes(StandardCharsets.UTF_8)); + + // Encode the first 15 bytes in Base64 URL-safe format + return Base64.getUrlEncoder().withoutPadding().encodeToString(Arrays.copyOf(hashBytes, 15)); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("SHA-256 algorithm not available", e); + } + } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java index f19f3c54532f..b6b48e7288e6 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java @@ -92,7 +92,7 @@ public void cleanup() throws Exception { @Test public void getWatchKeyTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); ConfigurationSetting watchKey = new ConfigurationSetting().setKey("watch").setLabel("\0"); @@ -123,7 +123,7 @@ public void getWatchKeyTest() { @Test public void listSettingsTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); ConfigurationSetting configurationSetting = new ConfigurationSetting().setKey("test-key"); List configurations = List.of(configurationSetting); @@ -155,7 +155,7 @@ public void listSettingsTest() { @Test public void listFeatureFlagsTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); FeatureFlagConfigurationSetting featureFlag = new FeatureFlagConfigurationSetting("Alpha", false); List configurations = List.of(featureFlag); @@ -192,7 +192,7 @@ public void listFeatureFlagsTest() { @Test public void listSettingsUnknownHostTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())) .thenThrow(new UncheckedIOException(new UnknownHostException())); @@ -201,7 +201,7 @@ public void listSettingsUnknownHostTest() { @Test public void listSettingsNoCredentialTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())) .thenThrow(new CredentialUnavailableException("No Credential")); @@ -211,7 +211,7 @@ public void listSettingsNoCredentialTest() { @Test public void getWatchNoCredentialTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); when(clientMock.getConfigurationSettingWithResponse(Mockito.any(), Mockito.any(), Mockito.anyBoolean(), Mockito.any())) .thenThrow(new CredentialUnavailableException("No Credential")); @@ -221,7 +221,7 @@ public void getWatchNoCredentialTest() { @Test public void backoffTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); // Setups in the past and with no errors. assertTrue(client.getBackoffEndTime().isBefore(Instant.now())); @@ -249,7 +249,7 @@ public void backoffTest() { @Test public void listSettingSnapshotTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); List configurations = new ArrayList<>(); ConfigurationSnapshot snapshot = new ConfigurationSnapshot(null); @@ -283,7 +283,7 @@ public void listSettingSnapshotTest() { @Test public void listSettingSnapshotInvalidCompositionTypeTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); ConfigurationSnapshot snapshot = new ConfigurationSnapshot(null); snapshot.setSnapshotComposition(SnapshotComposition.KEY_LABEL); @@ -299,7 +299,7 @@ public void listSettingSnapshotInvalidCompositionTypeTest() { @Test public void updateSyncTokenTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); String fakeToken = "fake_sync_token"; client.updateSyncToken(fakeToken); @@ -312,7 +312,7 @@ public void updateSyncTokenTest() { @Test public void checkWatchKeysTest() { - AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); FeatureFlagConfigurationSetting featureFlag = new FeatureFlagConfigurationSetting("Alpha", false); List configurations = List.of(featureFlag); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java index 4615b389cbaf..d462ccb7d481 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java @@ -4,7 +4,6 @@ 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_REFERENCE; import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.DEFAULT_ROLLOUT_PERCENTAGE; import static com.azure.spring.cloud.appconfiguration.config.implementation.TestConstants.FEATURE_LABEL; @@ -17,6 +16,7 @@ import static com.azure.spring.cloud.appconfiguration.config.implementation.TestUtils.createItemFeatureFlag; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; @@ -184,13 +184,49 @@ public void testAndRequirementType() { @Test public void testFeatureFlagTelemetry() { Feature feature = FeatureFlagClient.createFeature(TELEMETRY_FEATURE, TEST_ENDPOINT); - - String featureFlagId = "yON6V7DTGfVgOKfnPtue_2hS-CFVV5ecv-dcjqCFQt4"; String featureFlagReference = String.format("%s/kv/%s", TEST_ENDPOINT, ".appconfig.featureflag/Delta"); - assertEquals(featureFlagId, feature.getTelemetry().getMetadata().get(FEATURE_FLAG_ID)); assertEquals(featureFlagReference, feature.getTelemetry().getMetadata().get(FEATURE_FLAG_REFERENCE)); assertEquals(TEST_E_TAG, feature.getTelemetry().getMetadata().get(E_TAG)); } + @Test + public void testAllocationIdInTelemetry() { + Feature feature = FeatureFlagClient.createFeature(TELEMETRY_FEATURE, TEST_ENDPOINT); + + assertEquals("wz4oTwm3SjARe1SrmzT7", feature.getTelemetry().getMetadata().get("AllocationId")); + + feature = FeatureFlagClient.createFeature(ALL_FEATURE, TEST_ENDPOINT); + assertNull(feature.getTelemetry()); + } + + @Test + public void testAllocationIdWithDifferentSeed() { + FeatureFlagConfigurationSetting featureFlag = createItemFeatureFlag( + ".appconfig.featureflag/", "TestFeature", + "{\"allocation\":{\"seed\":\"newSeed\"},\"telemetry\":{\"enabled\":true}}", FEATURE_LABEL, FEATURE_FLAG_CONTENT_TYPE, TEST_E_TAG); + + Feature feature = FeatureFlagClient.createFeature(featureFlag, TEST_ENDPOINT); + assertEquals("RkxUK5CoaOaNWBjc55Mi", feature.getTelemetry().getMetadata().get("AllocationId")); + } + + @Test + public void testAllocationIdWithVariants() { + String flagValue = "{\"allocation\": { \"percentile\": [{\"variant\": \"Off\", \"from\": 0, \"to\": 50}, {\"variant\": \"On\", \"from\": 50, \"to\": 100}], \"default_when_enabled\": \"Off2\", \"default_when_disabled\": \"Off\" }, \"telemetry\": {\"enabled\": true}}"; + FeatureFlagConfigurationSetting featureFlag = createItemFeatureFlag( + ".appconfig.featureflag/", "TestFeature", flagValue, FEATURE_LABEL, FEATURE_FLAG_CONTENT_TYPE, TEST_E_TAG); + + Feature feature = FeatureFlagClient.createFeature(featureFlag, TEST_ENDPOINT); + assertEquals("wGzzPy4qGy92SHnMtSvY", feature.getTelemetry().getMetadata().get("AllocationId")); + } + + @Test + public void testAllocationIdWithEmptyAllocation() { + FeatureFlagConfigurationSetting featureFlag = createItemFeatureFlag( + ".appconfig.featureflag/", "TestFeature", + "{\"allocation\":{},\"telemetry\":{\"enabled\":true}}}", FEATURE_LABEL, FEATURE_FLAG_CONTENT_TYPE, TEST_E_TAG); + + Feature feature = FeatureFlagClient.createFeature(featureFlag, TEST_ENDPOINT); + assertNull(feature.getTelemetry().getMetadata().get("AllocationId")); + } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/TestConstants.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/TestConstants.java index 48e379747e3b..66739dec6904 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/TestConstants.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/TestConstants.java @@ -94,6 +94,7 @@ public final class TestConstants { public static final String FEATURE_VALUE_TELEMETRY = "{\"id\":\"Delta\",\"description\":\"\",\"enabled\":true," + "\"conditions\":{\"client_filters\":[{\"Name\":\"TestFilter\",\"Parameters\":{\"key\":\"value\"}}]}," + + "\"allocation\": { \"percentile\": [{\"variant\": \"Off\", \"from\": 0, \"to\": 50}, {\"variant\": \"On\", \"from\": 50, \"to\": 100}], \"default_when_enabled\": \"Off\", \"default_when_disabled\": \"Off\" }, " + "\"telemetry\":{\"enabled\":true,\"metadata\":{\"key\":\"value\"}}}"; public static final String FEATURE_VALUE_ALL = "{ \"id\": \"AndTest\", \"description\": \"\",\"enabled\": true,\"conditions\": {\"requirement_type\": \"All\",\"client_filters\": [{\"Name\": \"percentageFilter\",\"Parameters\": {\"Value\": 50}},{\"Name\": \"percentageFilter\",\"Parameters\": {\"Value\": 50}}]}}"; diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/TestUtils.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/TestUtils.java index 0f36bc5b6294..af7314924a2f 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/TestUtils.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/TestUtils.java @@ -69,26 +69,27 @@ static FeatureFlagConfigurationSetting createItemFeatureFlag(String prefix, Stri try { if (value != null) { - JsonNode node = MAPPER.readTree(value).get("conditions").get("client_filters"); - - for (int i = 0; i < node.size(); i++) { - JsonNode nodeFilter = node.get(i); - FeatureFlagFilter filter = new FeatureFlagFilter(nodeFilter.get("Name").asText()); - - JsonNode nodeParams = nodeFilter.get("Parameters"); - if (nodeParams != null) { - for (int j = 0; j < nodeParams.size(); j++) { - // JsonNode param = nodeParams. - Map result = MAPPER.convertValue(nodeParams, - new TypeReference>() { - }); - Set parameters = result.keySet(); - for (String paramKey : parameters) { - filter.addParameter(paramKey, result.get(paramKey)); + JsonNode node = MAPPER.readTree(value).get("conditions"); + if (node != null) { + JsonNode clientFiltersNode = node.get("client_filters"); + if (clientFiltersNode != null) { + for (int i = 0; i < clientFiltersNode.size(); i++) { + JsonNode nodeFilter = clientFiltersNode.get(i); + FeatureFlagFilter filter = new FeatureFlagFilter(nodeFilter.get("Name").asText()); + + JsonNode nodeParams = nodeFilter.get("Parameters"); + if (nodeParams != null) { + Map result = MAPPER.convertValue(nodeParams, + new TypeReference>() { + }); + Set parameters = result.keySet(); + for (String paramKey : parameters) { + filter.addParameter(paramKey, result.get(paramKey)); + } } + item.addClientFilter(filter); } } - item.addClientFilter(filter); } } } catch (JsonProcessingException e) { diff --git a/sdk/spring/spring-cloud-azure-feature-management/pom.xml b/sdk/spring/spring-cloud-azure-feature-management/pom.xml index b1b431e07538..abdc8e70d779 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/pom.xml +++ b/sdk/spring/spring-cloud-azure-feature-management/pom.xml @@ -90,6 +90,12 @@ 1.15.11 test + + ch.qos.logback + logback-classic + 1.5.18 + test + diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java index 106001a42214..8ca875376f42 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java @@ -21,6 +21,8 @@ import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties; import com.azure.spring.cloud.feature.management.targeting.TargetingContextAccessor; import com.azure.spring.cloud.feature.management.targeting.TargetingEvaluationOptions; +import com.azure.spring.cloud.feature.management.telemetry.LoggerTelemetryPublisher; +import com.azure.spring.cloud.feature.management.telemetry.TelemetryPublisher; /** * Configuration for setting up FeatureManager @@ -41,7 +43,7 @@ class FeatureManagementConfiguration implements ApplicationContextAware { */ @Bean FeatureManager featureManager(FeatureManagementProperties featureManagementConfigurations, - FeatureManagementConfigProperties properties, + FeatureManagementConfigProperties properties, TelemetryPublisher telemetryPublisher, ObjectProvider contextAccessorProvider, ObjectProvider evaluationOptionsProvider) { @@ -50,7 +52,7 @@ FeatureManager featureManager(FeatureManagementProperties featureManagementConfi .getIfAvailable(() -> new TargetingEvaluationOptions()); return new FeatureManager(appContext, featureManagementConfigurations, properties, contextAccessor, - evaluationOptions); + evaluationOptions, telemetryPublisher); } @Override @@ -77,4 +79,10 @@ public PercentageFilter percentageFilter() { public TargetingFilter targettingFilter(TargetingContextAccessor context) { return new TargetingFilter(context, new TargetingEvaluationOptions().setIgnoreCase(true)); } + + @Bean + @ConditionalOnMissingBean(TelemetryPublisher.class) + public TelemetryPublisher telemetryPublisher() { + return new LoggerTelemetryPublisher(); + } } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java index d30e844c39eb..775f03f107a7 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java @@ -41,6 +41,7 @@ import com.azure.spring.cloud.feature.management.targeting.TargetingContextAccessor; import com.azure.spring.cloud.feature.management.targeting.TargetingEvaluationOptions; import com.azure.spring.cloud.feature.management.targeting.TargetingFilterContext; +import com.azure.spring.cloud.feature.management.telemetry.TelemetryPublisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -63,6 +64,8 @@ public class FeatureManager { private final TargetingContextAccessor contextAccessor; private final TargetingEvaluationOptions evaluationOptions; + + private final TelemetryPublisher telemetryPublisher; /** * Used to evaluate the enabled state of a feature and/or get the assigned variant of a feature, if any. @@ -75,12 +78,13 @@ public class FeatureManager { */ FeatureManager(ApplicationContext context, FeatureManagementProperties featureManagementConfigurations, FeatureManagementConfigProperties properties, TargetingContextAccessor contextAccessor, - TargetingEvaluationOptions evaluationOptions) { + TargetingEvaluationOptions evaluationOptions, TelemetryPublisher telemetryPublisher) { this.context = context; this.featureManagementConfigurations = featureManagementConfigurations; this.properties = properties; this.contextAccessor = contextAccessor; this.evaluationOptions = evaluationOptions; + this.telemetryPublisher = telemetryPublisher; } /** @@ -181,7 +185,13 @@ public Mono getVariantAsync(String feature, Object featureContext) { private Mono checkFeature(String featureName, Object featureContext) throws FilterNotFoundException { - Feature featureFlag = featureManagementConfigurations.getFeatureFlags().stream() + List featureFlags = featureManagementConfigurations.getFeatureFlags(); + + if (featureFlags == null) { + return Mono.just(new EvaluationEvent(null)); + } + + Feature featureFlag = featureFlags.stream() .filter(feature -> feature.getId().equals(featureName)).findAny().orElse(null); EvaluationEvent event = new EvaluationEvent(featureFlag); @@ -193,14 +203,23 @@ private Mono checkFeature(String featureName, Object featureCon if (!featureFlag.isEnabled()) { this.assignDefaultDisabledReason(event); + event.setEnabled(false); + if (telemetryPublisher != null && featureFlag.getTelemetry().isEnabled()) { + telemetryPublisher.publishTelemetry(event); + } // If a feature flag is disabled and override can't enable it - return Mono.just(event.setEnabled(false)); + return Mono.just(event); } Mono result = this.checkFeatureFilters(event, featureContext); result = assignAllocation(result); + result = result.doOnSuccess(resultEvent -> { + if (telemetryPublisher != null && featureFlag.getTelemetry().isEnabled()) { + telemetryPublisher.publishTelemetry(resultEvent); + } + }); return result; } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementProperties.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementProperties.java index 73f9c01d891a..fbb4d4733d64 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementProperties.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementProperties.java @@ -2,23 +2,18 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.management.implementation; -import java.util.ArrayList; import java.util.List; -import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; import com.azure.spring.cloud.feature.management.models.Feature; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.ObjectMapper; /** * Configuration Properties for Feature Management. Processes the configurations to be usable by Feature Management. */ @ConfigurationProperties(prefix = "feature-management") public class FeatureManagementProperties { - - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @JsonProperty("feature-flags") private List featureFlags; @@ -26,12 +21,9 @@ public class FeatureManagementProperties { public List getFeatureFlags() { return featureFlags; } - - public void setFeatureFlags(List> featureFlags) { - this.featureFlags = new ArrayList<>(); - for (Map featureFlag: featureFlags) { - this.featureFlags.add(OBJECT_MAPPER.convertValue(featureFlag, Feature.class)); - } + + public void setFeatureFlags(List featureFlags) { + this.featureFlags = featureFlags; } } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java index 3738806af4c4..2beb3aef3be7 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java @@ -67,6 +67,14 @@ public Feature() { */ @JsonProperty("variants") private List variants; + + /** + * The telemetry configuration for this feature flag. + * Controls whether events related to this feature are logged + * and what additional metadata is included. + */ + @JsonProperty("telemetry") + private Telemetry telemetry = new Telemetry(); /** * Gets the unique identifier of this feature flag. @@ -152,7 +160,7 @@ public Feature setConditions(Conditions conditions) { this.conditions = conditions; return this; } - + /** * Gets the allocation strategy for this feature flag when using variants. * The allocation defines how users or requests are assigned to specific variants @@ -200,4 +208,29 @@ public Feature setVariants(List variants) { this.variants = variants; return this; } + + /** + * Gets the telemetry configuration for this feature flag. + * The telemetry configuration controls whether events related to this + * feature flag should be logged and what additional metadata should be included. + * + * @return the telemetry configuration for this feature flag + */ + public Telemetry getTelemetry() { + return telemetry; + } + + /** + * Sets the telemetry configuration for this feature flag. + * This controls whether events related to this feature flag + * should be logged and what additional metadata should be included. + * + * @param telemetry the telemetry configuration to set for this feature flag + * @return the updated Feature instance for method chaining + */ + public Feature setTelemetry(Telemetry telemetry) { + this.telemetry = telemetry; + return this; + } + } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/PercentileAllocation.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/PercentileAllocation.java index 38400bef0256..610947aaa230 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/PercentileAllocation.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/PercentileAllocation.java @@ -60,10 +60,14 @@ public String getVariant() { * in the feature flag configuration. * * @param variant the variant name to assign for this percentile range + * @return the updated PercentileAllocation instance for method chaining */ - public void setVariant(String variant) { + public PercentileAllocation setVariant(String variant) { this.variant = variant; - } /** + return this; + } + + /** * Gets the lower bound of the percentage range for this variant allocation. * This represents the starting point of the percentile range where users will be * assigned to this variant. The value is inclusive and typically between 0.0 and 100.0. @@ -81,10 +85,15 @@ public Double getFrom() { * and less than the 'to' value. * * @param from the lower bound percentage value to set for this allocation + * @return the updated PercentileAllocation instance for method chaining */ - public void setFrom(Double from) { + public PercentileAllocation setFrom(Double from) { this.from = from; - } /** + return this; + } + + + /** * Gets the upper bound of the percentage range for this variant allocation. *

* This value is exclusive (users with computed hash values strictly less than this value will be assigned to this variant), @@ -107,9 +116,11 @@ public Double getTo() { *

* * @param to the upper bound percentage value to set for this allocation + * @return the updated PercentileAllocation instance for method chaining */ - public void setTo(Double to) { + public PercentileAllocation setTo(Double to) { this.to = to; + return this; } } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Telemetry.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Telemetry.java new file mode 100644 index 000000000000..26c9ab88bb6f --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Telemetry.java @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.management.models; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Represents telemetry configuration for feature management. + * This class controls whether telemetry is enabled and provides additional + * metadata for customizing telemetry information. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Telemetry { + + private boolean enabled; + + private Map metadata; + + /** + * Creates a new instance of the Telemetry class. + * By default, telemetry is enabled and no metadata is set. + */ + public Telemetry() { + this.enabled = false; // Default to disabled + this.metadata = new HashMap<>(); + } + + /** + * Gets whether telemetry is enabled. + * + * @return {@code true} if telemetry is enabled, {@code false} otherwise + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Sets whether telemetry is enabled. + * + * @param enabled {@code true} to enable telemetry, {@code false} to disable it + * @return The updated Telemetry instance for method chaining + */ + public Telemetry setEnabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + /** + * Gets the metadata associated with telemetry. + * The metadata contains key-value pairs that provide additional context + * for telemetry events. + * + * @return A map of metadata key-value pairs + */ + public Map getMetadata() { + return metadata; + } + + /** + * Sets the metadata associated with telemetry. + * + * @param metadata A map of key-value pairs to provide additional context for telemetry events + * @return The updated Telemetry instance for method chaining + */ + public Telemetry setMetadata(Map metadata) { + this.metadata = metadata; + return this; + } + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/LoggerTelemetryPublisher.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/LoggerTelemetryPublisher.java new file mode 100644 index 000000000000..1c46ef143a6d --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/LoggerTelemetryPublisher.java @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.management.telemetry; + +import static com.azure.spring.cloud.feature.management.telemetry.TelemetryConstants.APPLICATION_INSIGHTS_CUSTOM_EVENT_KEY; +import static com.azure.spring.cloud.feature.management.telemetry.TelemetryConstants.DEFAULT_WHEN_ENABLED; +import static com.azure.spring.cloud.feature.management.telemetry.TelemetryConstants.ENABLED; +import static com.azure.spring.cloud.feature.management.telemetry.TelemetryConstants.EVALUATION_EVENT_VERSION; +import static com.azure.spring.cloud.feature.management.telemetry.TelemetryConstants.EVENT_NAME; +import static com.azure.spring.cloud.feature.management.telemetry.TelemetryConstants.FEATURE_NAME; +import static com.azure.spring.cloud.feature.management.telemetry.TelemetryConstants.REASON; +import static com.azure.spring.cloud.feature.management.telemetry.TelemetryConstants.VARIANT; +import static com.azure.spring.cloud.feature.management.telemetry.TelemetryConstants.VARIANT_ASSIGNMENT_PERCENTAGE; +import static com.azure.spring.cloud.feature.management.telemetry.TelemetryConstants.VERSION; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +import com.azure.spring.cloud.feature.management.models.EvaluationEvent; +import com.azure.spring.cloud.feature.management.models.Feature; +import com.azure.spring.cloud.feature.management.models.PercentileAllocation; +import com.azure.spring.cloud.feature.management.models.Variant; +import com.azure.spring.cloud.feature.management.models.VariantAssignmentReason; + +/** + * Telemetry publisher that logs feature evaluation events using SLF4J. This + * class implements the TelemetryPublisher interface and is responsible for + * publishing telemetry events related to feature evaluations. It uses SLF4J for + * logging the events, which allows for easy integration with various logging + * frameworks. It uses MDC (Mapped Diagnostic Context) to add contextual + * information to the logs, such as feature name, enabled status, variant name, + * and reason for the evaluation. + */ +public class LoggerTelemetryPublisher implements TelemetryPublisher { + + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerTelemetryPublisher.class); + + /** + * The name of the custom event for telemetry logging. This is used to + * identify the type of event being logged in the telemetry system. + */ + public LoggerTelemetryPublisher() { + // Private constructor to prevent instantiation + } + + /** + * Publishes telemetry events related to feature evaluations. It logs the + * evaluation event using SLF4J, adding contextual information to the logs using + * MDC. + * + * @param evaluationEvent The evaluation event to be published. + */ + public void publishTelemetry(EvaluationEvent evaluationEvent) { + if (evaluationEvent == null || evaluationEvent.getFeature() == null) { + return; + } + + Feature feature = evaluationEvent.getFeature(); + + Variant variant = evaluationEvent.getVariant(); + + Map eventProperties = new HashMap<>(Map.of( + FEATURE_NAME, feature.getId(), + ENABLED, String.valueOf(evaluationEvent.isEnabled()), + REASON, evaluationEvent.getReason().getType(), + VERSION, EVALUATION_EVENT_VERSION)); + + if (variant != null) { + eventProperties.put(VARIANT, variant.getName()); + } + + if (evaluationEvent.getReason() == VariantAssignmentReason.DEFAULT_WHEN_ENABLED) { + // Calculate the amount of unallocated variant percentage. This is therefore the amount allocated to the default when enabled variant. + eventProperties.put(VARIANT_ASSIGNMENT_PERCENTAGE, "100"); + if (feature.getAllocation() != null && feature.getAllocation().getPercentile() != null) { + double allocationPercentage = 0.0; + for (PercentileAllocation allocation : feature.getAllocation().getPercentile()) { + if (allocation.getTo() != null && allocation.getFrom() != null) { + allocationPercentage += allocation.getTo() - allocation.getFrom(); + } + } + + eventProperties.put(VARIANT_ASSIGNMENT_PERCENTAGE, String.valueOf(100 - allocationPercentage)); + } + } else if (evaluationEvent.getReason() == VariantAssignmentReason.PERCENTILE) { + if (feature.getAllocation() != null && feature.getAllocation().getPercentile() != null) { + eventProperties.put(VARIANT_ASSIGNMENT_PERCENTAGE, String.valueOf(feature.getAllocation().getPercentile().stream() + // Filter out null values and calculate the sum of the allocation percentages + // for the specific variant. + .filter(percentile -> percentile.getVariant() != null && variant != null + && percentile.getVariant().equals(variant.getName())) + .filter(allocation -> allocation.getTo() != null && allocation.getFrom() != null) + // Calculate the percentage of the variant allocation. + .mapToDouble(allocation -> allocation.getTo() - allocation.getFrom()) + .sum())); + } + } + + if (evaluationEvent.getFeature().getAllocation() != null + && evaluationEvent.getFeature().getAllocation().getDefaultWhenEnabled() != null) { + eventProperties.put(DEFAULT_WHEN_ENABLED, + evaluationEvent.getFeature().getAllocation().getDefaultWhenEnabled()); + } + + // Set the key-value pair in the MDC context for logging + for (Map.Entry entry : eventProperties.entrySet()) { + MDC.put(entry.getKey(), entry.getValue()); + } + MDC.put(APPLICATION_INSIGHTS_CUSTOM_EVENT_KEY, EVENT_NAME); + LOGGER.info(EVENT_NAME); + + // Remove the key-value pairs from the MDC context after logging. + MDC.remove(APPLICATION_INSIGHTS_CUSTOM_EVENT_KEY); + for (String key : eventProperties.keySet()) { + MDC.remove(key); + } + } + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/TelemetryConstants.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/TelemetryConstants.java new file mode 100644 index 000000000000..54be2d2f09e7 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/TelemetryConstants.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.management.telemetry; + +/** + * This class contains constants used for telemetry events related to feature + * evaluations. These constants are used to define the structure and content of + * telemetry events, including event names, property names, and versioning + * information. + */ +public final class TelemetryConstants { + + static final String EVENT_NAME = "FeatureEvaluation"; + + static final String FEATURE_NAME = "FeatureName"; + + static final String ENABLED = "Enabled"; + + static final String VARIANT = "Variant"; + + static final String REASON = "VariantAssignmentReason"; + + static final String DEFAULT_WHEN_ENABLED = "DefaultWhenEnabled"; + + static final String VERSION = "Version"; + + static final String VARIANT_ASSIGNMENT_PERCENTAGE = "VariantAssignmentPercentage"; + + static final String EVALUATION_EVENT_VERSION = "1.1.0"; + + static final String APPLICATION_INSIGHTS_CUSTOM_EVENT_KEY = "microsoft.custom_event.name"; + + /** + * This class contains constants used for telemetry events related to feature + * evaluations. These constants are used to define the structure and content of + * telemetry events, including event names, property names, and versioning + * information. + */ + private TelemetryConstants() { + // Private constructor to prevent instantiation + } +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/TelemetryPublisher.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/TelemetryPublisher.java new file mode 100644 index 000000000000..bd324507aae4 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/TelemetryPublisher.java @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.management.telemetry; + +import com.azure.spring.cloud.feature.management.models.EvaluationEvent; + +/** + * TelemetryPublisher is an interface for publishing telemetry events. + * Implementations of this interface can be used to send telemetry data to various + * telemetry services or systems. + */ +public interface TelemetryPublisher { + + /** + * Publishes an evaluation event to the telemetry system. + * + * @param evaluationEvent The evaluation event to be published. + */ + void publishTelemetry(EvaluationEvent evaluationEvent); + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/package-info.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/package-info.java new file mode 100644 index 000000000000..2e07b8d70f7a --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/** + * Package containing classes for telemetry related to feature flags. + */ + +package com.azure.spring.cloud.feature.management.telemetry; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java index bceb7de20fae..ec7d9a03fb31 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java @@ -30,9 +30,12 @@ import com.azure.spring.cloud.feature.management.implementation.FeatureManagementConfigProperties; import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties; import com.azure.spring.cloud.feature.management.models.Conditions; +import com.azure.spring.cloud.feature.management.models.EvaluationEvent; import com.azure.spring.cloud.feature.management.models.Feature; import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext; import com.azure.spring.cloud.feature.management.models.FilterNotFoundException; +import com.azure.spring.cloud.feature.management.models.Telemetry; +import com.azure.spring.cloud.feature.management.telemetry.TelemetryPublisher; /** * Unit tests for FeatureManager. @@ -51,12 +54,15 @@ public class FeatureManagerTest { @Mock private FeatureManagementProperties featureManagementPropertiesMock; + @Mock + private TelemetryPublisher telemetryPublisher; + @BeforeEach public void setup() { MockitoAnnotations.openMocks(this); when(properties.isFailFast()).thenReturn(true); - featureManager = new FeatureManager(context, featureManagementPropertiesMock, properties, null, null); + featureManager = new FeatureManager(context, featureManagementPropertiesMock, properties, null, null, telemetryPublisher); } @AfterEach @@ -211,6 +217,36 @@ public void timeWindowFilter() { assertTrue(featureManager.isEnabled("Alpha")); } + @Test + public void telemetryPublisherCalledWhenFeatureEnabledWithTelemetry() { + List features = List.of(new Feature().setId("EnabledFeatureWithTelemetry").setEnabled(true) + .setTelemetry(new Telemetry().setEnabled(true))); + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + + assertTrue(featureManager.isEnabled("EnabledFeatureWithTelemetry")); + verify(telemetryPublisher, times(1)).publishTelemetry(Mockito.any(EvaluationEvent.class)); + } + + @Test + public void telemetryPublisherNotCalledWhenTelemetryDisabled() { + List features = List.of(new Feature().setId("FeatureWithTelemetryDisabled").setEnabled(true) + .setTelemetry(new Telemetry().setEnabled(false))); + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + + assertTrue(featureManager.isEnabled("FeatureWithTelemetryDisabled")); + verify(telemetryPublisher, times(0)).publishTelemetry(Mockito.any(EvaluationEvent.class)); + } + + @Test + public void telemetryPublisherCalledWhenFeatureDisabledWithTelemetry() { + List features = List.of(new Feature().setId("DisabledFeatureWithTelemetry").setEnabled(false) + .setTelemetry(new Telemetry().setEnabled(true))); + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + + assertFalse(featureManager.isEnabled("DisabledFeatureWithTelemetry")); + verify(telemetryPublisher, times(1)).publishTelemetry(Mockito.any(EvaluationEvent.class)); + } + class AlwaysOnFilter implements FeatureFilter { @Override diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerVariantTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerVariantTest.java index 7638d141bd55..93e2cdebe716 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerVariantTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerVariantTest.java @@ -57,7 +57,7 @@ public void setup() { when(properties.isFailFast()).thenReturn(true); featureManager = new FeatureManager(context, featureManagementPropertiesMock, properties, contextAccessorMock, - null); + null, null); } @AfterEach @@ -80,7 +80,7 @@ public void noAssignedVariants() { @Test public void noAssigner() { - featureManager = new FeatureManager(context, featureManagementPropertiesMock, properties, null, null); + featureManager = new FeatureManager(context, featureManagementPropertiesMock, properties, null, null, null); List features = List.of(new Feature().setId("No Assigner").setVariants(createVariants())); when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementPropertiesTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementPropertiesTest.java index 9077a8968abb..6a8b330688b7 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementPropertiesTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementPropertiesTest.java @@ -5,25 +5,25 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import org.junit.jupiter.api.Test; +import com.azure.spring.cloud.feature.management.models.Feature; + public class FeatureManagementPropertiesTest { @Test public void setFeatureFlagsTest() { FeatureManagementProperties properties = new FeatureManagementProperties(); - List> featureFlags = new ArrayList<>(); + List featureFlags = new ArrayList<>(); properties.setFeatureFlags(featureFlags); assertEquals(0, properties.getFeatureFlags().size()); - Map alphaFeatureFlag = new HashMap(); - alphaFeatureFlag.put("id", "alpha"); - alphaFeatureFlag.put("enabled", true); + Feature alphaFeatureFlag = new Feature(); + alphaFeatureFlag.setId("alpha"); + alphaFeatureFlag.setEnabled(true); featureFlags.add(alphaFeatureFlag); properties.setFeatureFlags(featureFlags); assertEquals(1, properties.getFeatureFlags().size()); diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/telemetry/LoggerTelemetryPublisherTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/telemetry/LoggerTelemetryPublisherTest.java new file mode 100644 index 000000000000..e826f873cf09 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/telemetry/LoggerTelemetryPublisherTest.java @@ -0,0 +1,286 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.management.telemetry; + +import static com.azure.spring.cloud.feature.management.telemetry.TelemetryConstants.APPLICATION_INSIGHTS_CUSTOM_EVENT_KEY; +import static com.azure.spring.cloud.feature.management.telemetry.TelemetryConstants.DEFAULT_WHEN_ENABLED; +import static com.azure.spring.cloud.feature.management.telemetry.TelemetryConstants.ENABLED; +import static com.azure.spring.cloud.feature.management.telemetry.TelemetryConstants.EVALUATION_EVENT_VERSION; +import static com.azure.spring.cloud.feature.management.telemetry.TelemetryConstants.EVENT_NAME; +import static com.azure.spring.cloud.feature.management.telemetry.TelemetryConstants.FEATURE_NAME; +import static com.azure.spring.cloud.feature.management.telemetry.TelemetryConstants.REASON; +import static com.azure.spring.cloud.feature.management.telemetry.TelemetryConstants.VARIANT; +import static com.azure.spring.cloud.feature.management.telemetry.TelemetryConstants.VARIANT_ASSIGNMENT_PERCENTAGE; +import static com.azure.spring.cloud.feature.management.telemetry.TelemetryConstants.VERSION; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.mockito.Mockito.when; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.slf4j.LoggerFactory; + +import com.azure.spring.cloud.feature.management.models.Allocation; +import com.azure.spring.cloud.feature.management.models.EvaluationEvent; +import com.azure.spring.cloud.feature.management.models.Feature; +import com.azure.spring.cloud.feature.management.models.PercentileAllocation; +import com.azure.spring.cloud.feature.management.models.Variant; +import com.azure.spring.cloud.feature.management.models.VariantAssignmentReason; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.read.ListAppender; + +@TestMethodOrder(MethodOrderer.MethodName.class) +public class LoggerTelemetryPublisherTest { + + private Logger publisherLogger; + + private ListAppender listAppender; + + @Mock + private Feature featureMock; + + @Mock + private Variant variantMock; + + private ILoggingEvent logEvent; + + @BeforeAll + public static void setUpLogging() { + // Force SLF4J to initialize + LoggerFactory.getLogger(LoggerTelemetryPublisherTest.class).info("Initializing SLF4J in test"); + } + + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + + org.slf4j.Logger slf4jLogger = LoggerFactory.getLogger(LoggerTelemetryPublisher.class); + + // Check if we can cast to Logback's Logger + if (slf4jLogger instanceof ch.qos.logback.classic.Logger) { + publisherLogger = (ch.qos.logback.classic.Logger) slf4jLogger; + + // Create a new ListAppender for each test + listAppender = new ListAppender<>(); + listAppender.start(); + + // Remove any existing appenders of this type first + for (Iterator> it = publisherLogger.iteratorForAppenders(); it.hasNext();) { + Appender appender = it.next(); + if (appender instanceof ListAppender) { + publisherLogger.detachAppender(appender); + } + } + + // Add the fresh appender + publisherLogger.addAppender(listAppender); + } else { + assumeTrue( + false, + "Tests require Logback implementation, but found: " + slf4jLogger.getClass().getName() + ); + } + } + + @Test + void featureFlagTest(TestInfo testInfo) throws Exception { + when(featureMock.getId()).thenReturn(testInfo.getDisplayName()); + EvaluationEvent evaluationEvent = new EvaluationEvent(featureMock); + + LoggerTelemetryPublisher publisher = new LoggerTelemetryPublisher(); + publisher.publishTelemetry(evaluationEvent); + + logEvent = getEvent(listAppender.list, testInfo.getDisplayName()); + Map mdcMap = logEvent.getMDCPropertyMap(); + + assertEquals(EVENT_NAME, logEvent.getMessage()); + assertEquals(Level.INFO, logEvent.getLevel()); + assertEquals("None", mdcMap.get(REASON)); + assertEquals(testInfo.getDisplayName(), mdcMap.get(FEATURE_NAME)); + assertEquals("false", mdcMap.get(ENABLED)); + assertEquals(EVALUATION_EVENT_VERSION, mdcMap.get(VERSION)); + assertEquals(EVENT_NAME, mdcMap.get(APPLICATION_INSIGHTS_CUSTOM_EVENT_KEY)); + } + + @Test + void featureVariantTest(TestInfo testInfo) throws Exception { + when(featureMock.getId()).thenReturn(testInfo.getDisplayName()); + EvaluationEvent evaluationEvent = new EvaluationEvent(featureMock); + evaluationEvent.setVariant(variantMock); + evaluationEvent.setReason(VariantAssignmentReason.DEFAULT_WHEN_ENABLED); + + when(variantMock.getName()).thenReturn("fake-variant"); + + LoggerTelemetryPublisher publisher = new LoggerTelemetryPublisher(); + publisher.publishTelemetry(evaluationEvent); + + logEvent = getEvent(listAppender.list, testInfo.getDisplayName()); + Map mdcMap = logEvent.getMDCPropertyMap(); + + assertEquals(EVENT_NAME, logEvent.getMessage()); + assertEquals(Level.INFO, logEvent.getLevel()); + assertEquals(DEFAULT_WHEN_ENABLED, mdcMap.get(REASON)); + assertEquals(testInfo.getDisplayName(), mdcMap.get(FEATURE_NAME)); + assertEquals("false", mdcMap.get(ENABLED)); + assertEquals(EVALUATION_EVENT_VERSION, mdcMap.get(VERSION)); + assertEquals(EVENT_NAME, mdcMap.get(APPLICATION_INSIGHTS_CUSTOM_EVENT_KEY)); + assertEquals("fake-variant", mdcMap.get(VARIANT)); + assertEquals("100", mdcMap.get(VARIANT_ASSIGNMENT_PERCENTAGE)); + } + + @Test + void featureFlagDisabledTest(TestInfo testInfo) throws Exception { + when(featureMock.getId()).thenReturn(testInfo.getDisplayName()); + when(featureMock.isEnabled()).thenReturn(false); + EvaluationEvent evaluationEvent = new EvaluationEvent(featureMock); + + LoggerTelemetryPublisher publisher = new LoggerTelemetryPublisher(); + publisher.publishTelemetry(evaluationEvent); + + logEvent = getEvent(listAppender.list, testInfo.getDisplayName()); + Map mdcMap = logEvent.getMDCPropertyMap(); + + assertEquals(EVENT_NAME, logEvent.getMessage()); + assertEquals(Level.INFO, logEvent.getLevel()); + assertEquals("None", mdcMap.get(REASON)); + assertEquals(testInfo.getDisplayName(), mdcMap.get(FEATURE_NAME)); + assertEquals("false", mdcMap.get(ENABLED)); + assertEquals(EVALUATION_EVENT_VERSION, mdcMap.get(VERSION)); + assertEquals(EVENT_NAME, mdcMap.get(APPLICATION_INSIGHTS_CUSTOM_EVENT_KEY)); + } + + @Test + void featureVariantWithPercentageTest(TestInfo testInfo) throws Exception { + when(featureMock.getId()).thenReturn(testInfo.getDisplayName()); + EvaluationEvent evaluationEvent = new EvaluationEvent(featureMock); + evaluationEvent.setVariant(variantMock); + evaluationEvent.setReason(VariantAssignmentReason.PERCENTILE); + + PercentileAllocation fakeVariant1 = new PercentileAllocation().setVariant("fake-variant").setFrom((double) 0) + .setTo((double) 75); + PercentileAllocation fakeVariant2 = new PercentileAllocation().setVariant("fake-variant2").setFrom((double) 76) + .setTo((double) 100); + + when(featureMock.getAllocation()).thenReturn(new Allocation().setPercentile(List.of(fakeVariant1, fakeVariant2)) + .setDefaultWhenEnabled("defaultVariant")); + when(variantMock.getName()).thenReturn("fake-variant"); + + LoggerTelemetryPublisher publisher = new LoggerTelemetryPublisher(); + publisher.publishTelemetry(evaluationEvent); + + logEvent = getEvent(listAppender.list, testInfo.getDisplayName()); + Map mdcMap = logEvent.getMDCPropertyMap(); + + assertEquals(EVENT_NAME, logEvent.getMessage()); + assertEquals(Level.INFO, logEvent.getLevel()); + assertEquals("Percentile", mdcMap.get(REASON)); + assertEquals(testInfo.getDisplayName(), mdcMap.get(FEATURE_NAME)); + assertEquals("false", mdcMap.get(ENABLED)); + assertEquals(EVALUATION_EVENT_VERSION, mdcMap.get(VERSION)); + assertEquals(EVENT_NAME, mdcMap.get(APPLICATION_INSIGHTS_CUSTOM_EVENT_KEY)); + assertEquals("fake-variant", mdcMap.get(VARIANT)); + assertEquals("75.0", mdcMap.get(VARIANT_ASSIGNMENT_PERCENTAGE)); + assertEquals("defaultVariant", mdcMap.get(DEFAULT_WHEN_ENABLED)); + } + + @Test + void nullEvaluationEventTest(TestInfo testInfo) { + when(featureMock.getId()).thenReturn(testInfo.getDisplayName()); + LoggerTelemetryPublisher publisher = new LoggerTelemetryPublisher(); + publisher.publishTelemetry(null); + + // Ensure no logs are generated + assertEquals(0, listAppender.list.size()); + } + + @Test + void nullFeatureInEvaluationEventTest(TestInfo testInfo) { + when(featureMock.getId()).thenReturn(testInfo.getDisplayName()); + EvaluationEvent evaluationEvent = new EvaluationEvent(null); + + LoggerTelemetryPublisher publisher = new LoggerTelemetryPublisher(); + publisher.publishTelemetry(evaluationEvent); + + // Ensure no logs are generated + assertEquals(0, listAppender.list.size()); + } + + @Test + void nullVariantInEvaluationEventTest(TestInfo testInfo) { + when(featureMock.getId()).thenReturn(testInfo.getDisplayName()); + EvaluationEvent evaluationEvent = new EvaluationEvent(featureMock); + evaluationEvent.setVariant(null); + evaluationEvent.setReason(VariantAssignmentReason.DEFAULT_WHEN_ENABLED); + + LoggerTelemetryPublisher publisher = new LoggerTelemetryPublisher(); + publisher.publishTelemetry(evaluationEvent); + + logEvent = getEvent(listAppender.list, testInfo.getDisplayName()); + Map mdcMap = logEvent.getMDCPropertyMap(); + + assertEquals(EVENT_NAME, logEvent.getMessage()); + assertEquals(Level.INFO, logEvent.getLevel()); + assertEquals(DEFAULT_WHEN_ENABLED, mdcMap.get(REASON)); + assertEquals(testInfo.getDisplayName(), mdcMap.get(FEATURE_NAME)); + assertEquals("false", mdcMap.get(ENABLED)); + assertEquals(EVALUATION_EVENT_VERSION, mdcMap.get(VERSION)); + assertEquals(EVENT_NAME, mdcMap.get(APPLICATION_INSIGHTS_CUSTOM_EVENT_KEY)); + assertEquals(null, mdcMap.get(VARIANT)); + assertEquals("100", mdcMap.get(VARIANT_ASSIGNMENT_PERCENTAGE)); + } + + @Test + void emptyPercentileAllocationTest(TestInfo testInfo) { + when(featureMock.getId()).thenReturn(testInfo.getDisplayName()); + EvaluationEvent evaluationEvent = new EvaluationEvent(featureMock); + evaluationEvent.setVariant(variantMock); + evaluationEvent.setReason(VariantAssignmentReason.PERCENTILE); + + when(featureMock.getAllocation()).thenReturn(new Allocation().setPercentile(List.of())); + when(variantMock.getName()).thenReturn("fake-variant"); + + LoggerTelemetryPublisher publisher = new LoggerTelemetryPublisher(); + publisher.publishTelemetry(evaluationEvent); + + logEvent = getEvent(listAppender.list, testInfo.getDisplayName()); + Map mdcMap = logEvent.getMDCPropertyMap(); + + assertEquals(EVENT_NAME, logEvent.getMessage()); + assertEquals(Level.INFO, logEvent.getLevel()); + assertEquals("Percentile", mdcMap.get(REASON)); + assertEquals(testInfo.getDisplayName(), mdcMap.get(FEATURE_NAME)); + assertEquals("false", mdcMap.get(ENABLED)); + assertEquals(EVALUATION_EVENT_VERSION, mdcMap.get(VERSION)); + assertEquals(EVENT_NAME, mdcMap.get(APPLICATION_INSIGHTS_CUSTOM_EVENT_KEY)); + assertEquals("fake-variant", mdcMap.get(VARIANT)); + assertEquals("0.0", mdcMap.get(VARIANT_ASSIGNMENT_PERCENTAGE)); + } + + ILoggingEvent getEvent(List evnets, String featureName) { + for (ILoggingEvent event : evnets) { + if (featureName.equals(event.getMDCPropertyMap().get(FEATURE_NAME))) { + return event; + } + } + assumeTrue( + false, + "Log event not found for feature: " + featureName + ); + return null; // This line will never be reached due to the assumption above + } +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/resources/logback-test.xml b/sdk/spring/spring-cloud-azure-feature-management/src/test/resources/logback-test.xml new file mode 100644 index 000000000000..4490745c459d --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + \ No newline at end of file From a52639d6f8ac96e338f235b91e7643710d8ed86e Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 6 May 2025 17:09:09 -0700 Subject: [PATCH 13/41] Update external_dependencies.txt --- eng/versioning/external_dependencies.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index 2feadc7feeb9..16d6c3e33fc4 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -339,6 +339,7 @@ springboot3_io.netty:netty-transport-native-kqueue;4.1.119.Final springboot3_io.netty:netty-transport;4.1.119.Final springboot3_io.projectreactor.netty:reactor-netty;1.2.4 springboot3_io.projectreactor:reactor-test;3.7.4 +springboot3_jakarta.annotations:jakarta.annotation-api;3.0.0 springboot3_jakarta.servlet:jakarta.servlet-api;6.0.0 springboot3_jakarta.validation:jakarta.validation-api;3.0.2 springboot3_javax.annotation:javax.annotation-api;1.3.2 From aecbc400cf0d147a5625e04530ccc7cc903abdd9 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 16 May 2025 10:35:01 -0700 Subject: [PATCH 14/41] fixing external dependencies --- eng/versioning/external_dependencies.txt | 1 + sdk/spring/spring-cloud-azure-feature-management/pom.xml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index 16d6c3e33fc4..5fb9f6c32269 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -412,6 +412,7 @@ 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_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-feature-management/pom.xml b/sdk/spring/spring-cloud-azure-feature-management/pom.xml index abdc8e70d779..89d421774cdd 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/pom.xml +++ b/sdk/spring/spring-cloud-azure-feature-management/pom.xml @@ -93,7 +93,7 @@ ch.qos.logback logback-classic - 1.5.18 + 1.5.18 test From 61e2f5cfdc6a5bdde9fbdbda5413775f2d8b081d Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 16 May 2025 11:28:19 -0700 Subject: [PATCH 15/41] Update external_dependencies.txt --- eng/versioning/external_dependencies.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index 5fb9f6c32269..43470ab6ad55 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -412,6 +412,7 @@ 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 From 6090a6a44e81f05704eb2b0b9dc6d7d8e5e03bb9 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 16 May 2025 11:29:32 -0700 Subject: [PATCH 16/41] Update external_dependencies.txt --- eng/versioning/external_dependencies.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index 43470ab6ad55..74d93e18e17c 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -339,7 +339,6 @@ springboot3_io.netty:netty-transport-native-kqueue;4.1.119.Final springboot3_io.netty:netty-transport;4.1.119.Final springboot3_io.projectreactor.netty:reactor-netty;1.2.4 springboot3_io.projectreactor:reactor-test;3.7.4 -springboot3_jakarta.annotations:jakarta.annotation-api;3.0.0 springboot3_jakarta.servlet:jakarta.servlet-api;6.0.0 springboot3_jakarta.validation:jakarta.validation-api;3.0.2 springboot3_javax.annotation:javax.annotation-api;1.3.2 From 641c527c52f8002a20c4483818b7a4750eae94ff Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 20 May 2025 14:59:11 -0700 Subject: [PATCH 17/41] pom update --- .../spring-cloud-azure-appconfiguration-config-web/pom.xml | 4 ++-- .../spring-cloud-azure-appconfiguration-config/pom.xml | 2 +- .../spring-cloud-azure-feature-management-web/pom.xml | 4 ++-- sdk/spring/spring-cloud-azure-feature-management/pom.xml | 2 +- .../pom.xml | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml index 9dc5308d38cd..9626c0119688 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml @@ -9,7 +9,7 @@ 4.0.0 com.azure.spring spring-cloud-azure-appconfiguration-config-web - 5.23.0-beta.1 + 6.0.0-beta.1 Spring Cloud Azure App Configuration Config Web Integration of Spring Cloud Config and Azure App Configuration Service @@ -21,7 +21,7 @@ com.azure.spring spring-cloud-azure-appconfiguration-config - 5.23.0-beta.1 + 6.0.0-beta.1 org.springframework.boot diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index bf2a8d518612..b59931eafbff 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -9,7 +9,7 @@ 4.0.0 com.azure.spring spring-cloud-azure-appconfiguration-config - 5.23.0-beta.1 + 6.0.0-beta.1 Spring Cloud Azure App Configuration Config Integration of Spring Cloud Config and Azure App Configuration Service diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/pom.xml b/sdk/spring/spring-cloud-azure-feature-management-web/pom.xml index 2d51c6b9ad24..ae5a7f35ac69 100644 --- a/sdk/spring/spring-cloud-azure-feature-management-web/pom.xml +++ b/sdk/spring/spring-cloud-azure-feature-management-web/pom.xml @@ -8,7 +8,7 @@ 4.0.0 com.azure.spring spring-cloud-azure-feature-management-web - 5.23.0-beta.1 + 6.0.0-beta.1 Spring Cloud Azure Feature Management Web Adds Feature Management into Spring Web @@ -39,7 +39,7 @@ com.azure.spring spring-cloud-azure-feature-management - 5.23.0-beta.1 + 6.0.0-beta.1 jakarta.servlet diff --git a/sdk/spring/spring-cloud-azure-feature-management/pom.xml b/sdk/spring/spring-cloud-azure-feature-management/pom.xml index 89d421774cdd..471c20e9ee36 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/pom.xml +++ b/sdk/spring/spring-cloud-azure-feature-management/pom.xml @@ -8,7 +8,7 @@ 4.0.0 com.azure.spring spring-cloud-azure-feature-management - 5.23.0-beta.1 + 6.0.0-beta.1 Spring Cloud Azure Feature Management Adds Feature Management into Spring diff --git a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/pom.xml index a6a037ca9e7b..7a92bf6b71e7 100644 --- a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/pom.xml @@ -6,7 +6,7 @@ com.azure.spring spring-cloud-azure-starter-appconfiguration-config - 5.23.0-beta.1 + 6.0.0-beta.1 Spring Cloud Azure Starter App Configuration Config Spring Cloud Azure Starter App Configuration Config https://microsoft.github.io/spring-cloud-azure @@ -88,12 +88,12 @@ com.azure.spring spring-cloud-azure-appconfiguration-config-web - 5.23.0-beta.1 + 6.0.0-beta.1 com.azure.spring spring-cloud-azure-feature-management-web - 5.23.0-beta.1 + 6.0.0-beta.1 From d6641291fac8f179f146134ab09dae289146821b Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 20 May 2025 15:37:58 -0700 Subject: [PATCH 18/41] Fix Log --- .../azure/spring/cloud/feature/management/FeatureManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java index 775f03f107a7..319aed6c802e 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java @@ -197,7 +197,7 @@ private Mono checkFeature(String featureName, Object featureCon EvaluationEvent event = new EvaluationEvent(featureFlag); if (featureFlag == null) { - LOGGER.warn("Feature flag %s not found", featureName); + LOGGER.warn("Feature flag {} not found", featureName); return Mono.just(event); } From 6fd977943ab8c4e71252e7ca3be18e088b94fb00 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 27 May 2025 10:09:05 -0700 Subject: [PATCH 19/41] Fixing some bugs --- ...rationFeatureManagementPropertySource.java | 11 +++-- .../AzureAppConfigDataLoader.java | 7 ++- ...onFeatureManagementPropertySourceTest.java | 4 +- .../feature/management/FeatureManager.java | 14 +++--- .../FeatureManagementProperties.java | 43 ++++++++++++++++++- 5 files changed, 61 insertions(+), 18 deletions(-) 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 186ae9586f14..63efff2e78bf 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 @@ -17,15 +17,14 @@ *

*/ class AppConfigurationFeatureManagementPropertySource extends EnumerablePropertySource { - private final FeatureFlagClient featureFlagLoader; - private static final String FEATURE_MANAGEMENT_KEY = "feature_management"; - - private static final String FEATURE_FLAG_KEY = FEATURE_MANAGEMENT_KEY + ".feature_flags"; + private static final String FEATURE_MANAGEMENT_KEY = "feature-management"; private static final ObjectMapper MAPPER = JsonMapper.builder().build(); + private static final String FEATURE_FLAG_KEY = FEATURE_MANAGEMENT_KEY + ".feature-flags"; + AppConfigurationFeatureManagementPropertySource(FeatureFlagClient featureFlagLoader) { super(FEATURE_MANAGEMENT_KEY, featureFlagLoader); this.featureFlagLoader = featureFlagLoader; @@ -33,8 +32,8 @@ class AppConfigurationFeatureManagementPropertySource extends EnumerableProperty @Override public String[] getPropertyNames() { - if (featureFlagLoader != null && featureFlagLoader.getFeatureFlags().size() > 0) { - return new String[]{ FEATURE_FLAG_KEY }; + if (featureFlagLoader != null && !featureFlagLoader.getFeatureFlags().isEmpty()) { + return new String[] { FEATURE_FLAG_KEY }; } return new String[0]; } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java index 9386a2bab524..c018efb0566d 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java @@ -131,7 +131,7 @@ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResour } catch (Exception e) { failedToGeneratePropertySource(e); - // Not a retiable error + // Not a retriable error break; } if (sourceList.size() > 0) { @@ -141,7 +141,10 @@ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResour } StateHolder.updateState(storeState); - sourceList.add(new AppConfigurationFeatureManagementPropertySource(featureFlagClient)); + if (featureFlagClient.getFeatureFlags().size() > 0) { + // Don't add feature flags if there are none, otherwise the local file can't load them. + sourceList.add(new AppConfigurationFeatureManagementPropertySource(featureFlagClient)); + } return new ConfigData(sourceList); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationFeatureManagementPropertySourceTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationFeatureManagementPropertySourceTest.java index 457ff57d8d99..27eea55bd83a 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationFeatureManagementPropertySourceTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationFeatureManagementPropertySourceTest.java @@ -26,7 +26,7 @@ public class AppConfigurationFeatureManagementPropertySourceTest { @Mock private FeatureFlagClient featureFlagLoaderMock; - private static final String FEATURE_FLAG_KEY = "feature_management.feature_flags"; + private static final String FEATURE_FLAG_KEY = "feature-management.feature-flags"; private MockitoSession session; @@ -61,7 +61,7 @@ public void getPropertyTest() { featureFlagLoaderMock); assertNull(featureManagementPropertySource.getProperty("NotFeatureFlagProperty")); - when(featureFlagLoaderMock.getFeatureFlags()).thenReturn(List.of()); + when(featureFlagLoaderMock.getFeatureFlags()).thenReturn(List.of(new Feature())); assertNotNull(featureManagementPropertySource.getProperty(FEATURE_FLAG_KEY)); } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java index 319aed6c802e..20e965d4e3ea 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java @@ -64,7 +64,7 @@ public class FeatureManager { private final TargetingContextAccessor contextAccessor; private final TargetingEvaluationOptions evaluationOptions; - + private final TelemetryPublisher telemetryPublisher; /** @@ -186,11 +186,11 @@ public Mono getVariantAsync(String feature, Object featureContext) { private Mono checkFeature(String featureName, Object featureContext) throws FilterNotFoundException { List featureFlags = featureManagementConfigurations.getFeatureFlags(); - + if (featureFlags == null) { return Mono.just(new EvaluationEvent(null)); } - + Feature featureFlag = featureFlags.stream() .filter(feature -> feature.getId().equals(featureName)).findAny().orElse(null); @@ -204,7 +204,8 @@ private Mono checkFeature(String featureName, Object featureCon if (!featureFlag.isEnabled()) { this.assignDefaultDisabledReason(event); event.setEnabled(false); - if (telemetryPublisher != null && featureFlag.getTelemetry().isEnabled()) { + if (telemetryPublisher != null && featureFlag.getTelemetry() != null + && featureFlag.getTelemetry().isEnabled()) { telemetryPublisher.publishTelemetry(event); } @@ -216,9 +217,10 @@ private Mono checkFeature(String featureName, Object featureCon result = assignAllocation(result); result = result.doOnSuccess(resultEvent -> { - if (telemetryPublisher != null && featureFlag.getTelemetry().isEnabled()) { + if (telemetryPublisher != null && featureFlag.getTelemetry() != null + && featureFlag.getTelemetry().isEnabled()) { telemetryPublisher.publishTelemetry(resultEvent); - } + } }); return result; } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementProperties.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementProperties.java index fbb4d4733d64..70476797637c 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementProperties.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementProperties.java @@ -2,12 +2,14 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.management.implementation; +import java.util.ArrayList; import java.util.List; import org.springframework.boot.context.properties.ConfigurationProperties; import com.azure.spring.cloud.feature.management.models.Feature; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; /** * Configuration Properties for Feature Management. Processes the configurations to be usable by Feature Management. @@ -15,15 +17,52 @@ @ConfigurationProperties(prefix = "feature-management") public class FeatureManagementProperties { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + @JsonProperty("feature-flags") private List featureFlags; + //private List convertedFeatureFlags; + public List getFeatureFlags() { return featureFlags; } - + + /** + * Sets the feature flags from the configuration. This method handles conversion from various input formats. + * + * @param featureFlags the feature flags to set + */ public void setFeatureFlags(List featureFlags) { - this.featureFlags = featureFlags; + if (featureFlags == null || featureFlags.isEmpty()) { + System.out.println("No feature flags provided, setting empty list"); + this.featureFlags = List.of(); + return; + } + + // Check if the first element is a Feature instance to determine if we need conversion + if (featureFlags.get(0) instanceof Feature) { + List features = new ArrayList<>(); + for (Object flag : featureFlags) { + // This should always be true based on our check, but we verify each element to be safe + if (flag instanceof Feature) { + features.add((Feature) flag); + } + } + + if (!features.isEmpty()) { + this.featureFlags = features; + return; + } + } + + // Convert the feature flags to the correct type + List convertedFlags = new ArrayList<>(); + for (Object feature : featureFlags) { + Feature convertedFeature = OBJECT_MAPPER.convertValue(feature, Feature.class); + convertedFlags.add(convertedFeature); + } + this.featureFlags = convertedFlags; } } From cee03055902aab0e49901a8651e50b061ae96314 Mon Sep 17 00:00:00 2001 From: Matthew Metcalf Date: Tue, 3 Jun 2025 09:06:48 -0700 Subject: [PATCH 20/41] Adding missing java docs (#45551) --- ...AppConfigurationKeyVaultClientFactory.java | 44 +++++- .../AppConfigurationPropertySource.java | 36 ++++- .../AppConfigurationPullRefresh.java | 75 +++++++--- .../AppConfigurationRefreshUtil.java | 135 ++++++++++++++---- .../AppConfigurationReplicaClient.java | 37 ++++- .../AppConfigurationReplicaClientFactory.java | 60 +++++--- .../AzureAppConfigDataLoader.java | 67 +++++++-- .../AzureAppConfigDataLocationResolver.java | 39 +++++ .../AzureAppConfigDataResource.java | 94 ++++++------ .../implementation/BackoffTimeCalculator.java | 18 ++- .../implementation/ConnectionManager.java | 65 ++++++--- .../feature/management/FeatureManager.java | 3 +- .../implementation/FeatureFilterUtils.java | 9 +- 13 files changed, 526 insertions(+), 156 deletions(-) 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 67f9e17256b0..14882dd17047 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,20 +10,49 @@ import com.azure.spring.cloud.appconfiguration.config.implementation.stores.AppConfigurationSecretClientManager; import com.azure.spring.cloud.service.implementation.keyvault.secrets.SecretClientBuilderFactory; +/** + * Factory for creating and managing AppConfigurationSecretClientManager instances. This class caches clients per Key + * Vault host. + */ class AppConfigurationKeyVaultClientFactory { + /** + * Cache of secret client managers by Key Vault host. private final Map keyVaultClients; + /** + * Optional customizer for Key Vault secret clients. + */ private final SecretClientCustomizer keyVaultClientProvider; + /** + * Optional provider for custom secret resolution. + */ private final KeyVaultSecretProvider keyVaultSecretProvider; + /** + * Factory for creating secret client builders. + */ private final SecretClientBuilderFactory secretClientFactory; - + + /** + * Flag indicating whether credentials are configured. + */ private final boolean credentialsConfigured; + /** + * Flag indicating whether the factory being used for telemetry. + */ private final boolean isConfigured; + /** + * Creates a new AppConfigurationKeyVaultClientFactory. + * + * @param keyVaultClientProvider optional customizer for Key Vault secret clients + * @param keyVaultSecretProvider optional provider for custom secret resolution + * @param secretClientFactory factory for creating secret client builders + * @param credentialsConfigured whether credentials are configured + */ AppConfigurationKeyVaultClientFactory(SecretClientCustomizer keyVaultClientProvider, KeyVaultSecretProvider keyVaultSecretProvider, SecretClientBuilderFactory secretClientFactory, boolean credentialsConfigured) { @@ -35,6 +64,12 @@ class AppConfigurationKeyVaultClientFactory { isConfigured = keyVaultClientProvider != null || credentialsConfigured; } + /** + * Gets or creates a secret client manager for the specified Key Vault host. + * + * @param host the Key Vault host endpoint + * @return the secret client manager for the host + */ AppConfigurationSecretClientManager getClient(String host) { // Check if we already have a client for this key vault, if not we will make // one @@ -46,7 +81,12 @@ AppConfigurationSecretClientManager getClient(String host) { return keyVaultClients.get(host); } - public boolean isConfigured() { + /** + * Returns if Key Vault is configured to be used. + * + * @return true if either a client provider is configured or credentials are configured + */ + boolean isConfigured() { return isConfigured; } } 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 9515c30258a9..c2ce5df5e3f0 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 @@ -11,22 +11,31 @@ import org.springframework.core.env.EnumerablePropertySource; import com.azure.core.util.Context; -import com.azure.data.appconfiguration.ConfigurationClient; /** - * Azure App Configuration PropertySource unique per Store Label(Profile) combo. - * + * Abstract base class for Azure App Configuration PropertySource implementations. + * *

- * i.e. If connecting to 2 stores and have 2 labels set 4 AppConfigurationPropertySources need to be - * created. + * Each PropertySource is unique per Store-Label(Profile) combination. For example, if connecting to 2 stores with 2 + * labels each, 4 AppConfigurationPropertySources need to be created. *

*/ -abstract class AppConfigurationPropertySource extends EnumerablePropertySource { +abstract class AppConfigurationPropertySource extends EnumerablePropertySource { + /** protected final Map properties = new LinkedHashMap<>(); + /** + * Client for communicating with Azure App Configuration service. + */ protected final AppConfigurationReplicaClient replicaClient; + /** + * Creates a new AppConfigurationPropertySource. + * + * @param name the name of this property source, should be unique to identify the store-label combination + * @param replicaClient the client for communicating with Azure App Configuration + */ AppConfigurationPropertySource(String name, AppConfigurationReplicaClient replicaClient) { // The context alone does not uniquely define a PropertySource, append storeName // and label to uniquely define a PropertySource @@ -34,17 +43,32 @@ abstract class AppConfigurationPropertySource extends EnumerablePropertySource keySet = properties.keySet(); return keySet.toArray(new String[keySet.size()]); } + /** + * Returns the value of the specified property. + * + */ @Override public Object getProperty(String name) { return properties.get(name); } + /** + * Creates a comma-separated string from the given label filters. + * + * @param labelFilters array of label filters, may be null + * @return comma-separated string of labels, or empty string if null/empty + */ protected static String getLabelName(String[] labelFilters) { if (labelFilters == null) { return ""; 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 c45799b10629..cb5d5735d39b 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 @@ -20,32 +20,56 @@ import reactor.core.publisher.Mono; /** - * Enables checking of Configuration updates. + * Component responsible for checking Azure App Configuration for updates and triggering refresh events. */ @Component public class AppConfigurationPullRefresh implements AppConfigurationRefresh { private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationPullRefresh.class); + /** + * Flag to prevent concurrent refresh operations. + */ private final AtomicBoolean running = new AtomicBoolean(false); + /** + * Publisher for Spring refresh events. + */ private ApplicationEventPublisher publisher; - private final Long defaultMinBackoff = (long) 30; + /** + * Default minimum backoff duration in seconds when refresh operations fail. + */ + private static final Long DEFAULT_MIN_BACKOFF_SECONDS = 30L; + + /** + * Factory for creating App Configuration replica clients. + */ private final AppConfigurationReplicaClientFactory clientFactory; + /** + * Time interval between configuration refresh checks. + */ private final Duration refreshInterval; - + + /** + * Component for replica lookup and failover functionality. + */ private final ReplicaLookUp replicaLookUp; - + + /** + * Utility component for refresh operations. + */ private final AppConfigurationRefreshUtil refreshUtils; /** - * Component used for checking for and triggering configuration refreshes. + * Creates a new AppConfigurationPullRefresh component. * - * @param clientFactory Clients stores used to connect to App Configuration. * @param defaultMinBackoff default - * @param refreshInterval time between refresh intervals + * @param clientFactory factory for creating App Configuration clients to connect to stores + * @param refreshInterval time duration between refresh interval checks + * @param replicaLookUp component for handling replica lookup and failover + * @param refreshUtils utility component for refresh operations */ public AppConfigurationPullRefresh(AppConfigurationReplicaClientFactory clientFactory, Duration refreshInterval, ReplicaLookUp replicaLookUp, AppConfigurationRefreshUtil refreshUtils) { @@ -53,20 +77,24 @@ public AppConfigurationPullRefresh(AppConfigurationReplicaClientFactory clientFa this.clientFactory = clientFactory; this.replicaLookUp = replicaLookUp; this.refreshUtils = refreshUtils; - } + /** + * Sets the Spring application event publisher for publishing refresh events. + * + * @param applicationEventPublisher the Spring event publisher to use for refresh events + */ @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.publisher = applicationEventPublisher; } /** - * Checks configurations to see if configurations should be reloaded. If the refresh interval has passed and a - * trigger has been updated configuration are reloaded. - * - * @return Future with a boolean of if a RefreshEvent was published. If refreshConfigurations is currently being run - * elsewhere this method will return right away as false. + * Checks configurations to see if they should be reloaded. If the refresh interval has passed and a trigger has + * been updated, configurations are reloaded. + * + * @return a Mono containing a boolean indicating if a RefreshEvent was published. Returns {@code false} if + * refreshConfigurations is currently being executed elsewhere. */ public Mono refreshConfigurations() { return Mono.just(refreshStores()); @@ -74,9 +102,10 @@ public Mono refreshConfigurations() { /** * Soft expires refresh interval. Sets amount of time to next refresh to be a random value between 0 and 15 seconds, - * unless value is less than the amount of time to the next refresh check. - * @param endpoint Config Store endpoint to expire refresh interval on. - * @param syncToken syncToken to verify the latest changes are available on pull + * unless that value is less than the amount of time to the next refresh check. + * + * @param endpoint the Config Store endpoint to expire refresh interval on + * @param syncToken the syncToken to verify the latest changes are available on pull */ public void expireRefreshInterval(String endpoint, String syncToken) { LOGGER.debug("Expiring refresh interval for " + endpoint); @@ -90,22 +119,23 @@ public void expireRefreshInterval(String endpoint, String syncToken) { /** * Goes through each config store and checks if any of its keys need to be refreshed. If any store has a value that - * needs to be updated a refresh event is called after every store is checked. + * needs to be updated, a refresh event is called after every store is checked. * - * @return If a refresh event is called. + * @return true if a refresh event is published, false otherwise */ private boolean refreshStores() { if (running.compareAndSet(false, true)) { try { RefreshEventData eventData = refreshUtils.refreshStoresCheck(clientFactory, - refreshInterval, defaultMinBackoff, replicaLookUp); + refreshInterval, DEFAULT_MIN_BACKOFF_SECONDS, replicaLookUp); if (eventData.getDoRefresh()) { publisher.publishEvent(new RefreshEvent(this, eventData, eventData.getMessage())); return true; } } catch (Exception e) { + LOGGER.warn("Error occurred during configuration refresh, will retry at next interval", e); // The next refresh will happen sooner if refresh interval is expired. - StateHolder.getCurrentState().updateNextRefreshTime(refreshInterval, defaultMinBackoff); + StateHolder.getCurrentState().updateNextRefreshTime(refreshInterval, DEFAULT_MIN_BACKOFF_SECONDS); throw e; } finally { running.set(false); @@ -114,6 +144,11 @@ private boolean refreshStores() { return false; } + /** + * Gets the health status of all configured App Configuration stores. + * + * @return a map containing the health status of each store, keyed by store identifier + */ @Override public Map getAppConfigurationStoresHealth() { return clientFactory.getHealth(); 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 a5337b08e43a..bca843fdfe2c 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 @@ -3,6 +3,7 @@ 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; @@ -22,15 +23,21 @@ import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring.PushNotification; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagStore; +/** + * Utility class for handling Azure App Configuration refresh operations. + */ public class AppConfigurationRefreshUtil { - private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationPullRefresh.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationRefreshUtil.class); /** - * Goes through each config store and checks if any of its keys need to be refreshed. If any store has a value that - * needs to be updated a refresh event is called after every store is checked. + * Checks all configured stores to determine if any configurations need to be refreshed. * - * @return If a refresh event is called. + * @param clientFactory factory for accessing App Configuration clients + * @param refreshInterval the time interval between refresh checks + * @param defaultMinBackoff the minimum backoff time for failed operations + * @param replicaLookUp component for handling replica failover + * @return RefreshEventData containing information about whether a refresh should occur */ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientFactory, Duration refreshInterval, Long defaultMinBackoff, ReplicaLookUp replicaLookUp) { @@ -78,9 +85,11 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF break; } catch (HttpResponseException e) { LOGGER.warn( - "Failed attempting to connect to " + client.getEndpoint() + " during refresh check."); + "Failed to connect to App Configuration store {} during configuration refresh check. " + + "Status: {}, Message: {}", + client.getEndpoint(), e.getResponse().getStatusCode(), e.getMessage()); - clientFactory.backoffClientClient(originEndpoint, client.getEndpoint()); + clientFactory.backoffClient(originEndpoint, client.getEndpoint()); } } } else { @@ -102,9 +111,11 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF break; } catch (HttpResponseException e) { LOGGER.warn( - "Failed attempting to connect to " + client.getEndpoint() + " during refresh check."); + "Failed to connect to App Configuration store {} during feature flag refresh check. " + + "Status: {}, Message: {}", + client.getEndpoint(), e.getResponse().getStatusCode(), e.getMessage()); - clientFactory.backoffClientClient(originEndpoint, client.getEndpoint()); + clientFactory.backoffClient(originEndpoint, client.getEndpoint()); } } } else { @@ -121,11 +132,13 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF } /** - * This is for a refresh fail only. + * Performs a refresh check for a specific store client without time constraints. This method is used for refresh + * failure scenarios only. * - * @param client Client checking for refresh - * @param originEndpoint config store origin endpoint - * @return A refresh should be triggered. + * @param client the client for checking refresh status + * @param originEndpoint the original config store endpoint + * @param context the operation context + * @return true if a refresh should be triggered, false otherwise */ static boolean refreshStoreCheck(AppConfigurationReplicaClient client, String originEndpoint, Context context) { RefreshEventData eventData = new RefreshEventData(); @@ -136,11 +149,13 @@ static boolean refreshStoreCheck(AppConfigurationReplicaClient client, String or } /** - * This is for a refresh fail only. + * Performs a feature flag refresh check for a specific store client. This method is used for refresh failure + * scenarios only. * - * @param featureStore Feature info for the store - * @param client Client checking for refresh - * @return true if a refresh should be triggered. + * @param featureStoreEnabled whether feature store is enabled + * @param client the client for checking refresh status + * @param context the operation context + * @return true if a refresh should be triggered, false otherwise */ static boolean refreshStoreFeatureFlagCheck(Boolean featureStoreEnabled, AppConfigurationReplicaClient client, Context context) { @@ -156,14 +171,20 @@ static boolean refreshStoreFeatureFlagCheck(Boolean featureStoreEnabled, } /** - * Checks refresh trigger for etag changes. If they have changed a RefreshEventData is published. + * Checks configuration refresh triggers for etag changes with time-based validation. Only performs the refresh + * check if the refresh interval has elapsed. * - * @param state The refresh state of the endpoint being checked. - * @param refreshInterval Amount of time to wait until next check of this endpoint. - * @param eventData Info for this refresh event. + * @param client the App Configuration client to use for checking + * @param state the current refresh state of the endpoint being checked + * @param refreshInterval the time duration to wait until next check of this endpoint + * @param eventData the refresh event data to update if changes are detected + * @param replicaLookUp component for updating auto-failover endpoints + * @param context the operation context + * @throws AppConfigurationStatusException if there's an error during the refresh check */ private static void refreshWithTime(AppConfigurationReplicaClient client, State state, Duration refreshInterval, - RefreshEventData eventData, ReplicaLookUp replicaLookUp, Context context) throws AppConfigurationStatusException { + RefreshEventData eventData, ReplicaLookUp replicaLookUp, Context context) + throws AppConfigurationStatusException { if (Instant.now().isAfter(state.getNextRefreshCheck())) { replicaLookUp.updateAutoFailoverEndpoints(); refreshWithoutTime(client, state.getWatchKeys(), eventData, context); @@ -173,11 +194,14 @@ private static void refreshWithTime(AppConfigurationReplicaClient client, State } /** - * Checks refresh trigger for etag changes. If they have changed a RefreshEventData is published. + * Checks configuration refresh triggers for etag changes without time validation. This method immediately checks + * all watch keys for changes regardless of refresh intervals. * - * @param client Client checking for refresh - * @param watchKeys Watch keys for the store. - * @param eventData Refresh event info + * @param client the App Configuration client to use for checking + * @param watchKeys the list of configuration settings to watch for changes + * @param eventData the refresh event data to update if changes are detected + * @param context the operation context + * @throws AppConfigurationStatusException if there's an error during the refresh check */ private static void refreshWithoutTime(AppConfigurationReplicaClient client, List watchKeys, RefreshEventData eventData, Context context) throws AppConfigurationStatusException { @@ -195,6 +219,18 @@ private static void refreshWithoutTime(AppConfigurationReplicaClient client, Lis } } + /** + * Checks feature flag refresh triggers with time-based validation. Only performs the refresh check if the refresh + * interval has elapsed. + * + * @param client the App Configuration client to use for checking + * @param state the current feature flag state of the endpoint being checked + * @param refreshInterval the time duration to wait until next check of this endpoint + * @param eventData the refresh event data to update if changes are detected + * @param replicaLookUp component for updating auto-failover endpoints + * @param context the operation context + * @throws AppConfigurationStatusException if there's an error during the refresh check + */ private static void refreshWithTimeFeatureFlags(AppConfigurationReplicaClient client, FeatureFlagState state, Duration refreshInterval, RefreshEventData eventData, ReplicaLookUp replicaLookUp, Context context) throws AppConfigurationStatusException { @@ -220,6 +256,16 @@ private static void refreshWithTimeFeatureFlags(AppConfigurationReplicaClient cl } } + /** + * Checks feature flag refresh triggers without time validation. This method immediately checks all feature flag + * watch keys for changes regardless of refresh intervals. + * + * @param client the App Configuration client to use for checking + * @param watchKeys the feature flag state containing watch keys to check for changes + * @param eventData the refresh event data to update if changes are detected + * @param context the operation context + * @throws AppConfigurationStatusException if there's an error during the refresh check + */ private static void refreshWithoutTimeFeatureFlags(AppConfigurationReplicaClient client, FeatureFlagState watchKeys, RefreshEventData eventData, Context context) throws AppConfigurationStatusException { @@ -237,13 +283,23 @@ private static void refreshWithoutTimeFeatureFlags(AppConfigurationReplicaClient } } + /** + * Checks the etag values between watched and current configuration settings to determine if a refresh is needed. + * + * @param watchSetting the configuration setting being watched for changes + * @param currentTriggerConfiguration the current configuration setting from the store + * @param endpoint the endpoint of the configuration store + * @param eventData the refresh event data to update if a change is detected + */ private static void checkETag(ConfigurationSetting watchSetting, ConfigurationSetting currentTriggerConfiguration, String endpoint, RefreshEventData eventData) { if (currentTriggerConfiguration == null) { return; } - LOGGER.debug(watchSetting.getETag(), " - ", currentTriggerConfiguration.getETag()); + LOGGER.debug("Comparing eTags - watched: {} vs current: {}", + watchSetting.getETag(), currentTriggerConfiguration.getETag()); + if (watchSetting.getETag() != null && !watchSetting.getETag().equals(currentTriggerConfiguration.getETag())) { LOGGER.trace("Some keys in store [{}] matching the key [{}] and label [{}] is updated, " + "will send refresh event.", endpoint, watchSetting.getKey(), watchSetting.getLabel()); @@ -258,7 +314,7 @@ private static void checkETag(ConfigurationSetting watchSetting, ConfigurationSe } /** - * For each refresh, multiple etags can change, but even one etag is changed, refresh is required. + * Data structure containing information about a refresh event. */ static class RefreshEventData { @@ -268,25 +324,50 @@ static class RefreshEventData { private boolean doRefresh = false; + /** + * Creates a new RefreshEventData with empty message and refresh flag set to false. + */ RefreshEventData() { this.message = ""; } + /** + * Sets the refresh message using the standard message template. + * + * @param prefix the prefix to include in the message (typically a key name) + * @return this RefreshEventData instance for method chaining + */ RefreshEventData setMessage(String prefix) { setFullMessage(String.format(MSG_TEMPLATE, prefix)); return this; } + /** + * Sets the full refresh message and marks that a refresh should occur. + * + * @param message the complete message describing the refresh event + * @return this RefreshEventData instance for method chaining + */ private RefreshEventData setFullMessage(String message) { this.message = message; this.doRefresh = true; return this; } + /** + * Gets the refresh event message. + * + * @return the message describing what triggered the refresh + */ public String getMessage() { return this.message; } + /** + * Indicates whether a refresh should be performed. + * + * @return true if a refresh is needed, false otherwise + */ public boolean getDoRefresh() { return doRefresh; } 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 5c192dc04dbc..ed9d32b4c985 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 @@ -27,8 +27,15 @@ /** * Client for connecting to App Configuration when multiple replicas are in use. + * + * The client automatically retries retryable HTTP errors (429, 408, 5xx) and implements backoff logic to prevent + * overwhelming failing replicas. */ class AppConfigurationReplicaClient { + private static final long INITIAL_BACKOFF_OFFSET_MS = 1L; + + /** HTTP status code for "Not Modified" responses */ + private static final int HTTP_NOT_MODIFIED = 304; private final String endpoint; @@ -49,7 +56,7 @@ class AppConfigurationReplicaClient { this.endpoint = endpoint; this.originClient = originClient; this.client = client; - this.backoffEndTime = Instant.now().minusMillis(1); + this.backoffEndTime = Instant.now().minusMillis(INITIAL_BACKOFF_OFFSET_MS); this.failedAttempts = 0; } @@ -96,6 +103,7 @@ String getOriginClient() { * * @param key String value of the watch key * @param label String value of the watch key, use \0 for null. + * @param context Azure SDK context for request correlation * @return The first returned configuration. */ ConfigurationSetting getWatchKey(String key, String label, Context context) @@ -108,7 +116,7 @@ ConfigurationSetting getWatchKey(String key, String label, Context context) this.failedAttempts = 0; return watchKey; } catch (HttpResponseException e) { - throw hanndleHttpResponseException(e); + throw handleHttpResponseException(e); } catch (UncheckedIOException e) { throw new AppConfigurationStatusException(e.getMessage(), null, null); } @@ -118,7 +126,9 @@ ConfigurationSetting getWatchKey(String key, String label, Context context) * Gets a list of Configuration Settings from the given config store that match the Setting Selector criteria. * * @param settingSelector Information on which setting to pull. i.e. number of results, key value... + * @param context Azure SDK context for request correlation * @return List of Configuration Settings. + * @throws HttpResponseException if the request fails */ List listSettings(SettingSelector settingSelector, Context context) throws HttpResponseException { @@ -132,12 +142,20 @@ List listSettings(SettingSelector settingSelector, Context this.failedAttempts = 0; return configurationSettings; } catch (HttpResponseException e) { - throw hanndleHttpResponseException(e); + throw handleHttpResponseException(e); } catch (UncheckedIOException e) { throw new AppConfigurationStatusException(e.getMessage(), null, null); } } + /** + * Lists feature flags from the Azure App Configuration store. + * + * @param settingSelector selector criteria for feature flags + * @param context Azure SDK context for request correlation + * @return FeatureFlags containing the retrieved feature flags and match conditions + * @throws HttpResponseException if the request fails + */ FeatureFlags listFeatureFlags(SettingSelector settingSelector, Context context) throws HttpResponseException { List configurationSettings = new ArrayList<>(); @@ -157,12 +175,21 @@ FeatureFlags listFeatureFlags(SettingSelector settingSelector, Context context) settingSelector.setMatchConditions(checks); return new FeatureFlags(settingSelector, configurationSettings); } catch (HttpResponseException e) { - throw hanndleHttpResponseException(e); + throw handleHttpResponseException(e); } catch (UncheckedIOException e) { throw new AppConfigurationStatusException(e.getMessage(), null, null); } } + /** + * Lists configuration settings from a specific snapshot. + * + * @param snapshotName the name of the snapshot to retrieve settings from + * @param context Azure SDK context for request correlation + * @return list of configuration settings from the snapshot + * @throws IllegalArgumentException if the snapshot is not of type KEY + * @throws HttpResponseException if the request fails + */ List listSettingSnapshot(String snapshotName, Context context) { List configurationSettings = new ArrayList<>(); try { @@ -178,7 +205,7 @@ List listSettingSnapshot(String snapshotName, Context cont settings.forEach(setting -> configurationSettings.add(NormalizeNull.normalizeNullLabel(setting))); return configurationSettings; } catch (HttpResponseException e) { - throw hanndleHttpResponseException(e); + throw handleHttpResponseException(e); } catch (UncheckedIOException e) { throw new AppConfigurationStatusException(e.getMessage(), null, null); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactory.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactory.java index 1aef5729e617..6c6221db85ca 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 @@ -11,19 +11,22 @@ import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore; /** - * Manages all client connections for all configuration stores. + * Manages all client connections for all configuration stores with support for replica failover. */ public class AppConfigurationReplicaClientFactory { + /** Map of connection managers keyed by origin endpoint */ private static final Map CONNECTIONS = new HashMap<>(); + /** List of configured stores for endpoint resolution */ private final List configStores; /** - * Sets up Connections to all configuration stores. + * Sets up connections to all configuration stores with replica support. * - * @param clientBuilder builder for app configuration replica clients - * @param configStores configuration info for config stores + * @param clientBuilder builder for creating app configuration replica clients + * @param configStores configuration information for all config stores + * @param replicaLookUp service for discovering and managing replica endpoints */ AppConfigurationReplicaClientFactory(AppConfigurationReplicaClientsBuilder clientBuilder, List configStores, ReplicaLookUp replicaLookUp) { @@ -37,42 +40,47 @@ public class AppConfigurationReplicaClientFactory { } /** - * @return the connections + * Gets all connection managers mapped by their origin endpoints. + * + * @return map of endpoint to connection manager */ public Map getConnections() { return CONNECTIONS; } /** - * Returns the current used endpoint for a given config store. - * @param originEndpoint identifier of the store. The identifier is the primary endpoint of the store. - * @return ConfigurationClient for accessing App Configuration + * Returns available replica clients for a given configuration store. + * */ List getAvailableClients(String originEndpoint) { return CONNECTIONS.get(originEndpoint).getAvailableClients(); } /** - * Returns the current used endpoint for a given config store. - * @param originEndpoint identifier of the store. The identifier is the primary endpoint of the store. - * @return ConfigurationClient for accessing App Configuration + * Returns available replica clients for a given configuration store with current client preference. + * + * @param originEndpoint identifier of the store (primary endpoint) + * @param useCurrent whether to prefer the currently active client + * @return list of available replica clients for the store */ List getAvailableClients(String originEndpoint, Boolean useCurrent) { return CONNECTIONS.get(originEndpoint).getAvailableClients(useCurrent); } /** - * Sets backoff time for the current client that is being used, and attempts to get a new one. - * @param originEndpoint identifier of the store. The identifier is the primary endpoint of the store. - * @param endpoint replica endpoint + * Sets backoff time for a specific replica client due to connection failure. + * + * @param originEndpoint identifier of the store (primary endpoint) + * @param endpoint the specific replica endpoint that failed */ void backoffClientClient(String originEndpoint, String endpoint) { CONNECTIONS.get(originEndpoint).backoffClient(endpoint); } /** - * Gets the health of the client connections to App Configuration - * @return map of endpoint origin it's health + * Gets the health status of all managed configuration store connections. + * + * @return map of origin endpoint to health status */ Map getHealth() { Map health = new HashMap<>(); @@ -83,10 +91,10 @@ Map getHealth() { } /** - * Returns the origin endpoint for a given endpoint. If not found will return the given endpoint; + * Finds the origin endpoint for a given replica endpoint. * - * @param endpoint App Configuration Endpoint - * @return String Endpoint + * @param endpoint the replica endpoint to find the origin for + * @return the origin endpoint, or the input endpoint if no mapping is found */ String findOriginForEndpoint(String endpoint) { for (ConfigStore store : configStores) { @@ -100,14 +108,22 @@ String findOriginForEndpoint(String endpoint) { } /** - * Sets the replica as the currently used endpoint for connecting to the config store. - * @param originEndpoint Origin Configuration Store - * @param replicaEndpoint Replica that was last successfully connected to. + * Sets the current active replica for a configuration store. + * + * @param originEndpoint the origin configuration store endpoint + * @param replicaEndpoint the replica endpoint that was successfully connected to */ void setCurrentConfigStoreClient(String originEndpoint, String replicaEndpoint) { CONNECTIONS.get(originEndpoint).setCurrentClient(replicaEndpoint); } + /** + * Updates the sync token for a specific replica endpoint. + * + * @param originEndpoint the origin configuration store endpoint + * @param endpoint the specific replica endpoint + * @param syncToken the new sync token to store + */ void updateSyncToken(String originEndpoint, String endpoint, String syncToken) { CONNECTIONS.get(originEndpoint).updateSyncToken(endpoint, syncToken); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java index c018efb0566d..ce848571356e 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java @@ -28,36 +28,82 @@ import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring.PushNotification; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagKeyValueSelector; +/** + * Azure App Configuration data loader implementation for Spring Boot's ConfigDataLoader. + * + * @since 6.0.0 + */ + public class AzureAppConfigDataLoader implements ConfigDataLoader { + /** + * Logger instance for this class. + */ private static Log logger = new DeferredLog(); + /** + * The Azure App Configuration data resource being processed. + */ private AzureAppConfigDataResource resource; + /** + * Factory for creating replica clients to connect to Azure App Configuration. + */ private AppConfigurationReplicaClientFactory replicaClientFactory; + /** + * Factory for creating Key Vault clients for secret resolution. + */ private AppConfigurationKeyVaultClientFactory keyVaultClientFactory; + /** + * State holder for managing configuration and feature flag states. + */ private StateHolder storeState = new StateHolder(); + /** + * Client for handling feature flag operations. + */ private FeatureFlagClient featureFlagClient; + /** + * Request context for tracking operations and telemetry. + */ private Context requestContext; + /** + * Application start time for calculating delays. + */ private static final Instant START_DATE = Instant.now(); + /** + * Pre-kill time in seconds for delaying exceptions during startup. + */ private static final Integer PREKILL_TIME = 5; + /** + * Constructs a new AzureAppConfigDataLoader with the specified logger factory. + * + * @param logFactory the deferred log factory for creating loggers + */ public AzureAppConfigDataLoader(DeferredLogFactory logFactory) { logger = logFactory.getLog(getClass()); } + /** + * Loads configuration data from Azure App Configuration service. + * + * @param context the config data loader context + * @param resource the Azure App Configuration data resource + * @return ConfigData containing loaded property sources + * @throws IOException if an I/O error occurs during loading + * @throws ConfigDataResourceNotFoundException if the configuration resource is not found + */ @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 { @@ -67,9 +113,7 @@ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResour } // Reset telemetry usage for refresh featureFlagClient.resetTelemetry(); - List> sourceList = new ArrayList<>(); - if (resource.isConfigStoreEnabled()) { replicaClientFactory = context.getBootstrapContext() .get(AppConfigurationReplicaClientFactory.class); @@ -80,7 +124,6 @@ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResour .getAvailableClients(resource.getEndpoint(), true); boolean reloadFailed = false; - boolean pushRefresh = false; PushNotification notification = resource.getMonitoring().getPushNotification(); if ((notification.getPrimaryToken() != null @@ -148,9 +191,15 @@ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResour return new ConfigData(sourceList); } + /** + * Handles failed property source generation when all replicas fail during application startup. + * + * @param e the exception that caused the failure + * @throws RuntimeException always thrown to indicate the startup failure + */ 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() + "."); + logger.error("Configuration loading failed during application startup from Azure App Configuration store " + + resource.getEndpoint() + ". Application cannot start without required configuration."); delayException(); throw new RuntimeException("Failed to generate property sources for " + resource.getEndpoint(), e); } @@ -187,11 +236,11 @@ private List createSettings(AppConfigurationRepl } /** - * Creates a new set of AppConfigurationPropertySources, 1 per Label. + * Creates a list of feature flags from Azure App Configuration. * * @param client client for connecting to App Configuration - * @return a list of AppConfigurationPropertySources - * @throws Exception creating a property source failed + * @return a list of FeatureFlags + * @throws Exception creating feature flags failed */ private List createFeatureFlags(AppConfigurationReplicaClient client) throws Exception { 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 index 8c5d1cabd38e..927b63d9ec72 100644 --- 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 @@ -27,15 +27,30 @@ import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProperties; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore; +/** + * Resolves Azure App Configuration data locations for Spring Boot's ConfigData API. + * + * @since 6.0.0 + */ + public class AzureAppConfigDataLocationResolver implements ConfigDataLocationResolver { private static final Log LOGGER = new DeferredLog(); + /** Prefix used to identify Azure App Configuration locations */ public static final String PREFIX = "azureAppConfiguration"; + /** Flag to track startup phase for proper resource initialization */ private static final AtomicBoolean START_UP = new AtomicBoolean(true); + /** + * Determines if the given location can be resolved by this resolver. + * + * @param context the resolver context containing binder and bootstrap information + * @param location the configuration data location to check + * @return true if this resolver can handle the location, false otherwise + */ @Override public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) { if (!location.hasPrefix(PREFIX)) { @@ -57,6 +72,15 @@ public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDat return (hasEndpoint || hasConnectionString || hasEndpoints || hasConnectionStrings); } + } + + /** + * Resolves configuration data resources for the given location. + * + * @param context the resolver context + * @param location the configuration data location + * @return empty list of resources + */ @Override public List resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location) @@ -64,6 +88,15 @@ public List resolve(ConfigDataLocationResolverContex return Collections.emptyList(); } + /** + * Resolves profile-specific configuration data resources. + * + * @param resolverContext the resolver context + * @param location the configuration data location + * @param profiles the active Spring profiles + * @return list of Azure App Configuration data resources + * @throws ConfigDataLocationNotFoundException if location cannot be found + */ @Override public List resolveProfileSpecific( ConfigDataLocationResolverContext resolverContext, ConfigDataLocation location, Profiles profiles) @@ -80,6 +113,12 @@ public List resolveProfileSpecific( return locations; } + /** + * Loads and validates Azure App Configuration properties from the configuration context. + * + * @param context the configuration data location resolver context + * @return validated Azure App Configuration properties + */ protected AppConfigurationProperties loadProperties(ConfigDataLocationResolverContext context) { Binder binder = context.getBinder(); BindHandler bindHandler = getBindHandler(context); 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 index 3f31ee7fd926..b59937ca3b15 100644 --- 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 @@ -14,26 +14,48 @@ import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagKeyValueSelector; +/** + * Represents an Azure App Configuration data resource that extends Spring Boot's ConfigDataResource. + * + * @since 6.0.0 + */ public class AzureAppConfigDataResource extends ConfigDataResource { + /** Indicates whether the configuration store is enabled for loading configuration data. */ private final boolean configStoreEnabled; + /** The endpoint URL of the Azure App Configuration store. */ private final String endpoint; - private List trimKeyPrefix; + /** List of key prefixes to trim from configuration keys when loading. */ + private final List trimKeyPrefix; + /** Spring Boot profiles configuration for conditional property loading. */ private final Profiles profiles; - private List selects = new ArrayList<>(); + /** List of selectors for filtering key-value pairs from the configuration store. */ + private final List selects; - private List featureFlagSelects = new ArrayList<>(); + /** List of selectors for filtering feature flag key-value pairs from the configuration store. */ + private final List featureFlagSelects; + /** Monitoring configuration for the configuration store including refresh triggers. */ private final AppConfigurationStoreMonitoring monitoring; + /** Indicates whether this resource supports configuration refresh at runtime. */ private final boolean isRefresh; - private Duration refreshInterval; + /** The interval at which configuration should be refreshed from the store. */ + private final Duration refreshInterval; + /** + * Constructs a new AzureAppConfigDataResource with the specified configuration store settings. + * + * @param configStore the configuration store settings containing endpoint, selectors, and other options + * @param profiles the Spring Boot profiles for conditional configuration loading + * @param isRefresh whether this resource supports runtime configuration refresh + * @param refreshInterval the interval at which configuration should be refreshed + */ AzureAppConfigDataResource(ConfigStore configStore, Profiles profiles, boolean isRefresh, Duration refreshInterval) { this.configStoreEnabled = configStore.isEnabled(); @@ -48,93 +70,83 @@ public class AzureAppConfigDataResource extends ConfigDataResource { } /** - * @return the selects + * Gets the list of key-value selectors used to filter configuration data from the store. + * + * @return the list of configuration key-value selectors, may be null or empty */ 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 + * Gets the list of feature flag selectors used to filter feature flag data from the store. + * + * @return the list of feature flag selectors, may be null or empty */ public List getFeatureFlagSelects() { return featureFlagSelects; } /** - * @param featureFlagSelects the selects to set - */ - public void setFeatureFlagSelects(List featureFlagSelects) { - this.featureFlagSelects = featureFlagSelects; - } - - /** - * @return the configStoreEnabled + * Checks whether the configuration store is enabled for loading configuration data. + * + * @return true if the configuration store is enabled, false otherwise */ public boolean isConfigStoreEnabled() { return configStoreEnabled; } /** - * @return the endpoint + * Gets the endpoint URL of the Azure App Configuration store. + * + * @return the endpoint URL as a string, may be null if not configured */ public String getEndpoint() { return endpoint; } /** - * @return the monitoring + * Gets the monitoring configuration for this configuration store. + * + * @return the monitoring configuration, may be null if not configured */ public AppConfigurationStoreMonitoring getMonitoring() { return monitoring; } /** - * @return the trimKeyPrefix + * Gets the list of key prefixes to trim from configuration keys when loading. + * + * @return the list of key prefixes to trim, may be null or empty if no trimming is configured */ public List getTrimKeyPrefix() { return trimKeyPrefix; } /** - * @param trimKeyPrefix the trimKeyPrefix to set - */ - public void setTrimKeyPrefix(List trimKeyPrefix) { - this.trimKeyPrefix = trimKeyPrefix; - } - - /** - * @return the profiles + * Gets the Spring Boot profiles configuration for conditional property loading. + * + * @return the profiles configuration, never null */ public Profiles getProfiles() { return profiles; } /** - * @return the isRefresh + * Returns if true if this resource is being refreshed. False if the resource is being loaded at startup. + * + * @return true if this is a refresh operation, false if it is a startup load */ public boolean isRefresh() { return isRefresh; } /** - * @return the refreshInterval + * Gets the interval at which configuration should be refreshed from the store. + * + * @return the refresh interval, may be null if not configured */ 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 073691bd9477..ecedfbbc2a7c 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 @@ -5,14 +5,26 @@ import java.util.Random; /** - * Calculates the amount of time to the next refresh, if a refresh fails. + * Utility class for calculating exponential backoff times for Azure App Configuration retry operations. */ final class BackoffTimeCalculator { - private static final Long MAX_ATTEMPTS = (long) 63; + /** + * Maximum number of attempts to consider for exponential backoff calculation. This prevents integer overflow when + * calculating 2^attempts. Value of 63 ensures that 2^63 is the largest power of 2 that fits in a long. + */ + private static final long MAX_ATTEMPTS = 63; - private static final Long SECONDS_TO_NANO_SECONDS = (long) 1000000000; + /** + * Conversion factor from seconds to nanoseconds. Used to convert backoff times from seconds to nanoseconds for + * precise timing. + */ + private static final long SECONDS_TO_NANOSECONDS = 1_000_000_000L; + /** + * Generator for introducing jitter in backoff calculations. Jitter helps prevent multiple + * clients from retrying simultaneously (thundering herd). + */ private static final Random RANDOM = new Random(); private static Long maxBackoff = (long) 600; 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 17124ea72904..041e114884d2 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 @@ -18,33 +18,43 @@ import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagStore; /** - * Holds a set of connections to an app configuration store with zero to many geo-replications. + * Manages connection pools and client lifecycle for Azure App Configuration stores with support for geo-replication, + * auto-failover, and intelligent client routing. */ class ConnectionManager { private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionManager.class); + /** The primary endpoint URL for the App Configuration store. */ private final String originEndpoint; - // Used if multiple connection method is given. + /** List of configured replica clients for the primary App Configuration store. */ private List clients; - private Map autoFailoverClients; + /** Map of auto-discovered failover clients, keyed by endpoint URL. */ + private final Map autoFailoverClients; + /** Currently active replica endpoint being used for requests. */ private String currentReplica; + /** Current health status of the App Configuration store connection. */ private AppConfigurationStoreHealth health; + /** Builder for creating App Configuration replica clients. */ private final AppConfigurationReplicaClientsBuilder clientBuilder; + /** Configuration store settings and connection parameters. */ private final ConfigStore configStore; + /** Service for discovering auto-failover replica endpoints. */ private final ReplicaLookUp replicaLookUp; /** - * Creates a set of connections to an app configuration store. - * @param clientBuilder Builder for App Configuration Clients - * @param configStore Connection info for the store + * Creates a connection manager for the specified App Configuration store. + * + * @param clientBuilder the builder for creating App Configuration replica clients; must not be null + * @param configStore the configuration store settings and connection parameters; must not be null + * @param replicaLookUp the service for discovering auto-failover endpoints; must not be null */ ConnectionManager(AppConfigurationReplicaClientsBuilder clientBuilder, ConfigStore configStore, ReplicaLookUp replicaLookUp) { @@ -58,35 +68,47 @@ class ConnectionManager { } /** - * Gets the current health information on the Connection to the Config Store - * @return AppConfigurationConfigStoreHealth + * Retrieves the current health status of the App Configuration store connection. + * + * @return the current health status; never null */ AppConfigurationStoreHealth getHealth() { return this.health; } + /** + * Sets the current active replica endpoint for client routing. + * + * @param replicaEndpoint the endpoint URL to set as current; may be null to reset to primary endpoint + */ void setCurrentClient(String replicaEndpoint) { this.currentReplica = replicaEndpoint; } /** - * @return the originEndpoint + * Retrieves the primary (origin) endpoint URL for the App Configuration store. + * + * @return the primary endpoint URL; never null */ String getMainEndpoint() { return originEndpoint; } /** - * Returns a client. - * @return ConfigurationClient + * Retrieves all available App Configuration clients that are ready for use. + * + * @return a list of available clients; may be empty if all clients are currently unavailable */ List getAvailableClients() { return getAvailableClients(false); } /** - * Returns a client. - * @return ConfigurationClient + * Retrieves available App Configuration clients with optional current replica preference. + * + * @param useCurrent if true, prioritizes returning clients starting from the current replica; if false, returns all + * available clients + * @return a list of available clients ordered by preference; may be empty if all clients are currently unavailable */ List getAvailableClients(Boolean useCurrent) { if (clients == null) { @@ -138,13 +160,12 @@ List getAvailableClients(Boolean useCurrent) { } else if (clients.size() > 0) { this.health = AppConfigurationStoreHealth.UP; } - - return availableClients; } /** - * Call when the current client failed - * @param endpoint replica endpoint + * Applies exponential backoff to a failed client endpoint. + * + * @param endpoint the endpoint URL of the failed client; must not be null or empty */ void backoffClient(String endpoint) { for (AppConfigurationReplicaClient client : clients) { @@ -173,10 +194,20 @@ void updateSyncToken(String endpoint, String syncToken) { .ifPresent(client -> client.updateSyncToken(syncToken)); } + /** + * Retrieves the monitoring configuration for the App Configuration store. + * + * @return the monitoring configuration; may be null if not configured + */ AppConfigurationStoreMonitoring getMonitoring() { return configStore.getMonitoring(); } + /** + * Retrieves the feature flag store configuration. + * + * @return the feature flag store configuration; may be null if not configured + */ FeatureFlagStore getFeatureFlagStore() { return configStore.getFeatureFlags(); } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java index 20e965d4e3ea..8bf39dd1115c 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java @@ -74,7 +74,8 @@ public class FeatureManager { * @param featureManagementConfigurations Configuration Properties for Feature Flags * @param properties FeatureManagementConfigProperties * @param contextAccessor TargetingContextAccessor - * @param evaluationOptions TargetingE + * @param evaluationOptions TargetingEvaluationOptions + * @param telemetryPublisher TelemetryPublisher */ FeatureManager(ApplicationContext context, FeatureManagementProperties featureManagementConfigurations, FeatureManagementConfigProperties properties, TargetingContextAccessor contextAccessor, diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java index fd52e85912ee..0d25d4508272 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java @@ -21,10 +21,12 @@ public static void updateValueFromMapToList(Map parameters, Stri } /** - * Looks at the given key in the parameters and coverts it to a list if it is currently a map. + * Looks at the given key in the parameters and converts it to a list if it is currently a map. Also handles empty + * strings and null values based on the fixNull parameter. * * @param parameters map of generic objects - * @param key key of object int the parameters map + * @param key key of object in the parameters map + * @param fixNull whether to convert null values to empty lists */ @SuppressWarnings("unchecked") public static void updateValueFromMapToList(Map parameters, String key, boolean fixNull) { @@ -47,7 +49,8 @@ public static String getKeyCase(Map parameters, String key) { } /** - * Computes the percentage that the contextId falls into. + * Computes the percentage bucket (0-100) that the contextId falls into. Uses SHA-256 hash for consistent + * distribution across the percentage range. * * @param contextId Id of the context being targeted * @return the bucket value of the context id From 871dc26c6ae1c60869cfd5bded3ed4fb6a4db0c0 Mon Sep 17 00:00:00 2001 From: Matthew Metcalf Date: Tue, 3 Jun 2025 13:53:47 -0700 Subject: [PATCH 21/41] App Config 6.0 bug fixes (#45568) * Simple Fixes * Fixing Startup Failover logging * Cleaned up resolver validation * Update ConnectionManager.java * Cleaning up telemetry publishing --- ...ationApplicationSettingPropertySource.java | 4 +- ...rationFeatureManagementPropertySource.java | 3 +- ...AppConfigurationKeyVaultClientFactory.java | 2 +- .../AppConfigurationPropertySource.java | 22 +++-- .../AppConfigurationReplicaClient.java | 16 +++- .../AppConfigurationReplicaClientFactory.java | 15 +-- .../AzureAppConfigDataLoader.java | 71 ++++++++++---- .../AzureAppConfigDataLocationResolver.java | 41 +++++--- .../implementation/BackoffTimeCalculator.java | 47 +++++++--- .../implementation/ConnectionManager.java | 11 ++- .../FeatureManagementConfiguration.java | 7 +- .../feature/management/FeatureManager.java | 93 +++++++++++-------- 12 files changed, 223 insertions(+), 109 deletions(-) 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 2f1f5e0f28a7..4ac64874a871 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 @@ -116,11 +116,11 @@ private void handleKeyVaultReference(String key, SecretReferenceConfigurationSet KeyVaultSecret secret = keyVaultClientFactory.getClient("https://" + uri.getHost()).getSecret(uri); properties.put(key, secret.getValue()); } catch (URISyntaxException e) { - LOGGER.error(String.format("Error Retrieving Key Vault Entry for key %s.", key)); + LOGGER.error("Error Retrieving Key Vault Entry for key {}.", key); throw new InvalidConfigurationPropertyValueException(key, "", "Invalid URI found in JSON property field 'uri' unable to parse."); } catch (RuntimeException e) { - LOGGER.error(String.format("Error Retrieving Key Vault Entry for key %s.", key)); + LOGGER.error("Error Retrieving Key Vault Entry for key {}.", key); throw e; } } 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 63efff2e78bf..97efc7c20260 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 @@ -32,7 +32,8 @@ class AppConfigurationFeatureManagementPropertySource extends EnumerableProperty @Override public String[] getPropertyNames() { - if (featureFlagLoader != null && !featureFlagLoader.getFeatureFlags().isEmpty()) { + if (featureFlagLoader != null && featureFlagLoader.getFeatureFlags() != null + && !featureFlagLoader.getFeatureFlags().isEmpty()) { return new String[] { FEATURE_FLAG_KEY }; } return new String[0]; 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 14882dd17047..e9ded98208b9 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 @@ -15,9 +15,9 @@ * Vault host. */ class AppConfigurationKeyVaultClientFactory { - /** * Cache of secret client managers by Key Vault host. + */ private final Map keyVaultClients; /** 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 c2ce5df5e3f0..120adc189672 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 @@ -5,7 +5,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Set; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.core.env.EnumerablePropertySource; @@ -23,6 +22,8 @@ abstract class AppConfigurationPropertySource extends EnumerablePropertySource { /** + * Cache for storing configuration properties retrieved from Azure App Configuration. + */ protected final Map properties = new LinkedHashMap<>(); /** @@ -39,7 +40,7 @@ abstract class AppConfigurationPropertySource extends EnumerablePropertySource keySet = properties.keySet(); - return keySet.toArray(new String[keySet.size()]); + return properties.keySet().toArray(String[]::new); } /** * Returns the value of the specified property. * + * @param name the name of the property to retrieve + * @return the value of the property, or null if not found */ @Override public Object getProperty(String name) { @@ -70,11 +72,19 @@ public Object getProperty(String name) { * @return comma-separated string of labels, or empty string if null/empty */ protected static String getLabelName(String[] labelFilters) { - if (labelFilters == null) { + if (labelFilters == null || labelFilters.length == 0) { return ""; } return String.join(",", labelFilters); } - protected abstract void initProperties(List trim, Context context) throws InvalidConfigurationPropertyValueException; + /** + * Initializes the properties for this property source by loading them from Azure App Configuration. + * + * @param trim list of key prefixes to trim from configuration keys + * @param context the context for loading properties, may contain additional metadata + * @throws InvalidConfigurationPropertyValueException if there are issues with the configuration properties + */ + 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/AppConfigurationReplicaClient.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClient.java index ed9d32b4c985..0cde712d5867 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 @@ -50,6 +50,7 @@ class AppConfigurationReplicaClient { /** * Holds Configuration Client and info needed to manage backoff. * @param endpoint client endpoint + * @param originClient origin client identifier * @param client Configuration Client to App Configuration store */ AppConfigurationReplicaClient(String endpoint, String originClient, ConfigurationClient client) { @@ -105,6 +106,7 @@ String getOriginClient() { * @param label String value of the watch key, use \0 for null. * @param context Azure SDK context for request correlation * @return The first returned configuration. + * @throws HttpResponseException if the request fails */ ConfigurationSetting getWatchKey(String key, String label, Context context) throws HttpResponseException { @@ -212,8 +214,9 @@ List listSettingSnapshot(String snapshotName, Context cont } boolean checkWatchKeys(SettingSelector settingSelector, Context context) { - List> results = client.listConfigurationSettings(settingSelector, context) - .streamByPage().filter(pagedResponse -> pagedResponse.getStatusCode() != 304).toList(); + List> results = client + .listConfigurationSettings(settingSelector, context) + .streamByPage().filter(pagedResponse -> pagedResponse.getStatusCode() != HTTP_NOT_MODIFIED).toList(); return results.size() > 0; } @@ -227,7 +230,14 @@ void updateSyncToken(String syncToken) { } } - private HttpResponseException hanndleHttpResponseException(HttpResponseException e) { + /** + * Handles HTTP response exceptions by determining if they are retryable. + * + * @param e the HTTP response exception to handle + * @return an AppConfigurationStatusException for retryable errors, or the original exception for non-retryable + * errors + */ + private HttpResponseException handleHttpResponseException(HttpResponseException e) { if (e.getResponse() != null) { int statusCode = e.getResponse().getStatusCode(); 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 6c6221db85ca..63b2fd4db0d7 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 @@ -31,7 +31,7 @@ public class AppConfigurationReplicaClientFactory { AppConfigurationReplicaClientFactory(AppConfigurationReplicaClientsBuilder clientBuilder, List configStores, ReplicaLookUp replicaLookUp) { this.configStores = configStores; - if (CONNECTIONS.size() == 0) { + if (CONNECTIONS.isEmpty()) { for (ConfigStore store : configStores) { ConnectionManager manager = new ConnectionManager(clientBuilder, store, replicaLookUp); CONNECTIONS.put(manager.getMainEndpoint(), manager); @@ -51,9 +51,11 @@ public Map getConnections() { /** * Returns available replica clients for a given configuration store. * + * @param originEndpoint identifier of the store (primary endpoint) + * @return list of available replica clients for the store */ List getAvailableClients(String originEndpoint) { - return CONNECTIONS.get(originEndpoint).getAvailableClients(); + return getAvailableClients(originEndpoint, false); } /** @@ -73,7 +75,7 @@ List getAvailableClients(String originEndpoint, B * @param originEndpoint identifier of the store (primary endpoint) * @param endpoint the specific replica endpoint that failed */ - void backoffClientClient(String originEndpoint, String endpoint) { + void backoffClient(String originEndpoint, String endpoint) { CONNECTIONS.get(originEndpoint).backoffClient(endpoint); } @@ -98,10 +100,9 @@ Map getHealth() { */ String findOriginForEndpoint(String endpoint) { for (ConfigStore store : configStores) { - for (String replica : store.getEndpoints()) { - if (replica.equals(endpoint)) { - return store.getEndpoint(); - } + List replicas = store.getEndpoints(); + if (replicas != null && replicas.contains(endpoint)) { + return store.getEndpoint(); } } return endpoint; diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java index ce848571356e..616056a0a6b1 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java @@ -7,6 +7,7 @@ import java.io.IOException; import java.time.Instant; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import org.apache.commons.logging.Log; @@ -125,6 +126,7 @@ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResour boolean reloadFailed = false; boolean pushRefresh = false; + Exception lastException = null; PushNotification notification = resource.getMonitoring().getPushNotification(); if ((notification.getPrimaryToken() != null && StringUtils.hasText(notification.getPrimaryToken().getName())) @@ -132,13 +134,13 @@ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResour && 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. + requestContext = new Context("refresh", resource.isRefresh()).addData(PUSH_REFRESH, pushRefresh); + + Iterator clientIterator = clients.iterator(); - for (AppConfigurationReplicaClient client : clients) { - sourceList = new ArrayList<>(); + while (clientIterator.hasNext()) { + AppConfigurationReplicaClient client = clientIterator.next(); if (reloadFailed && !AppConfigurationRefreshUtil.refreshStoreCheck(client, @@ -167,19 +169,31 @@ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResour storeState.setState(resource.getEndpoint(), watchKeysSettings, monitoring.getRefreshInterval()); } - storeState.setLoadState(resource.getEndpoint(), true); + storeState.setLoadState(resource.getEndpoint(), true); // Success - configuration loaded, exit loop + lastException = null; + // Break out of the loop since we have successfully loaded configuration + break; } catch (AppConfigurationStatusException e) { reloadFailed = true; - replicaClientFactory.backoffClientClient(resource.getEndpoint(), client.getEndpoint()); + replicaClientFactory.backoffClient(resource.getEndpoint(), client.getEndpoint()); + lastException = e; + // Log the specific replica failure with context + logReplicaFailure(client, "status exception", clientIterator.hasNext(), e); } catch (Exception e) { - failedToGeneratePropertySource(e); - - // Not a retriable error - break; - } - if (sourceList.size() > 0) { - break; + // Store the exception to potentially use if all replicas fail + lastException = e; // Log the specific replica failure with context + logReplicaFailure(client, "exception", clientIterator.hasNext(), e); } + } // Check if all replicas failed + if (lastException != null && !resource.isRefresh()) { + // During startup, if all replicas failed, fail the application + logger.error("Azure App Configuration failed to load configuration during startup for store: " + + resource.getEndpoint() + ". Application cannot start without required configuration."); + failedToGeneratePropertySource(lastException); + } else if (lastException != null && resource.isRefresh()) { + // During refresh, log warning but don't fail the application + logger.warn("Azure App Configuration failed during refresh for store: " + + resource.getEndpoint() + ". Continuing with existing configuration."); } } @@ -256,15 +270,36 @@ private List createFeatureFlags(AppConfigurationReplicaClient clie return featureFlagWatchKeys; } + /** + * Logs a replica failure with contextual information about the failure scenario and available replicas. + * + * @param client the replica client that failed + * @param exceptionType a brief description of the exception type (e.g., "status exception", "exception") + * @param hasMoreReplicas whether there are additional replicas available to try + * @param exception the exception that caused the failure + */ + private void logReplicaFailure(AppConfigurationReplicaClient client, String exceptionType, + boolean hasMoreReplicas, Exception exception) { + String scenario = resource.isRefresh() ? "refresh" : "startup"; + String nextAction = hasMoreReplicas ? "Trying next replica." : "No more replicas available."; + + logger.warn("Azure App Configuration replica " + client.getEndpoint() + + " failed during " + scenario + " with " + exceptionType + ". " + + nextAction + " Store: " + resource.getEndpoint(), exception); + } + + /** + * Introduces a delay before throwing exceptions during startup to prevent fast crash loops. + */ 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()); + 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."); + Thread.currentThread().interrupt(); // Restore interrupted status } } } 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 index 927b63d9ec72..41acde2f8765 100644 --- 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 @@ -56,22 +56,29 @@ public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDat 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); + + // Check if the configuration properties for Azure App Configuration are present + return hasValidStoreConfiguration(context.getBinder()); + } + + /** + * Checks if the required configuration properties for Azure App Configuration are present. + * + * @param binder the binder to check for properties + * @return true if at least one of the required properties is present, false otherwise + */ + private boolean hasValidStoreConfiguration(Binder binder) { + // Check if any of the required properties for Azure App Configuration stores are present + String configPrefix = AppConfigurationProperties.CONFIG_PREFIX + ".stores[0]."; + + return hasNonEmptyProperty(binder, configPrefix + "endpoint") + || hasNonEmptyProperty(binder, configPrefix + "connection-string") + || hasNonEmptyProperty(binder, configPrefix + "endpoints") + || hasNonEmptyProperty(binder, configPrefix + "connection-strings"); } + private boolean hasNonEmptyProperty(Binder binder, String propertyPath) { + return StringUtils.hasText(binder.bind(propertyPath, String.class).orElse("")); } /** @@ -105,6 +112,12 @@ public List resolveProfileSpecific( AppConfigurationProperties properties = loadProperties(resolverContext); List locations = new ArrayList<>(); + if (properties.getStores() == null || properties.getStores().isEmpty()) { + throw new ConfigDataLocationNotFoundException(location, + "No Azure App Configuration stores are configured. Please check your application properties.", + new IllegalStateException("No stores configured")); + } + for (ConfigStore store : properties.getStores()) { locations.add( new AzureAppConfigDataResource(store, profiles, START_UP.get(), properties.getRefreshInterval())); 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 ecedfbbc2a7c..39f378fe2eff 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 @@ -22,28 +22,46 @@ final class BackoffTimeCalculator { private static final long SECONDS_TO_NANOSECONDS = 1_000_000_000L; /** - * Generator for introducing jitter in backoff calculations. Jitter helps prevent multiple - * clients from retrying simultaneously (thundering herd). + * Generator for introducing jitter in backoff calculations. Jitter helps prevent multiple clients from retrying + * simultaneously (thundering herd). */ private static final Random RANDOM = new Random(); - private static Long maxBackoff = (long) 600; + /** + * Maximum backoff time in seconds. Default: 600 seconds (10 minutes) - reasonable maximum to prevent excessively + * long waits. + */ + private static long maxBackoffSeconds = 600; + + /** + * Minimum backoff time in seconds. Default: 30 seconds - ensures proper rate limiting and prevents rapid retry + * loops. + */ + private static long minBackoffSeconds = 30; - 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.maxBackoffSeconds = maxBackoff != null ? maxBackoff : 600L; + BackoffTimeCalculator.minBackoffSeconds = minBackoff != null ? minBackoff : 30L; + } /** - * Calculates the new Backoff time for requests. - * @param attempts Number of attempts so far - * @return Nano Seconds to the next request - * @throws IllegalArgumentException when back off time or attempt number is invalid + * Calculates the exponential backoff time with jitter for retry operations. + * + * @param attempts the number of retry attempts made so far; must be non-negative + * @return the calculated backoff time in nanoseconds; never negative */ - static Long calculateBackoff(Integer attempts) { + static long calculateBackoff(Integer attempts) { - if (minBackoff < 0) { + if (minBackoffSeconds < 0) { throw new IllegalArgumentException("Minimum Backoff time needs to be greater than or equal to 0."); } - if (maxBackoff < 0) { + if (maxBackoffSeconds < 0) { throw new IllegalArgumentException("Maximum Backoff time needs to be greater than or equal to 0."); } @@ -51,10 +69,11 @@ static Long calculateBackoff(Integer attempts) { throw new IllegalArgumentException("Number of previous attempts needs to be a positive number."); } - long minBackoffNano = minBackoff * SECONDS_TO_NANO_SECONDS; - long maxBackoffNano = maxBackoff * SECONDS_TO_NANO_SECONDS; + final long minBackoffNano = minBackoffSeconds * SECONDS_TO_NANOSECONDS; + final long maxBackoffNano = maxBackoffSeconds * SECONDS_TO_NANOSECONDS; - if (attempts <= 1 || maxBackoff <= minBackoff) { + // For first attempts or when min equals max, return minimum backoff + if (attempts <= 1 || maxBackoffNano <= minBackoffNano) { return minBackoffNano; } 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 041e114884d2..f160649abe06 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 @@ -114,7 +114,7 @@ List getAvailableClients(Boolean useCurrent) { if (clients == null) { clients = clientBuilder.buildClients(configStore); - if (clients.size() == 0) { + if (clients.isEmpty()) { this.health = AppConfigurationStoreHealth.NOT_LOADED; } } @@ -160,6 +160,8 @@ List getAvailableClients(Boolean useCurrent) { } else if (clients.size() > 0) { this.health = AppConfigurationStoreHealth.UP; } + + return availableClients; } /** @@ -183,9 +185,10 @@ void backoffClient(String endpoint) { } /** - * Updates the sync token of the client. Only works if no replicas are being used. - * - * @param syncToken App Configuration sync token + * Updates the synchronization token for the specified client endpoint. + * + * @param endpoint the endpoint URL of the client to update; may be null (method will have no effect if null) + * @param syncToken the new synchronization token; may be null to clear the existing token */ void updateSyncToken(String endpoint, String syncToken) { clients.stream().filter(client -> client.getEndpoint().equals(endpoint)).findFirst() diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java index 8ca875376f42..9096856a8082 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java @@ -76,8 +76,11 @@ public PercentageFilter percentageFilter() { @Scope("request") @ConditionalOnMissingBean(TargetingFilter.class) @ConditionalOnBean(TargetingContextAccessor.class) - public TargetingFilter targettingFilter(TargetingContextAccessor context) { - return new TargetingFilter(context, new TargetingEvaluationOptions().setIgnoreCase(true)); + public TargetingFilter targetingFilter(TargetingContextAccessor context, + ObjectProvider evaluationOptionsProvider) { + TargetingEvaluationOptions evaluationOptions = evaluationOptionsProvider + .getIfAvailable(() -> new TargetingEvaluationOptions().setIgnoreCase(true)); + return new TargetingFilter(context, evaluationOptions); } @Bean diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java index 8bf39dd1115c..d4e49d5e491e 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java @@ -14,7 +14,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; -import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import com.azure.spring.cloud.feature.management.filters.ContextualFeatureFilter; @@ -50,9 +49,10 @@ * Used to evaluate the enabled state of a feature and/or get the assigned variant of a feature, if any. */ public class FeatureManager { - private static final Logger LOGGER = LoggerFactory.getLogger(FeatureManager.class); + private static final double PERCENTILE_MAXIMUM = 100.0; + private transient ApplicationContext context; private final FeatureManagementProperties featureManagementConfigurations; @@ -205,29 +205,32 @@ private Mono checkFeature(String featureName, Object featureCon if (!featureFlag.isEnabled()) { this.assignDefaultDisabledReason(event); event.setEnabled(false); - if (telemetryPublisher != null && featureFlag.getTelemetry() != null - && featureFlag.getTelemetry().isEnabled()) { - telemetryPublisher.publishTelemetry(event); - } + publishTelemetryIfEnabled(event); // If a feature flag is disabled and override can't enable it return Mono.just(event); } Mono result = this.checkFeatureFilters(event, featureContext); - result = assignAllocation(result); - result = result.doOnSuccess(resultEvent -> { - if (telemetryPublisher != null && featureFlag.getTelemetry() != null - && featureFlag.getTelemetry().isEnabled()) { - telemetryPublisher.publishTelemetry(resultEvent); - } - }); + result = result.doOnSuccess(resultEvent -> publishTelemetryIfEnabled(resultEvent)); return result; } - private Mono assignAllocation(Mono monoEvent) { + /** + * Publishes telemetry if enabled for the feature. + * + * @param event Evaluation event + */ + private void publishTelemetryIfEnabled(EvaluationEvent event) { + if (telemetryPublisher != null && event.getFeature() != null + && event.getFeature().getTelemetry() != null + && event.getFeature().getTelemetry().isEnabled()) { + telemetryPublisher.publishTelemetry(event); + } + } + private Mono assignAllocation(Mono monoEvent) { return monoEvent.map(event -> { Feature featureFlag = event.getFeature(); @@ -333,7 +336,8 @@ private void assignVariant(EvaluationEvent event) { double box = FeatureFilterUtils.isTargetedPercentage(contextId); for (PercentileAllocation percentileAllocation : allocation.getPercentile()) { Double to = percentileAllocation.getTo(); - if ((box == 100 && to == 100) || (percentileAllocation.getFrom() <= box && box < to)) { + if ((box == PERCENTILE_MAXIMUM && to == PERCENTILE_MAXIMUM) + || (percentileAllocation.getFrom() <= box && box < to)) { event.setReason(VariantAssignmentReason.PERCENTILE); variantName = percentileAllocation.getVariant(); break; @@ -385,28 +389,7 @@ private Mono checkFeatureFilters(EvaluationEvent event, Object for (FeatureFilterEvaluationContext featureFilter : featureFilters) { String filterName = featureFilter.getName(); - try { - Object filter = context.getBean(filterName); - featureFilter.setFeatureName(featureFlag.getId()); - if (filter instanceof FeatureFilter) { - filterResults.add(Mono.just(((FeatureFilter) filter).evaluate(featureFilter))); - } else if (filter instanceof ContextualFeatureFilter) { - filterResults - .add(Mono.just(((ContextualFeatureFilter) filter).evaluate(featureFilter, featureContext))); - } else if (filter instanceof FeatureFilterAsync) { - filterResults.add(((FeatureFilterAsync) filter).evaluateAsync(featureFilter)); - } else if (filter instanceof ContextualFeatureFilterAsync) { - filterResults - .add(((ContextualFeatureFilterAsync) filter).evaluateAsync(featureFilter, featureContext)); - } - } catch (NoSuchBeanDefinitionException e) { - LOGGER.error("Was unable to find Filter {}. Does the class exist and set as an @Component?", - filterName); - if (properties.isFailFast()) { - String message = "Fail fast is set and a Filter was unable to be found"; - ReflectionUtils.rethrowRuntimeException(new FilterNotFoundException(message, e, featureFilter)); - } - } + filterResults.add(evaluateFilter(featureFilter, featureContext, filterName)); } if (ALL_REQUIREMENT_TYPE.equals(featureFlag.getConditions().getRequirementType())) { @@ -448,4 +431,40 @@ public Set getAllFeatureNames() { return new HashSet( featureManagementConfigurations.getFeatureFlags().stream().map(feature -> feature.getId()).toList()); } + + /** + * Enhanced filter evaluation with better error handling and logging. + * + * @param featureFilter Feature filter evaluation context + * @param featureContext Feature context + * @param filterName Name of the filter for logging + * @return Mono result of filter evaluation + */ + private Mono evaluateFilter(FeatureFilterEvaluationContext featureFilter, Object featureContext, + String filterName) { + try { + Object filter = context.getBean(filterName); + featureFilter.setFeatureName(featureFilter.getFeatureName()); + if (filter instanceof FeatureFilter) { + return Mono.just(((FeatureFilter) filter).evaluate(featureFilter)); + } else if (filter instanceof ContextualFeatureFilter) { + return Mono.just(((ContextualFeatureFilter) filter).evaluate(featureFilter, featureContext)); + } else if (filter instanceof FeatureFilterAsync) { + return ((FeatureFilterAsync) filter).evaluateAsync(featureFilter); + } else if (filter instanceof ContextualFeatureFilterAsync) { + return ((ContextualFeatureFilterAsync) filter).evaluateAsync(featureFilter, featureContext); + } else { + LOGGER.warn("Filter {} does not implement any known filter interface", filterName); + return Mono.just(false); + } + } catch (NoSuchBeanDefinitionException e) { + LOGGER.error("Was unable to find Filter {}. Does the class exist and set as an @Component?", filterName, e); + if (properties.isFailFast()) { + String message = "Fail fast is set and a Filter was unable to be found"; + return Mono.error(new FilterNotFoundException(message, e, featureFilter)); + } + return Mono.just(false); + } + } + } From f297bd0ac9d0dd03ed2119dae56adc5804aab477 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 3 Jun 2025 15:36:15 -0700 Subject: [PATCH 22/41] Fix format issues --- .../AppConfigurationRefreshUtil.java | 16 +++++++++------- .../AzureAppConfigDataResource.java | 1 - .../cloud/feature/management/FeatureManager.java | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) 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 bca843fdfe2c..f347bbe28e96 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 @@ -61,11 +61,13 @@ 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()))) { + 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); @@ -85,8 +87,8 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF break; } catch (HttpResponseException e) { LOGGER.warn( - "Failed to connect to App Configuration store {} during configuration refresh check. " + - "Status: {}, Message: {}", + "Failed to connect to App Configuration store {} during configuration refresh check. " + + "Status: {}, Message: {}", client.getEndpoint(), e.getResponse().getStatusCode(), e.getMessage()); clientFactory.backoffClient(originEndpoint, client.getEndpoint()); @@ -111,8 +113,8 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF break; } catch (HttpResponseException e) { LOGGER.warn( - "Failed to connect to App Configuration store {} during feature flag refresh check. " + - "Status: {}, Message: {}", + "Failed to connect to App Configuration store {} during feature flag refresh check. " + + "Status: {}, Message: {}", client.getEndpoint(), e.getResponse().getStatusCode(), e.getMessage()); clientFactory.backoffClient(originEndpoint, client.getEndpoint()); 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 index b59937ca3b15..0d828552c0e3 100644 --- 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 @@ -3,7 +3,6 @@ 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; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java index d4e49d5e491e..2ef769e0e1fd 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java @@ -438,7 +438,7 @@ public Set getAllFeatureNames() { * @param featureFilter Feature filter evaluation context * @param featureContext Feature context * @param filterName Name of the filter for logging - * @return Mono result of filter evaluation + * @return {@code Mono} result of filter evaluation */ private Mono evaluateFilter(FeatureFilterEvaluationContext featureFilter, Object featureContext, String filterName) { From 45778923bc80ffcf0f2aea4af0719bc065b35740 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 3 Jun 2025 16:09:25 -0700 Subject: [PATCH 23/41] fix build issues --- eng/versioning/version_client.txt | 10 +++++----- sdk/spring/spring-cloud-azure-actuator/pom.xml | 2 +- .../AppConfigurationWebAutoConfiguration.java | 6 ------ 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index b04d701dd93c..e22b9197a75b 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -201,11 +201,11 @@ com.azure:identity-test-vm;1.0.0-beta.1;1.0.0-beta.1 com.azure:identity-test-webapp;1.0.0-beta.1;1.0.0-beta.1 com.azure:azure-ai-vision-face;1.0.0-beta.2;1.0.0-beta.3 com.azure.spring:azure-monitor-spring-native-test;1.0.0-beta.1;1.0.0-beta.1 -com.azure.spring:spring-cloud-azure-appconfiguration-config-web;5.22.0;5.23.0-beta.1 -com.azure.spring:spring-cloud-azure-appconfiguration-config;5.22.0;5.23.0-beta.1 -com.azure.spring:spring-cloud-azure-feature-management-web;5.22.0;5.23.0-beta.1 -com.azure.spring:spring-cloud-azure-feature-management;5.22.0;5.23.0-beta.1 -com.azure.spring:spring-cloud-azure-starter-appconfiguration-config;5.22.0;5.23.0-beta.1 +com.azure.spring:spring-cloud-azure-appconfiguration-config-web;5.22.0;6.0.0-beta.1 +com.azure.spring:spring-cloud-azure-appconfiguration-config;5.22.0;6.0.0-beta.1 +com.azure.spring:spring-cloud-azure-feature-management-web;5.22.0;6.0.0-beta.1 +com.azure.spring:spring-cloud-azure-feature-management;5.22.0;6.0.0-beta.1 +com.azure.spring:spring-cloud-azure-starter-appconfiguration-config;5.22.0;6.0.0-beta.1 com.azure.spring:spring-cloud-azure-dependencies;5.21.0;5.22.0 com.azure.spring:spring-messaging-azure;5.22.0;5.23.0-beta.1 com.azure.spring:spring-messaging-azure-eventhubs;5.22.0;5.23.0-beta.1 diff --git a/sdk/spring/spring-cloud-azure-actuator/pom.xml b/sdk/spring/spring-cloud-azure-actuator/pom.xml index 312b52e2ee38..2c18b477d784 100644 --- a/sdk/spring/spring-cloud-azure-actuator/pom.xml +++ b/sdk/spring/spring-cloud-azure-actuator/pom.xml @@ -109,7 +109,7 @@ com.azure.spring spring-cloud-azure-appconfiguration-config-web - 5.23.0-beta.1 + 6.0.0-beta.1 true 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 fd08cc653354..8e1376a46045 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,18 +42,15 @@ 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); @@ -66,18 +63,15 @@ 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); From 8d2ff20bbdc77d5d5a09ba7f8fe7a36a0e6ba9ec Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 3 Jun 2025 16:22:11 -0700 Subject: [PATCH 24/41] Updating poms --- sdk/spring/spring-cloud-azure-actuator-autoconfigure/pom.xml | 2 +- .../pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-actuator-autoconfigure/pom.xml b/sdk/spring/spring-cloud-azure-actuator-autoconfigure/pom.xml index b7d16b7c216a..7fbddb7adc7a 100644 --- a/sdk/spring/spring-cloud-azure-actuator-autoconfigure/pom.xml +++ b/sdk/spring/spring-cloud-azure-actuator-autoconfigure/pom.xml @@ -62,7 +62,7 @@ com.azure.spring spring-cloud-azure-appconfiguration-config-web - 5.23.0-beta.1 + 6.0.0-beta.1 true diff --git a/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/pom.xml index 828a786a82c7..6cbaed15e7fd 100644 --- a/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/pom.xml @@ -21,7 +21,7 @@ com.azure.spring spring-cloud-azure-starter-appconfiguration-config - 5.23.0-beta.1 + 6.0.0-beta.1 org.springframework.boot From 58b393df72f33576db39eb52c209ace5aef4e5a7 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 3 Jun 2025 16:40:04 -0700 Subject: [PATCH 25/41] Update external_dependencies.txt --- eng/versioning/external_dependencies.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index 74d93e18e17c..d281c24aabe5 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -382,7 +382,6 @@ springboot3_org.springframework.boot:spring-boot-test;3.4.4 springboot3_org.springframework.boot:spring-boot-testcontainers;3.4.4 springboot3_org.springframework.cloud:spring-cloud-bus;4.2.1 springboot3_org.springframework.cloud:spring-cloud-context;4.2.1 -springboot3_org.springframework.cloud:spring-cloud-starter-bootstrap;4.2.1 springboot3_org.springframework.cloud:spring-cloud-starter-stream-kafka;4.2.1 springboot3_org.springframework.cloud:spring-cloud-stream-test-binder;4.2.1 springboot3_org.springframework.cloud:spring-cloud-stream;4.2.1 From 8ffeab458fc913909a25cd81ebe91993228098fc Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 4 Jun 2025 09:21:26 -0700 Subject: [PATCH 26/41] Fixing poms --- sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml | 1 - sdk/spring/spring-cloud-azure-feature-management-web/pom.xml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index b59931eafbff..0f2a0bf552a7 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -184,7 +184,6 @@ org.springframework.boot:spring-boot-autoconfigure:[3.4.4] org.springframework.cloud:spring-cloud-context:[4.2.1] org.springframework.boot:spring-boot-configuration-processor:[3.4.4] - org.springframework.cloud:spring-cloud-starter-bootstrap:[4.2.1] diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/pom.xml b/sdk/spring/spring-cloud-azure-feature-management-web/pom.xml index ae5a7f35ac69..e72569c2c2dd 100644 --- a/sdk/spring/spring-cloud-azure-feature-management-web/pom.xml +++ b/sdk/spring/spring-cloud-azure-feature-management-web/pom.xml @@ -127,7 +127,7 @@ - com.azure.spring:spring-cloud-azure-feature-management:[5.23.0-beta.1] + com.azure.spring:spring-cloud-azure-feature-management:[6.0.0-beta.1] jakarta.servlet:jakarta.servlet-api:[6.0.0] org.springframework:spring-web:[6.2.5] org.springframework:spring-webmvc:[6.2.5] From e980eae545b425361b13a76af44a8862ad1420e4 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 4 Jun 2025 12:45:18 -0700 Subject: [PATCH 27/41] Update pom.xml --- sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index 0f2a0bf552a7..4c99e1b33ff2 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -69,12 +69,12 @@ com.azure.spring spring-cloud-azure-service - 5.23.0-beta.1 + 5.22.0 com.azure.spring spring-cloud-azure-autoconfigure - 5.23.0-beta.1 + 5.22.0 From cd2a4e43ce42d89eda76a0135ccec9e7c5687ca4 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 4 Jun 2025 13:45:23 -0700 Subject: [PATCH 28/41] try to fix pom --- eng/versioning/version_client.txt | 3 +++ sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index e22b9197a75b..71f0faa284ee 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -512,3 +512,6 @@ unreleased_com.azure.v2:azure-core;2.0.0-beta.1 # Format; # beta_:;dependency-version # note: Released beta versions will not be manipulated with the automatic PR creation code. + +beta_com.azure.spring:spring-cloud-azure-service;5.22.0 +beta_com.azure.spring:spring-cloud-azure-autoconfigure;5.22.0 \ No newline at end of file diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index 4c99e1b33ff2..99fc51aef912 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -69,12 +69,12 @@ com.azure.spring spring-cloud-azure-service - 5.22.0 + 5.22.0 com.azure.spring spring-cloud-azure-autoconfigure - 5.22.0 + 5.22.0 From d8a1a0e9ebf90af1f5b2c2f665fa40bedfd0de19 Mon Sep 17 00:00:00 2001 From: Azure SDK Bot <53356347+azure-sdk@users.noreply.github.com> Date: Thu, 5 Jun 2025 09:03:41 -0700 Subject: [PATCH 29/41] Increment package versions for spring releases (#45589) --- eng/versioning/version_client.txt | 12 ++++++------ .../pom.xml | 2 +- sdk/spring/spring-cloud-azure-actuator/pom.xml | 2 +- .../CHANGELOG.md | 10 ++++++++++ .../pom.xml | 4 ++-- .../CHANGELOG.md | 10 ++++++++++ .../pom.xml | 2 +- .../CHANGELOG.md | 10 ++++++++++ .../pom.xml | 6 +++--- .../CHANGELOG.md | 10 ++++++++++ .../spring-cloud-azure-feature-management/pom.xml | 2 +- .../pom.xml | 2 +- .../CHANGELOG.md | 10 ++++++++++ .../pom.xml | 6 +++--- 14 files changed, 69 insertions(+), 19 deletions(-) diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index 71f0faa284ee..b7b5e4d53d66 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -201,11 +201,11 @@ com.azure:identity-test-vm;1.0.0-beta.1;1.0.0-beta.1 com.azure:identity-test-webapp;1.0.0-beta.1;1.0.0-beta.1 com.azure:azure-ai-vision-face;1.0.0-beta.2;1.0.0-beta.3 com.azure.spring:azure-monitor-spring-native-test;1.0.0-beta.1;1.0.0-beta.1 -com.azure.spring:spring-cloud-azure-appconfiguration-config-web;5.22.0;6.0.0-beta.1 -com.azure.spring:spring-cloud-azure-appconfiguration-config;5.22.0;6.0.0-beta.1 -com.azure.spring:spring-cloud-azure-feature-management-web;5.22.0;6.0.0-beta.1 -com.azure.spring:spring-cloud-azure-feature-management;5.22.0;6.0.0-beta.1 -com.azure.spring:spring-cloud-azure-starter-appconfiguration-config;5.22.0;6.0.0-beta.1 +com.azure.spring:spring-cloud-azure-appconfiguration-config-web;5.22.0;6.0.0-beta.2 +com.azure.spring:spring-cloud-azure-appconfiguration-config;5.22.0;6.0.0-beta.2 +com.azure.spring:spring-cloud-azure-feature-management-web;5.22.0;6.0.0-beta.2 +com.azure.spring:spring-cloud-azure-feature-management;5.22.0;6.0.0-beta.2 +com.azure.spring:spring-cloud-azure-starter-appconfiguration-config;5.22.0;6.0.0-beta.2 com.azure.spring:spring-cloud-azure-dependencies;5.21.0;5.22.0 com.azure.spring:spring-messaging-azure;5.22.0;5.23.0-beta.1 com.azure.spring:spring-messaging-azure-eventhubs;5.22.0;5.23.0-beta.1 @@ -514,4 +514,4 @@ unreleased_com.azure.v2:azure-core;2.0.0-beta.1 # note: Released beta versions will not be manipulated with the automatic PR creation code. beta_com.azure.spring:spring-cloud-azure-service;5.22.0 -beta_com.azure.spring:spring-cloud-azure-autoconfigure;5.22.0 \ No newline at end of file +beta_com.azure.spring:spring-cloud-azure-autoconfigure;5.22.0 diff --git a/sdk/spring/spring-cloud-azure-actuator-autoconfigure/pom.xml b/sdk/spring/spring-cloud-azure-actuator-autoconfigure/pom.xml index 7fbddb7adc7a..5476872a4779 100644 --- a/sdk/spring/spring-cloud-azure-actuator-autoconfigure/pom.xml +++ b/sdk/spring/spring-cloud-azure-actuator-autoconfigure/pom.xml @@ -62,7 +62,7 @@ com.azure.spring spring-cloud-azure-appconfiguration-config-web - 6.0.0-beta.1 + 6.0.0-beta.2 true diff --git a/sdk/spring/spring-cloud-azure-actuator/pom.xml b/sdk/spring/spring-cloud-azure-actuator/pom.xml index 2c18b477d784..97374d2d9614 100644 --- a/sdk/spring/spring-cloud-azure-actuator/pom.xml +++ b/sdk/spring/spring-cloud-azure-actuator/pom.xml @@ -109,7 +109,7 @@ com.azure.spring spring-cloud-azure-appconfiguration-config-web - 6.0.0-beta.1 + 6.0.0-beta.2 true diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/CHANGELOG.md b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/CHANGELOG.md index 929aa51abc28..4af3ffa5f305 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/CHANGELOG.md @@ -1,5 +1,15 @@ # Release History +## 6.0.0-beta.2 (Unreleased) + +### Features Added + +### Breaking Changes + +### Bugs Fixed + +### Other Changes + ## 5.23.0-beta.1 (Unreleased) ### Features Added diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml index 9626c0119688..9fddd6dd44e1 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml @@ -9,7 +9,7 @@ 4.0.0 com.azure.spring spring-cloud-azure-appconfiguration-config-web - 6.0.0-beta.1 + 6.0.0-beta.2 Spring Cloud Azure App Configuration Config Web Integration of Spring Cloud Config and Azure App Configuration Service @@ -21,7 +21,7 @@ com.azure.spring spring-cloud-azure-appconfiguration-config - 6.0.0-beta.1 + 6.0.0-beta.2 org.springframework.boot diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md b/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md index 5e12f534b577..127d8593896f 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md @@ -1,5 +1,15 @@ # Release History +## 6.0.0-beta.2 (Unreleased) + +### Features Added + +### Breaking Changes + +### Bugs Fixed + +### Other Changes + ## 5.23.0-beta.1 (Unreleased) ### Features Added diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index 99fc51aef912..ff186ff7945b 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -9,7 +9,7 @@ 4.0.0 com.azure.spring spring-cloud-azure-appconfiguration-config - 6.0.0-beta.1 + 6.0.0-beta.2 Spring Cloud Azure App Configuration Config Integration of Spring Cloud Config and Azure App Configuration Service diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/CHANGELOG.md b/sdk/spring/spring-cloud-azure-feature-management-web/CHANGELOG.md index f1231c0244fc..5d7e165f83a8 100644 --- a/sdk/spring/spring-cloud-azure-feature-management-web/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-feature-management-web/CHANGELOG.md @@ -1,5 +1,15 @@ # Release History +## 6.0.0-beta.2 (Unreleased) + +### Features Added + +### Breaking Changes + +### Bugs Fixed + +### Other Changes + ## 5.23.0-beta.1 (Unreleased) ### Features Added diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/pom.xml b/sdk/spring/spring-cloud-azure-feature-management-web/pom.xml index e72569c2c2dd..0ae957a882b5 100644 --- a/sdk/spring/spring-cloud-azure-feature-management-web/pom.xml +++ b/sdk/spring/spring-cloud-azure-feature-management-web/pom.xml @@ -8,7 +8,7 @@ 4.0.0 com.azure.spring spring-cloud-azure-feature-management-web - 6.0.0-beta.1 + 6.0.0-beta.2 Spring Cloud Azure Feature Management Web Adds Feature Management into Spring Web @@ -39,7 +39,7 @@ com.azure.spring spring-cloud-azure-feature-management - 6.0.0-beta.1 + 6.0.0-beta.2 jakarta.servlet @@ -127,7 +127,7 @@ - com.azure.spring:spring-cloud-azure-feature-management:[6.0.0-beta.1] + com.azure.spring:spring-cloud-azure-feature-management:[6.0.0-beta.2] jakarta.servlet:jakarta.servlet-api:[6.0.0] org.springframework:spring-web:[6.2.5] org.springframework:spring-webmvc:[6.2.5] diff --git a/sdk/spring/spring-cloud-azure-feature-management/CHANGELOG.md b/sdk/spring/spring-cloud-azure-feature-management/CHANGELOG.md index d6c1fc5f38da..13d1f4d41cf9 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-feature-management/CHANGELOG.md @@ -1,5 +1,15 @@ # Release History +## 6.0.0-beta.2 (Unreleased) + +### Features Added + +### Breaking Changes + +### Bugs Fixed + +### Other Changes + ## 5.23.0-beta.1 (Unreleased) ### Features Added diff --git a/sdk/spring/spring-cloud-azure-feature-management/pom.xml b/sdk/spring/spring-cloud-azure-feature-management/pom.xml index 471c20e9ee36..3b212066bacb 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/pom.xml +++ b/sdk/spring/spring-cloud-azure-feature-management/pom.xml @@ -8,7 +8,7 @@ 4.0.0 com.azure.spring spring-cloud-azure-feature-management - 6.0.0-beta.1 + 6.0.0-beta.2 Spring Cloud Azure Feature Management Adds Feature Management into Spring diff --git a/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/pom.xml index 6cbaed15e7fd..12cd41d63c24 100644 --- a/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/pom.xml @@ -21,7 +21,7 @@ com.azure.spring spring-cloud-azure-starter-appconfiguration-config - 6.0.0-beta.1 + 6.0.0-beta.2 org.springframework.boot diff --git a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/CHANGELOG.md b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/CHANGELOG.md index c4f730d0c756..ad390e4c6a33 100644 --- a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/CHANGELOG.md @@ -1,5 +1,15 @@ # Release History +## 6.0.0-beta.2 (Unreleased) + +### Features Added + +### Breaking Changes + +### Bugs Fixed + +### Other Changes + ## 5.23.0-beta.1 (Unreleased) ### Features Added diff --git a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/pom.xml index 7a92bf6b71e7..76b4433df5b3 100644 --- a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/pom.xml @@ -6,7 +6,7 @@ com.azure.spring spring-cloud-azure-starter-appconfiguration-config - 6.0.0-beta.1 + 6.0.0-beta.2 Spring Cloud Azure Starter App Configuration Config Spring Cloud Azure Starter App Configuration Config https://microsoft.github.io/spring-cloud-azure @@ -88,12 +88,12 @@ com.azure.spring spring-cloud-azure-appconfiguration-config-web - 6.0.0-beta.1 + 6.0.0-beta.2 com.azure.spring spring-cloud-azure-feature-management-web - 6.0.0-beta.1 + 6.0.0-beta.2 From 1c0b4cdb82933f70b8160e12727c616a932b6f89 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 5 Aug 2025 10:03:11 -0700 Subject: [PATCH 30/41] Undoing change from beta release --- sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index ff186ff7945b..4577be9c0a9b 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -69,12 +69,12 @@ com.azure.spring spring-cloud-azure-service - 5.22.0 + 5.22.0 com.azure.spring spring-cloud-azure-autoconfigure - 5.22.0 + 5.22.0 From c4960f0ba2da4218a533d5db7be14f81e6e801a8 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 5 Aug 2025 10:03:28 -0700 Subject: [PATCH 31/41] Updating provider readme --- .../README.md | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md index b700be31c77c..d1d222dfb426 100644 --- a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md +++ b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md @@ -21,7 +21,7 @@ There are two libraries that can be used spring-cloud-azure-appconfiguration-con com.azure.spring spring-cloud-azure-appconfiguration-config - 5.22.0 + 6.0.0 ``` [//]: # ({x-version-update-end}) @@ -33,7 +33,7 @@ or com.azure.spring spring-cloud-azure-appconfiguration-config-web - 5.22.0 + 6.0.0 ``` [//]: # ({x-version-update-end}) @@ -48,6 +48,10 @@ Please use this `sample` as a reference for how to use this starter. ### Supported properties +Name | Description | Required | Default +---|---|---|--- +spring.config.import | The Spring property that triggers the loading of Azure App Configuration properties. It need to contain the value `azureAppConfiguration` | yes | None + Name | Description | Required | Default ---|---|---|--- spring.cloud.azure.appconfiguration.stores | List of configuration stores from which to load configuration properties | Yes | true @@ -68,10 +72,17 @@ spring.cloud.azure.appconfiguration.stores[0].replicaDiscoveryEnabled | Enables Configuration Store Authentication +By default when connecting to Azure App Configuration, if no credential is provided, the `DefaultAzureCredential` will be used. + Name | Description | Required | Default ---|---|---|--- spring.cloud.azure.appconfiguration.stores[0].endpoint | When the endpoint of an App Configuration store is specified, a managed identity or a token credential provided using `AppConfigCredentialProvider` will be used to connect to the App Configuration service. An `IllegalArgumentException` will be thrown if the endpoint and connection-string are specified at the same time. | Conditional | null spring.cloud.azure.appconfiguration.stores[0].endpoints | When multiple replica endpoints of an App Configuration store are specified, a managed identity or a token credential provided using `AppConfigCredentialProvider` will be used to connect to the App Configuration service. Replica endpoints should be listed in priority order of connection. An `IllegalArgumentException` will be thrown if multiple authentication methods are provided. | Conditional | null + +Additionally, you can connect to Azure App Configuration using a connection string. But this method is not recommended. + +Name | Description | Required | Default +---|---|---|--- spring.cloud.azure.appconfiguration.stores[0].connection-string | When the connection-string of an App Configuration store is specified, HMAC authentication will be used to connect to the App Configuration service. An `IllegalArgumentException` will be thrown if the endpoint and connection-string are specified at the same time. | Conditional | null spring.cloud.azure.appconfiguration.stores[0].connection-strings | When the connection-strings of an App Configuration store is specified, HMAC authentication will be used to connect to the App Configuration service. Replica stores should be listed in priority order of connection. An `IllegalArgumentException` will be thrown if the endpoint and connection-string are specified at the same time. | Conditional | null @@ -84,6 +95,11 @@ spring.cloud.azure.appconfiguration.stores[0].monitoring.refresh-interval | Amou spring.cloud.azure.appconfiguration.stores[0].monitoring.feature-flag-refresh-interval | Amount of time, of type Duration, feature flags are stored before a check can occur. | No | 30s spring.cloud.azure.appconfiguration.stores[0].monitoring.triggers[0].key | A key that is watched for change via etag. If a change is detected on the key then a refresh of all configurations will be triggered. | Yes (If monitoring enabled) | null spring.cloud.azure.appconfiguration.stores[0].monitoring.triggers[0].label | The label of the key that is being watched for etag changes. | No | \0 + +These properties enable push-based notifications for configuration changes. But this method of refresh is no-longer recommended, but is currently still supported. + +Name | Description | Required | Default +---|---|---|--- spring.cloud.azure.appconfiguration.stores[0].monitoring.push-notification.primary-token.name | The name of a token used with Event Hub to trigger push based refresh. | No | null spring.cloud.azure.appconfiguration.stores[0].monitoring.push-notification.primary-token.secret | The secret value of a token used with Event Hub to trigger push based refresh. | No | null spring.cloud.azure.appconfiguration.stores[0].monitoring.push-notification.secondary-token.name | The name of a token used with Event Hub to trigger push based refresh. | No | null @@ -97,6 +113,17 @@ spring.cloud.azure.appconfiguration.stores[0].feature-flags.enabled | Whether fe spring.cloud.azure.appconfiguration.stores[0].feature-flags.selects[0].key-filter | The key pattern used to indicate which feature flags will be loaded. | No | \0 spring.cloud.azure.appconfiguration.stores[0].feature-flags.selects[0].label-filter | The label used to indicate which feature flags will be loaded. | No | \0 +### Basic usage + +Enabling the Azure App Configuration integration is as simple as adding the necessary dependencies, listed above, and the following configuration properties. + +```properties +spring.config.import=azureAppConfiguration +spring.cloud.azure.appconfiguration.stores[0].endpoint=[your-endpoint] +``` + +With these settings in place, your application will be able to connect to Azure App Configuration and retrieve configuration values. The default values loaded with these properties are all configuration that start with the value `/application/` and have no label. + ### Advanced usage #### Geo-Replication @@ -119,7 +146,7 @@ The failover won't happen for client errors like authentication failures. #### Load from multiple configuration stores -If the application needs to load configuration properties from multiple stores, following configuration sample describes how the bootstrap.properties(or .yaml) can be configured. +If the application needs to load configuration properties from multiple stores, following configuration sample describes how the application.properties(or .yaml) can be configured. ```properties spring.cloud.azure.appconfiguration.stores[0].connection-string=[first-store-connection-string] @@ -266,6 +293,8 @@ In the console library calling refreshConfiguration on `AppConfigurationRefresh` ##### Push Based Refresh +NOTE: This method of refresh is no longer recommended, but is still supported. + The Web Provider can be connect to your Azure App Configuration store via an Azure Event Grid Web Hook to trigger a refresh event. By adding the Spring Actuator as a dependency you can add App Configuration Refresh as an exposed endpoint. There are two options appconfiguration-refresh and appconfiguration-refresh-bus. These endpoints work just like there counterparts refresh and refresh-bus, but have the required web hook authorization to work with Azure Event Grid. When needing to refresh multiple application instances `azure-servicebus-jms-spring-boot-starter` needs to be setup to have the refresh triggered in all instances. ```properties @@ -320,11 +349,11 @@ Follow the below steps to enable accessing App Configuration with managed identi 1. Configure the [Azure RBAC][azure_rbac] of your App Configuration store to grant access to the Azure service where your application is running. Select the App Configuration Data Reader. The App Configuration Data Owner role is not required but can be used if needed. -1. Configure bootstrap.properties(or .yaml) in the Spring Boot application. +1. Configure application.properties(or .yaml) in the Spring Boot application. The configuration store endpoint must be configured when `connection-string` is empty. When using a User Assigned Id the value `spring.cloud.azure.appconfiguration.managed-identity.client-id=[client-id]` must be set. -##### bootstrap.application +##### application.application ```application spring.cloud.azure.appconfiguration.stores[0].endpoint=[config-store-endpoint] From 1fe15775df33a33299f27ee73d1c416db68af6f5 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 5 Aug 2025 10:03:47 -0700 Subject: [PATCH 32/41] Update version_client.txt --- eng/versioning/version_client.txt | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index b7b5e4d53d66..087932429d25 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -201,11 +201,11 @@ com.azure:identity-test-vm;1.0.0-beta.1;1.0.0-beta.1 com.azure:identity-test-webapp;1.0.0-beta.1;1.0.0-beta.1 com.azure:azure-ai-vision-face;1.0.0-beta.2;1.0.0-beta.3 com.azure.spring:azure-monitor-spring-native-test;1.0.0-beta.1;1.0.0-beta.1 -com.azure.spring:spring-cloud-azure-appconfiguration-config-web;5.22.0;6.0.0-beta.2 -com.azure.spring:spring-cloud-azure-appconfiguration-config;5.22.0;6.0.0-beta.2 -com.azure.spring:spring-cloud-azure-feature-management-web;5.22.0;6.0.0-beta.2 -com.azure.spring:spring-cloud-azure-feature-management;5.22.0;6.0.0-beta.2 -com.azure.spring:spring-cloud-azure-starter-appconfiguration-config;5.22.0;6.0.0-beta.2 +com.azure.spring:spring-cloud-azure-appconfiguration-config-web;6.0.0;6.0.0-beta.2 +com.azure.spring:spring-cloud-azure-appconfiguration-config;6.0.0;6.0.0-beta.2 +com.azure.spring:spring-cloud-azure-feature-management-web;6.0.0;6.0.0-beta.2 +com.azure.spring:spring-cloud-azure-feature-management;6.0.0;6.0.0-beta.2 +com.azure.spring:spring-cloud-azure-starter-appconfiguration-config;6.0.0;6.0.0-beta.2 com.azure.spring:spring-cloud-azure-dependencies;5.21.0;5.22.0 com.azure.spring:spring-messaging-azure;5.22.0;5.23.0-beta.1 com.azure.spring:spring-messaging-azure-eventhubs;5.22.0;5.23.0-beta.1 @@ -512,6 +512,3 @@ unreleased_com.azure.v2:azure-core;2.0.0-beta.1 # Format; # beta_:;dependency-version # note: Released beta versions will not be manipulated with the automatic PR creation code. - -beta_com.azure.spring:spring-cloud-azure-service;5.22.0 -beta_com.azure.spring:spring-cloud-azure-autoconfigure;5.22.0 From f2d378cf5c796f9212ba4c13a4885f1844d0e61e Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 6 Aug 2025 10:17:56 -0700 Subject: [PATCH 33/41] Update external_dependencies.txt --- eng/versioning/external_dependencies.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index c379d40a2864..7e8a7bed1b21 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -412,6 +412,8 @@ springboot3_org.springframework:spring-web;6.2.9 springboot3_org.springframework:spring-webmvc;6.2.9 springboot3_org.testcontainers:junit-jupiter;1.21.3 springboot3_org.testcontainers:azure;1.21.3 +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.5.4 springboot3_org.springframework.cloud:spring-cloud-dependencies;2025.0.0 From 07ab3ae0ee9541eed591416bb142e3da918c9ed2 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 6 Aug 2025 10:39:00 -0700 Subject: [PATCH 34/41] Update pom.xml --- sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index 0dfbf7c2fad3..db5427decb7d 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -183,6 +183,7 @@ org.springframework.boot:spring-boot-autoconfigure:[3.5.4] org.springframework.cloud:spring-cloud-context:[4.3.0] org.springframework.boot:spring-boot-configuration-processor:[3.5.4] + jakarta.annotation:jakarta.annotation-api:jar:[3.0.0] From b61c3759660630bb82baec0d3512c06ccb206a52 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 6 Aug 2025 10:44:17 -0700 Subject: [PATCH 35/41] Update pom.xml --- sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index db5427decb7d..824ac7e53121 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -183,7 +183,7 @@ org.springframework.boot:spring-boot-autoconfigure:[3.5.4] org.springframework.cloud:spring-cloud-context:[4.3.0] org.springframework.boot:spring-boot-configuration-processor:[3.5.4] - jakarta.annotation:jakarta.annotation-api:jar:[3.0.0] + jakarta.annotation:jakarta.annotation-api:[3.0.0] From 6d6dee588ff8d0c04633923a0afdc1f4dca2de55 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 6 Aug 2025 11:06:15 -0700 Subject: [PATCH 36/41] Update pom.xml --- sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index 824ac7e53121..810200c37899 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -183,7 +183,7 @@ org.springframework.boot:spring-boot-autoconfigure:[3.5.4] org.springframework.cloud:spring-cloud-context:[4.3.0] org.springframework.boot:spring-boot-configuration-processor:[3.5.4] - jakarta.annotation:jakarta.annotation-api:[3.0.0] + jakarta.annotation:jakarta.annotation-api:[3.0.0] From b1b0d51874ea89f5113c4634d7880f319ce53836 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 6 Aug 2025 11:21:42 -0700 Subject: [PATCH 37/41] Fixing deprecation --- .../config/implementation/FeatureFlagClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 31892d0a4e18..4dc95a48144b 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 @@ -240,7 +240,7 @@ static String generateAllocationId(JsonNode featureFlagValue) { variantsNode.forEach(variantNode -> { if (variantNode.isObject()) { Map variantMap = new LinkedHashMap<>(); - variantNode.fields().forEachRemaining(entry -> variantMap.put(entry.getKey(), entry.getValue())); + variantNode.properties().iterator().forEachRemaining(entry -> variantMap.put(entry.getKey(), entry.getValue())); variantsValue.add(variantMap); } }); From bded5105548e676074b520c1423b523e3125a213 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 7 Aug 2025 09:41:49 -0700 Subject: [PATCH 38/41] Copilot fixes --- .../FeatureManagementProperties.java | 1 - .../models/VariantAssignmentReason.java | 18 ++++++++++-------- .../telemetry/LoggerTelemetryPublisher.java | 8 -------- .../telemetry/TelemetryConstants.java | 8 +------- .../LoggerTelemetryPublisherTest.java | 4 ++-- 5 files changed, 13 insertions(+), 26 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementProperties.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementProperties.java index 70476797637c..f7e7cc3185ce 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementProperties.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementProperties.java @@ -35,7 +35,6 @@ public List getFeatureFlags() { */ public void setFeatureFlags(List featureFlags) { if (featureFlags == null || featureFlags.isEmpty()) { - System.out.println("No feature flags provided, setting empty list"); this.featureFlags = List.of(); return; } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/VariantAssignmentReason.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/VariantAssignmentReason.java index 4d1abbc64280..dc030a2c71e8 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/VariantAssignmentReason.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/VariantAssignmentReason.java @@ -4,8 +4,8 @@ package com.azure.spring.cloud.feature.management.models; /** - * The reason why a given boolean/variant was returned when calling isEnabled/getVariant. - * This enum represents the different filtering mechanisms that determined the feature flag state. + * The reason why a given boolean/variant was returned when calling isEnabled/getVariant. This enum represents the + * different filtering mechanisms that determined the feature flag state. */ public enum VariantAssignmentReason { @@ -13,31 +13,33 @@ public enum VariantAssignmentReason { * Indicates no specific reason was assigned for the feature flag evaluation. */ NONE("None"), - + /** * Indicates the feature flag was evaluated based on the default value when the flag is disabled. */ DEFAULT_WHEN_DISABLED("DefaultWhenDisabled"), - + /** * Indicates the feature flag was evaluated based on the default value when the flag is enabled. */ DEFAULT_WHEN_ENABLED("DefaultWhenEnabled"), - + /** * Indicates the feature flag was evaluated based on user targeting criteria. */ USER("User"), - + /** * Indicates the feature flag was evaluated based on group targeting criteria. */ GROUP("Group"), - + /** * Indicates the feature flag was evaluated based on percentile targeting criteria. */ - PERCENTILE("Percentile"); private final String type; + PERCENTILE("Percentile"); + + private final String type; /** * Creates a new instance of the VariantAssignmentReason enum with the specified type. diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/LoggerTelemetryPublisher.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/LoggerTelemetryPublisher.java index 1c46ef143a6d..e532a8f28223 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/LoggerTelemetryPublisher.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/LoggerTelemetryPublisher.java @@ -39,14 +39,6 @@ public class LoggerTelemetryPublisher implements TelemetryPublisher { private static final Logger LOGGER = LoggerFactory.getLogger(LoggerTelemetryPublisher.class); - /** - * The name of the custom event for telemetry logging. This is used to - * identify the type of event being logged in the telemetry system. - */ - public LoggerTelemetryPublisher() { - // Private constructor to prevent instantiation - } - /** * Publishes telemetry events related to feature evaluations. It logs the * evaluation event using SLF4J, adding contextual information to the logs using diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/TelemetryConstants.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/TelemetryConstants.java index 54be2d2f09e7..c1b909fb18a7 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/TelemetryConstants.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/TelemetryConstants.java @@ -30,13 +30,7 @@ public final class TelemetryConstants { static final String APPLICATION_INSIGHTS_CUSTOM_EVENT_KEY = "microsoft.custom_event.name"; - /** - * This class contains constants used for telemetry events related to feature - * evaluations. These constants are used to define the structure and content of - * telemetry events, including event names, property names, and versioning - * information. - */ + /** Private constructor to prevent instantiation. */ private TelemetryConstants() { - // Private constructor to prevent instantiation } } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/telemetry/LoggerTelemetryPublisherTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/telemetry/LoggerTelemetryPublisherTest.java index e826f873cf09..ff9d354323df 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/telemetry/LoggerTelemetryPublisherTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/telemetry/LoggerTelemetryPublisherTest.java @@ -271,8 +271,8 @@ void emptyPercentileAllocationTest(TestInfo testInfo) { assertEquals("0.0", mdcMap.get(VARIANT_ASSIGNMENT_PERCENTAGE)); } - ILoggingEvent getEvent(List evnets, String featureName) { - for (ILoggingEvent event : evnets) { + ILoggingEvent getEvent(List events, String featureName) { + for (ILoggingEvent event : events) { if (featureName.equals(event.getMDCPropertyMap().get(FEATURE_NAME))) { return event; } From df1ffc7700baae0d0e37df3d39b988857e9509b6 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 7 Aug 2025 15:21:30 -0700 Subject: [PATCH 39/41] Update LoggerTelemetryPublisher.java --- .../management/telemetry/LoggerTelemetryPublisher.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/LoggerTelemetryPublisher.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/LoggerTelemetryPublisher.java index e532a8f28223..39b8248d16f2 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/LoggerTelemetryPublisher.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/telemetry/LoggerTelemetryPublisher.java @@ -39,6 +39,13 @@ public class LoggerTelemetryPublisher implements TelemetryPublisher { private static final Logger LOGGER = LoggerFactory.getLogger(LoggerTelemetryPublisher.class); + /** + * Constructor for LoggerTelemetryPublisher. + */ + public LoggerTelemetryPublisher() { + + } + /** * Publishes telemetry events related to feature evaluations. It logs the * evaluation event using SLF4J, adding contextual information to the logs using From da8508df67584aeafe3f38ae7a7be495fc9ee6ef Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 12 Aug 2025 15:43:18 -0700 Subject: [PATCH 40/41] code review items --- .../config/implementation/FeatureFlagClient.java | 7 ++++++- .../config/implementation/JsonConfigurationParser.java | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) 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 4dc95a48144b..9c4f5433e02c 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 @@ -27,6 +27,8 @@ import java.util.TreeMap; import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @@ -52,6 +54,9 @@ @Component class FeatureFlagClient { + private static final Logger LOGGER = LoggerFactory + .getLogger(FeatureFlagClient.class); + private final Map properties = new LinkedHashMap<>(); private static final ObjectMapper CASE_INSENSITIVE_MAPPER = JsonMapper.builder() @@ -150,7 +155,7 @@ protected static Feature createFeature(FeatureFlagConfigurationSetting item, Str } } } catch (JsonProcessingException e) { - + LOGGER.error("Error parsing feature flag value for key: {}", item.getKey(), e); } return feature; } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/JsonConfigurationParser.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/JsonConfigurationParser.java index 78b6e5c7bfea..824f111ae238 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/JsonConfigurationParser.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/JsonConfigurationParser.java @@ -13,11 +13,14 @@ import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.json.JsonReadFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; final class JsonConfigurationParser { - private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static final ObjectMapper MAPPER = JsonMapper.builder().enable(JsonReadFeature.ALLOW_JAVA_COMMENTS).build(); static boolean isJsonContentType(String contentType) { String acceptedMainType = "application"; From 6d86973241a9eb0971113c4353e1a64179e20fe7 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 25 Aug 2025 13:14:31 -0700 Subject: [PATCH 41/41] Fixing feature name setting + adding tests --- .../feature/management/FeatureManager.java | 2 +- .../management/FeatureManagerTest.java | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java index 2ef769e0e1fd..7a9926df68f8 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java @@ -388,6 +388,7 @@ private Mono checkFeatureFilters(EvaluationEvent event, Object List> filterResults = new ArrayList>(); for (FeatureFilterEvaluationContext featureFilter : featureFilters) { String filterName = featureFilter.getName(); + featureFilter.setFeatureName(event.getFeature().getId()); filterResults.add(evaluateFilter(featureFilter, featureContext, filterName)); } @@ -444,7 +445,6 @@ private Mono evaluateFilter(FeatureFilterEvaluationContext featureFilte String filterName) { try { Object filter = context.getBean(filterName); - featureFilter.setFeatureName(featureFilter.getFeatureName()); if (filter instanceof FeatureFilter) { return Mono.just(((FeatureFilter) filter).evaluate(featureFilter)); } else if (filter instanceof ContextualFeatureFilter) { diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java index ec7d9a03fb31..74b806494a72 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/FeatureManagerTest.java @@ -3,6 +3,7 @@ package com.azure.spring.cloud.feature.management; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -56,6 +57,9 @@ public class FeatureManagerTest { @Mock private TelemetryPublisher telemetryPublisher; + + @Mock + private FeatureFilter filterMock; @BeforeEach public void setup() { @@ -107,6 +111,24 @@ public void isEnabledOnContext() throws InterruptedException, ExecutionException assertFalse(featureManager.isEnabled("On", false)); assertFalse(featureManager.isEnabledAsync("On", false).block()); } + + @Test + public void validateFeatureName() { + FeatureFilterEvaluationContext filterContext = new FeatureFilterEvaluationContext().setName("AlwaysOnContext"); + List features = List.of(new Feature().setId("On").setEnabled(true).setConditions(new Conditions() + .setClientFilters(List.of(filterContext)))); + + when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); + + when(context.getBean(Mockito.matches("AlwaysOnContext"))).thenReturn(filterMock); + + when(filterMock.evaluate(Mockito.eq(filterContext))).thenReturn(true); + + assertTrue(featureManager.isEnabled("On")); + assertEquals(filterContext.getFeatureName(), "On"); + assertTrue(featureManager.isEnabledAsync("On").block()); + assertEquals(filterContext.getFeatureName(), "On"); + } @Test public void isEnabledFeatureHasNoFilters() { @@ -214,7 +236,9 @@ public void timeWindowFilter() { when(featureManagementPropertiesMock.getFeatureFlags()).thenReturn(features); when(context.getBean(Mockito.matches("TimeWindowFilter"))).thenReturn(new TimeWindowFilter()); + assertEquals(null, weeklyAlwaysOn.getFeatureName()); assertTrue(featureManager.isEnabled("Alpha")); + assertEquals("Alpha", weeklyAlwaysOn.getFeatureName()); } @Test