Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support backend schema #38134

Merged
Merged
Show file tree
Hide file tree
Changes from 12 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 @@ -19,7 +19,7 @@ class FeatureManagementConfiguration {

ivywei0125 marked this conversation as resolved.
Show resolved Hide resolved
/**
* Creates Feature Manager
*
*
* @param context ApplicationContext
* @param featureManagementConfigurations Configuration Properties for Feature Flags
* @param properties Feature Management configuration properties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public class FeatureManager {

/**
* Can be called to check if a feature is enabled or disabled.
*
*
* @param context ApplicationContext
* @param featureManagementConfigurations Configuration Properties for Feature Flags
* @param properties FeatureManagementConfigProperties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.HashMap;
import java.util.Map;

import com.azure.spring.cloud.feature.management.implementation.models.ServerSideFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
Expand Down Expand Up @@ -40,41 +41,58 @@ public FeatureManagementProperties() {
featureManagement = new HashMap<>();
onOff = new HashMap<>();
}

@Override
public void putAll(Map<? extends String, ? extends Object> m) {
if (m == null) {
return;
}

// Need to reset or switch between on/off to conditional doesn't work
featureManagement = new HashMap<>();
onOff = new HashMap<>();

Map<? extends String, ? extends Object> features = removePrefixes(m, "featureManagement");
// try to parse the properties by server side schema as default
tryServerSideSchema(m);

if (!features.isEmpty()) {
m = features;
if (featureManagement.isEmpty() && onOff.isEmpty()) {
tryClientSideSchema(m);
}
}

for (String key : m.keySet()) {
addToFeatures(m, key, "");
private void tryServerSideSchema(Map<? extends String, ? extends Object> features) {
if (features.keySet().isEmpty()) {
return;
}

// check if FeatureFlags section exist
String featureFlagsSectionKey = "";
for (String key : features.keySet()) {
if ("FeatureFlags".equalsIgnoreCase(key) || "feature-flags".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<String, Object> featureFlagsSection = (Map<String, Object>) featureFlagsObject;
for (String key : featureFlagsSection.keySet()) {
addServerSideFeature(featureFlagsSection, key);
}
}
}

@SuppressWarnings("unchecked")
private Map<? extends String, ? extends Object> removePrefixes(Map<? extends String, ? extends Object> m,
String prefix) {
Map<? extends String, ? extends Object> removedPrefix = new HashMap<>();
if (m.containsKey(prefix)) {
removedPrefix = (Map<? extends String, ? extends Object>) m.get(prefix);
private void tryClientSideSchema(Map<? extends String, ? extends Object> features) {
for (String key : features.keySet()) {
addFeature(features, key, "");
}
return removedPrefix;
}

@SuppressWarnings("unchecked")
private void addToFeatures(Map<? extends String, ? extends Object> features, String key, String combined) {
private void addFeature(Map<? extends String, ? extends Object> features, String key, String combined) {
Object featureValue = features.get(key);
if (!combined.isEmpty() && !combined.endsWith(".")) {
combined += ".";
Expand All @@ -93,7 +111,7 @@ private void addToFeatures(Map<? extends String, ? extends Object> features, Str
if (Map.class.isAssignableFrom(featureValue.getClass())) {
features = (Map<String, Object>) featureValue;
for (String fKey : features.keySet()) {
addToFeatures(features, fKey, combined + key);
addFeature(features, fKey, combined + key);
}
}
} else {
Expand All @@ -105,13 +123,36 @@ private void addToFeatures(Map<? extends String, ? extends Object> features, Str
}
}

private void addServerSideFeature(Map<? extends String, ? extends Object> features, String key) {
final Object featureValue = features.get(key);

ServerSideFeature serverSideFeature = null;
try {
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) {
final Feature feature = new Feature();
feature.setKey(serverSideFeature.getId());
feature.setEvaluate(serverSideFeature.isEnabled());
feature.setEnabledFor(serverSideFeature.getConditions().getClientFilters());
feature.setRequirementType(serverSideFeature.getConditions().getRequirementType());
featureManagement.put(serverSideFeature.getId(), feature);
} else {
onOff.put(serverSideFeature.getId(), serverSideFeature.isEnabled());
}
}
}

/**
* @return the featureManagement
*/
public Map<String, Feature> getFeatureManagement() {
return featureManagement;
}

/**
* @return the onOff
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.spring.cloud.feature.management.implementation.models;

import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.Map;

import static com.azure.spring.cloud.feature.management.implementation.FeatureManagementConstants.DEFAULT_REQUIREMENT_TYPE;

@JsonIgnoreProperties(ignoreUnknown = true)
public class Conditions {
@JsonProperty("client_filters")
private Map<Integer, FeatureFilterEvaluationContext> clientFilters;

@JsonProperty("requirement_type")
private String requirementType = DEFAULT_REQUIREMENT_TYPE;

/**
* @return the requirementType
*/
public String getRequirementType() {
return requirementType;
}

/**
* @param requirementType the requirementType to set
*/
public void setRequirementType(String requirementType) {
this.requirementType = requirementType;
}

/**
* @return the enabledFor
*/
public Map<Integer, FeatureFilterEvaluationContext> getClientFilters() {
return clientFilters;
}

/**
* @param clientFilters the clientFilters to set
*/
public void setClientFilters(Map<Integer, FeatureFilterEvaluationContext> clientFilters) {
this.clientFilters = clientFilters;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class Feature {
private Map<Integer, FeatureFilterEvaluationContext> enabledFor;

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

/**
* @return the key
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.spring.cloud.feature.management.implementation.models;

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 ServerSideFeature {
@JsonProperty("id")
private String id;

@JsonProperty("description")
private String description;

@JsonProperty("enabled")
private boolean enabled;

@JsonProperty("conditions")
private Conditions conditions;

/**
* @return the id
*/
public String getId() {
return id;
}

/**
* @param id the id to set
*/
public void setId(String id) {
this.id = id;
}

/**
* @return the enabled
*/
public boolean isEnabled() {
return enabled;
}

/**
* @param enabled the enabled to set
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}

/**
* @return the description
* */
public String getDescription() {
return description;
}

/**
* @param description the description to set
* */
public void setDescription(String description) {
this.description = description;
}

/**
* @return the conditions
* */
public Conditions getConditions() {
return conditions;
}

/**
* @param conditions the conditions to set
* */
public void setConditions(Conditions conditions) {
this.conditions = conditions;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// 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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ public void noFilter() throws FilterNotFoundException {
() -> featureManager.isEnabledAsync("Off").block());
assertThat(e).hasMessage("Fail fast is set and a Filter was unable to be found: AlwaysOff");
}

@Test
public void allOn() {
HashMap<String, Feature> features = new HashMap<>();
Expand Down Expand Up @@ -211,7 +211,7 @@ public boolean evaluate(FeatureFilterEvaluationContext context) {
}

}

class AlwaysOffFilter implements FeatureFilter {

@Override
Expand Down
Loading
Loading