From 8dd4aaed48c49ec4c54f1d98a4e00084a3400297 Mon Sep 17 00:00:00 2001 From: Matthew Metcalf Date: Mon, 16 Dec 2024 11:20:59 -0800 Subject: [PATCH 01/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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