Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,11 @@ public class AppConfigurationConstants {
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";

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
package com.azure.spring.cloud.appconfiguration.config.implementation;

import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.AUDIENCE;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.DEFAULT_REQUIREMENT_TYPE;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.DEFAULT_ROLLOUT_PERCENTAGE;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.DEFAULT_ROLLOUT_PERCENTAGE_CAPS;
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_PREFIX;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_MANAGEMENT_KEY;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.GROUPS;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.GROUPS_CAPS;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.REQUIREMENT_TYPE_SERVICE;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.SELECT_ALL_FEATURE_FLAGS;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.TARGETING_FILTER;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.USERS;
Expand All @@ -32,7 +34,9 @@
import com.azure.data.appconfiguration.models.SettingSelector;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.entity.Feature;
import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.TracingInfo;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
Expand All @@ -50,7 +54,6 @@ final class AppConfigurationFeatureManagementPropertySource extends AppConfigura
.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build();

private final List<ConfigurationSetting> featureConfigurationSettings;

AppConfigurationFeatureManagementPropertySource(String originEndpoint, AppConfigurationReplicaClient replicaClient,
String keyFilter, String[] labelFilter) {
super("FM_" + originEndpoint, replicaClient, keyFilter, labelFilter);
Expand Down Expand Up @@ -127,7 +130,17 @@ List<ConfigurationSetting> getFeatureFlagSettings() {
@SuppressWarnings("unchecked")
private Object createFeature(FeatureFlagConfigurationSetting item) {
String key = getFeatureSimpleName(item);
Feature feature = new Feature(key, item);
String requirementType = DEFAULT_REQUIREMENT_TYPE;
try {
JsonNode node = CASE_INSENSITIVE_MAPPER.readTree(item.getValue());
JsonNode conditions = node.get("conditions");
if (conditions != null && conditions.get(REQUIREMENT_TYPE_SERVICE) != null) {
requirementType = conditions.get(REQUIREMENT_TYPE_SERVICE).asText();
}
} catch (JsonProcessingException e) {

}
Feature feature = new Feature(key, item, requirementType);
Map<Integer, FeatureFlagFilter> featureEnabledFor = feature.getEnabledFor();

// Setting Enabled For to null, but enabled = true will result in the feature
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
// Licensed under the MIT License.
package com.azure.spring.cloud.appconfiguration.config.implementation.feature.entity;

import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.DEFAULT_REQUIREMENT_TYPE;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.REQUIREMENT_TYPE;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting;
import com.azure.data.appconfiguration.models.FeatureFlagFilter;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

Expand All @@ -21,9 +23,12 @@ public final class Feature {
@JsonProperty("key")
private String key;

@JsonAlias("enabled-for")
@JsonProperty("enabled-for")
private Map<Integer, FeatureFlagFilter> enabledFor;

@JsonProperty(REQUIREMENT_TYPE)
private String requirementType = DEFAULT_REQUIREMENT_TYPE;

/**
* Feature Flag object.
*/
Expand All @@ -36,7 +41,7 @@ public Feature() {
* @param key Name of the Feature Flag
* @param featureItem Configurations of the Feature Flag.
*/
public Feature(String key, FeatureFlagConfigurationSetting featureItem) {
public Feature(String key, FeatureFlagConfigurationSetting featureItem, String requirementType) {
this.key = key;
List<FeatureFlagFilter> filterMapper = featureItem.getClientFilters();

Expand All @@ -45,6 +50,7 @@ public Feature(String key, FeatureFlagConfigurationSetting featureItem) {
for (int i = 0; i < filterMapper.size(); i++) {
enabledFor.put(i, filterMapper.get(i));
}
this.requirementType = requirementType;
}

/**
Expand Down Expand Up @@ -75,4 +81,18 @@ public void setEnabledFor(Map<Integer, FeatureFlagFilter> 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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
import com.azure.data.appconfiguration.ConfigurationAsyncClient;
import com.azure.data.appconfiguration.models.ConfigurationSetting;
import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.entity.Feature;
import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.TracingInfo;
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;
Expand Down Expand Up @@ -347,7 +349,50 @@ public void storeCreatedWithFeatureFlags() {
featureFlagStore.setEnabled(true);
featureFlagStore.validateAndInit();

List<ConfigurationSetting> featureList = new ArrayList<>();
FeatureFlagConfigurationSetting featureFlag = new FeatureFlagConfigurationSetting("Alpha", false);
featureFlag.setValue("{}");
featureList.add(featureFlag);

when(configStoreMock.getFeatureFlags()).thenReturn(featureFlagStore);
when(replicaClientMock.listSettings(Mockito.any())).thenReturn(featureList);

locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory,
null, stores);

try (MockedStatic<StateHolder> stateHolderMock = Mockito.mockStatic(StateHolder.class)) {
stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null);
PropertySource<?> source = locator.locate(emptyEnvironment);
assertTrue(source instanceof CompositePropertySource);

Collection<PropertySource<?>> sources = ((CompositePropertySource) source).getPropertySources();
// Application name: foo and active profile: dev,prod, should construct below
// composite Property Source:
// [/foo_prod/, /foo_dev/, /foo/, /application_prod/, /application_dev/,
// /application/]
String[] expectedSourceNames = new String[] {
"FM_store1/",
KEY_FILTER + "store1/\0"
};
assertEquals(expectedSourceNames.length, sources.size());
assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray());
}
}

@Test
public void storeCreatedWithFeatureFlagsRequireAll() {
FeatureFlagStore featureFlagStore = new FeatureFlagStore();
featureFlagStore.setEnabled(true);
featureFlagStore.validateAndInit();

List<ConfigurationSetting> featureList = new ArrayList<>();
FeatureFlagConfigurationSetting featureFlag = new FeatureFlagConfigurationSetting("Alpha", true);
featureFlag.setValue("{\"id\":null,\"description\":null,\"display_name\":null,\"enabled\":true,\"conditions\":{\"requirement_type\":\"All\", \"client_filters\":[{\"name\":\"AlwaysOn\",\"parameters\":{}}]}}");
featureList.add(featureFlag);

when(configStoreMock.getFeatureFlags()).thenReturn(featureFlagStore);
when(replicaClientMock.listSettings(Mockito.any())).thenReturn(featureList);
when(replicaClientMock.getTracingInfo()).thenReturn(new TracingInfo(false, false, 0, null));

locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory,
null, stores);
Expand All @@ -367,7 +412,11 @@ public void storeCreatedWithFeatureFlags() {
KEY_FILTER + "store1/\0"
};
assertEquals(expectedSourceNames.length, sources.size());
Object[] propertySources = sources.stream().map(c -> c.getProperty("feature-management.Alpha")).toArray();
Feature alpha = (Feature) propertySources[0];
assertEquals("All", alpha.getRequirementType());
assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray());

}
}

Expand All @@ -381,6 +430,7 @@ public void storeCreatedWithFeatureFlagsWithMonitoring() {

List<ConfigurationSetting> featureList = new ArrayList<>();
FeatureFlagConfigurationSetting featureFlag = new FeatureFlagConfigurationSetting("Alpha", false);
featureFlag.setValue("{}");
featureList.add(featureFlag);

when(configStoreMock.getFeatureFlags()).thenReturn(featureFlagStore);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -50,9 +51,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
Expand All @@ -63,9 +64,9 @@ public Mono<Boolean> 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
Expand Down Expand Up @@ -93,9 +94,16 @@ private boolean checkFeature(String feature) throws FilterNotFoundException {
return false;
}

return featureItem.getEnabledFor().values().stream().filter(Objects::nonNull)
.filter(featureFilter -> featureFilter.getName() != null)
.anyMatch(featureFilter -> isFeatureOn(featureFilter, feature));
Stream<FeatureFilterEvaluationContext> filters = featureItem.getEnabledFor().values().stream()
.filter(Objects::nonNull).filter(featureFilter -> featureFilter.getName() != null);

// All Filters must be true
if (featureItem.getRequirementType().equals("All")) {
return filters.allMatch(featureFilter -> isFeatureOn(featureFilter, feature));
}

// Any Filter must be true
return filters.anyMatch(featureFilter -> isFeatureOn(featureFilter, feature));
}

private boolean isFeatureOn(FeatureFilterEvaluationContext filter, String feature) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.spring.cloud.feature.management.implementation;

public class FeatureManagementConstants {

public static final String DEFAULT_REQUIREMENT_TYPE = "Any";

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
// 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.HashMap;
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;
import java.util.HashMap;

/**
* App Configuration Feature defines the feature name and a Map of FeatureFilterEvaluationContexts.
Expand All @@ -19,8 +23,11 @@ public class Feature {
@JsonProperty("evaluate")
private Boolean evaluate = true;

@JsonProperty("requirement-type")
private String requirementType = DEFAULT_REQUIREMENT_TYPE;;

@JsonProperty("enabled-for")
private HashMap<Integer, FeatureFilterEvaluationContext> enabledFor;
private Map<Integer, FeatureFilterEvaluationContext> enabledFor;

/**
* @return the key
Expand Down Expand Up @@ -53,7 +60,7 @@ public void setEvaluate(Boolean evaluate) {
/**
* @return the enabledFor
*/
public HashMap<Integer, FeatureFilterEvaluationContext> getEnabledFor() {
public Map<Integer, FeatureFilterEvaluationContext> getEnabledFor() {
return enabledFor;
}

Expand All @@ -64,4 +71,18 @@ public void setEnabledFor(HashMap<Integer, FeatureFilterEvaluationContext> enabl
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;
}

}
Loading