Skip to content

Commit

Permalink
Populate feature flag ETag/id/reference when telemetry enabled (#94)
Browse files Browse the repository at this point in the history
  • Loading branch information
linglingye001 authored Feb 28, 2025
1 parent 9cc6b1e commit a39c7be
Show file tree
Hide file tree
Showing 2 changed files with 231 additions and 1 deletion.
162 changes: 162 additions & 0 deletions internal/loader/configuraiton_setting_loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ func mockFeatureFlagSettings() []azappconfig.Setting {
return settingsToReturn
}

func mockVariantFeatureFlagSettings(key, label string, telemetryEnabled bool) []azappconfig.Setting {
settingsToReturn := make([]azappconfig.Setting, 1)
settingsToReturn[0] = newFeatureFlagVariant(key, label, telemetryEnabled)

return settingsToReturn
}

func newKeyValueSelector(key string, label *string) acpv1.Selector {
return acpv1.Selector{
KeyFilter: &key,
Expand Down Expand Up @@ -133,6 +140,49 @@ func newFeatureFlagSettings(key string, label string) azappconfig.Setting {
}
}

func newFeatureFlagVariant(key string, label string, telemetryEnabled bool) azappconfig.Setting {
featureFlagContentType := FeatureFlagContentType
fakeETag := azcore.ETag("fakeETag")
featureFlagId := strings.TrimPrefix(key, ".appconfig.featureflag/")
featureFlagValue := fmt.Sprintf(`{
"id": "%s",
"description": "",
"enabled": false,
"variants": [
{
"name": "Off",
"configuration_value": false
},
{
"name": "On",
"configuration_value": true
}
],
"allocation": {
"percentile": [
{
"variant": "Off",
"from": 0,
"to": 100
}
],
"default_when_enabled": "Off",
"default_when_disabled": "Off"
},
"telemetry": {
"enabled": %t
}
}`, featureFlagId, telemetryEnabled)

return azappconfig.Setting{
Key: &key,
Value: &featureFlagValue,
Label: &label,
ContentType: &featureFlagContentType,
ETag: &fakeETag,
}
}

type MockResolveSecretReference struct {
ctrl *gomock.Controller
recorder *MockResolveSecretReferenceMockRecorder
Expand Down Expand Up @@ -1074,6 +1124,108 @@ var _ = Describe("AppConfiguationProvider Get All Settings", func() {
Expect(allSettings.ConfigMapSettings["settings.json"]).Should(Equal("{\"feature_management\":{\"feature_flags\":[{\"conditions\":{\"client_filters\":[]},\"description\":\"\",\"enabled\":false,\"id\":\"Beta\"}]}}"))
})

It("Succeed to get feature flag settings", func() {
By("By updating telemetry when telemetry is enabled")
featureFlagKeyFilter := "*"
testSpec := acpv1.AzureAppConfigurationProviderSpec{
Endpoint: &EndpointName,
ReplicaDiscoveryEnabled: false,
Target: acpv1.ConfigurationGenerationParameters{
ConfigMapName: ConfigMapName,
ConfigMapData: &acpv1.ConfigMapDataOptions{
Type: acpv1.Json,
Key: "settings.json",
},
},
FeatureFlag: &acpv1.AzureAppConfigurationFeatureFlagOptions{
Selectors: []acpv1.Selector{
{
KeyFilter: &featureFlagKeyFilter,
},
},
},
}
testProvider := acpv1.AzureAppConfigurationProvider{
TypeMeta: metav1.TypeMeta{
APIVersion: "azconfig.io/v1",
Kind: "AppConfigurationProvider",
},
ObjectMeta: metav1.ObjectMeta{
Name: "testName",
Namespace: "testNamespace",
},
Spec: testSpec,
}

featureFlagsToReturn := mockVariantFeatureFlagSettings(".appconfig.featureflag/Telemetry_2", "Test", true)
featureFlagEtags := make(map[acpv1.Selector][]*azcore.ETag)
featureFlagEtags[newFeatureFlagSelector("*", nil)] = []*azcore.ETag{}
settingsResponse := &SettingsResponse{
Settings: featureFlagsToReturn,
Etags: featureFlagEtags,
}
mockSettingsClient.EXPECT().GetSettings(gomock.Any(), gomock.Any()).Return(settingsResponse, nil).Times(2)
mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{&fakeClientWrapper}, nil).Times(2)
configurationProvider, _ := NewConfigurationSettingLoader(testProvider, mockCongiurationClientManager, mockSettingsClient)
allSettings, err := configurationProvider.CreateTargetSettings(context.Background(), mockResolveSecretReference)

Expect(err).Should(BeNil())
Expect(len(allSettings.ConfigMapSettings)).Should(Equal(1))
Expect(allSettings.ConfigMapSettings["settings.json"]).Should(
Equal("{\"feature_management\":{\"feature_flags\":[{\"allocation\":{\"default_when_disabled\":\"Off\",\"default_when_enabled\":\"Off\",\"percentile\":[{\"from\":0,\"to\":100,\"variant\":\"Off\"}]},\"description\":\"\",\"enabled\":false,\"id\":\"Telemetry_2\",\"telemetry\":{\"enabled\":true,\"metadata\":{\"ETag\":\"fakeETag\",\"FeatureFlagId\":\"Rc8Am7HIGDT7HC5Ovs3wKN_aGaaK_Uz1mH2e11gaK0o\",\"FeatureFlagReference\":\"/kv/.appconfig.featureflag/Telemetry_2?label=Test\"}},\"variants\":[{\"configuration_value\":false,\"name\":\"Off\"},{\"configuration_value\":true,\"name\":\"On\"}]}]}}"))
})

It("Succeed to get feature flag settings", func() {
By("By not populating telemetry when telemetry is disabled")
featureFlagKeyFilter := "*"
testSpec := acpv1.AzureAppConfigurationProviderSpec{
Endpoint: &EndpointName,
ReplicaDiscoveryEnabled: false,
Target: acpv1.ConfigurationGenerationParameters{
ConfigMapName: ConfigMapName,
ConfigMapData: &acpv1.ConfigMapDataOptions{
Type: acpv1.Json,
Key: "settings.json",
},
},
FeatureFlag: &acpv1.AzureAppConfigurationFeatureFlagOptions{
Selectors: []acpv1.Selector{
{
KeyFilter: &featureFlagKeyFilter,
},
},
},
}
testProvider := acpv1.AzureAppConfigurationProvider{
TypeMeta: metav1.TypeMeta{
APIVersion: "azconfig.io/v1",
Kind: "AppConfigurationProvider",
},
ObjectMeta: metav1.ObjectMeta{
Name: "testName",
Namespace: "testNamespace",
},
Spec: testSpec,
}

featureFlagsToReturn := mockVariantFeatureFlagSettings(".appconfig.featureflag/Telemetry_2", "Test", false)
featureFlagEtags := make(map[acpv1.Selector][]*azcore.ETag)
featureFlagEtags[newFeatureFlagSelector("*", nil)] = []*azcore.ETag{}
settingsResponse := &SettingsResponse{
Settings: featureFlagsToReturn,
Etags: featureFlagEtags,
}
mockSettingsClient.EXPECT().GetSettings(gomock.Any(), gomock.Any()).Return(settingsResponse, nil).Times(2)
mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{&fakeClientWrapper}, nil).Times(2)
configurationProvider, _ := NewConfigurationSettingLoader(testProvider, mockCongiurationClientManager, mockSettingsClient)
allSettings, err := configurationProvider.CreateTargetSettings(context.Background(), mockResolveSecretReference)

Expect(err).Should(BeNil())
Expect(len(allSettings.ConfigMapSettings)).Should(Equal(1))
Expect(allSettings.ConfigMapSettings["settings.json"]).Should(
Equal("{\"feature_management\":{\"feature_flags\":[{\"allocation\":{\"default_when_disabled\":\"Off\",\"default_when_enabled\":\"Off\",\"percentile\":[{\"from\":0,\"to\":100,\"variant\":\"Off\"}]},\"description\":\"\",\"enabled\":false,\"id\":\"Telemetry_2\",\"telemetry\":{\"enabled\":false},\"variants\":[{\"configuration_value\":false,\"name\":\"Off\"},{\"configuration_value\":true,\"name\":\"On\"}]}]}}"))
})

It("Fail to get all configuration settings", func() {
By("By getting error from Azure App Configuration")
testSpec := acpv1.AzureAppConfigurationProviderSpec{
Expand Down Expand Up @@ -1397,6 +1549,16 @@ func TestGetFilters(t *testing.T) {
assert.Equal(t, "snapshot", *filters10[1].SnapshotName)
}

func TestFeatureFlagId(t *testing.T) {
telemetrySetting1 := newFeatureFlagVariant(".appconfig.featureflag/Telemetry_1", "", true)
calculatedId1 := calculateFeatureFlagId(telemetrySetting1)
assert.Equal(t, "krkOsu9dVV9huwbQDPR6gkV_2T0buWxOCS-nNsj5-6g", calculatedId1)

telemetrySetting2 := newFeatureFlagVariant(".appconfig.featureflag/Telemetry_2", "Test", true)
calculatedId2 := calculateFeatureFlagId(telemetrySetting2)
assert.Equal(t, "Rc8Am7HIGDT7HC5Ovs3wKN_aGaaK_Uz1mH2e11gaK0o", calculatedId2)
}

func TestCompare(t *testing.T) {
var nilString *string = nil
stringA := "stringA"
Expand Down
70 changes: 69 additions & 1 deletion internal/loader/configuration_setting_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package loader
import (
acpv1 "azappconfig/provider/api/v1"
"context"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
Expand All @@ -19,6 +20,7 @@ import (

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig"
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets"
"golang.org/x/crypto/pkcs12"
"golang.org/x/exp/maps"
Expand Down Expand Up @@ -99,6 +101,16 @@ const (
RequestTracingEnabled string = "REQUEST_TRACING_ENABLED"
)

// Feature flag telemetry
const (
TelemetryKey string = "telemetry"
EnabledKey string = "enabled"
MetadataKey string = "metadata"
ETagKey string = "ETag"
FeatureFlagIdKey string = "FeatureFlagId"
FeatureFlagReferenceKey string = "FeatureFlagReference"
)

func NewConfigurationSettingLoader(provider acpv1.AzureAppConfigurationProvider, clientManager ClientManager, settingsClient SettingsClient) (*ConfigurationSettingLoader, error) {
return &ConfigurationSettingLoader{
AzureAppConfigurationProvider: provider,
Expand Down Expand Up @@ -372,6 +384,10 @@ func (csl *ConfigurationSettingLoader) getFeatureFlagSettings(ctx context.Contex
settingsLength := len(settingsResponse.Settings)
featureFlagExist := make(map[string]bool, settingsLength)
deduplicatedFeatureFlags := make([]interface{}, 0)
clientEndpoint := ""
if manager, ok := csl.ClientManager.(*ConfigurationClientManager); ok {
clientEndpoint = manager.lastSuccessfulEndpoint
}

// if settings returned like this: [{"id": "Beta"...}, {"id": "Alpha"...}, {"id": "Beta"...}], we need to deduplicate it to [{"id": "Alpha"...}, {"id": "Beta"...}], the last one wins
for i := settingsLength - 1; i >= 0; i-- {
Expand All @@ -380,11 +396,12 @@ func (csl *ConfigurationSettingLoader) getFeatureFlagSettings(ctx context.Contex
continue
}
featureFlagExist[key] = true
var out interface{}
var out map[string]interface{}
err := json.Unmarshal([]byte(*settingsResponse.Settings[i].Value), &out)
if err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal feature flag settings: %s", err.Error())
}
populateTelemetryMetadata(out, settingsResponse.Settings[i], clientEndpoint)
deduplicatedFeatureFlags = append(deduplicatedFeatureFlags, out)
}

Expand Down Expand Up @@ -899,3 +916,54 @@ func reverseClients(clients []*ConfigurationClientWrapper, start, end int) {
end--
}
}

func calculateFeatureFlagId(setting azappconfig.Setting) string {
// Create the basic value string
featureFlagId := *setting.Key + "\n"
if setting.Label != nil && strings.TrimSpace(*setting.Label) != "" {
featureFlagId += *setting.Label
}

// Generate SHA-256 hash, and encode it to Base64
hash := sha256.Sum256([]byte(featureFlagId))
encodedFeatureFlag := base64.StdEncoding.EncodeToString(hash[:])

// Replace '+' with '-' and '/' with '_'
encodedFeatureFlag = strings.ReplaceAll(encodedFeatureFlag, "+", "-")
encodedFeatureFlag = strings.ReplaceAll(encodedFeatureFlag, "/", "_")

// Remove all instances of "=" at the end of the string that were added as padding
if idx := strings.Index(encodedFeatureFlag, "="); idx != -1 {
encodedFeatureFlag = encodedFeatureFlag[:idx]
}

return encodedFeatureFlag
}

func generateFeatureFlagReference(setting azappconfig.Setting, endpoint string) string {
featureFlagReference := fmt.Sprintf("%s/kv/%s", endpoint, *setting.Key)

// Check if the label is present and not empty
if setting.Label != nil && strings.TrimSpace(*setting.Label) != "" {
featureFlagReference += fmt.Sprintf("?label=%s", *setting.Label)
}

return featureFlagReference
}

func populateTelemetryMetadata(featureFlag map[string]interface{}, setting azappconfig.Setting, endpoint string) {
if telemetry, ok := featureFlag[TelemetryKey].(map[string]interface{}); ok {
if enabled, ok := telemetry[EnabledKey].(bool); ok && enabled {
metadata, _ := telemetry[MetadataKey].(map[string]interface{})
if metadata == nil {
metadata = make(map[string]interface{})
}

// Set the new metadata
metadata[ETagKey] = *setting.ETag
metadata[FeatureFlagIdKey] = calculateFeatureFlagId(setting)
metadata[FeatureFlagReferenceKey] = generateFeatureFlagReference(setting, endpoint)
telemetry[MetadataKey] = metadata
}
}
}

0 comments on commit a39c7be

Please sign in to comment.