Skip to content

Commit cabc516

Browse files
committed
allocation id support
1 parent 3655f77 commit cabc516

File tree

2 files changed

+262
-1
lines changed

2 files changed

+262
-1
lines changed

internal/loader/configuraiton_setting_loader_test.go

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"crypto/x509"
1212
"crypto/x509/pkix"
1313
"encoding/base64"
14+
"encoding/json"
1415
"encoding/pem"
1516
"errors"
1617
"fmt"
@@ -1172,7 +1173,7 @@ var _ = Describe("AppConfiguationProvider Get All Settings", func() {
11721173
Expect(err).Should(BeNil())
11731174
Expect(len(allSettings.ConfigMapSettings)).Should(Equal(1))
11741175
Expect(allSettings.ConfigMapSettings["settings.json"]).Should(
1175-
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\"}]}]}}"))
1176+
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\":{\"AllocationId\":\"5QQaU_fROjgJ3gjG0sVe\",\"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\"}]}]}}"))
11761177
})
11771178

11781179
It("Fail to get all configuration settings", func() {
@@ -1490,6 +1491,126 @@ func TestFeatureFlagId(t *testing.T) {
14901491
assert.Equal(t, "Rc8Am7HIGDT7HC5Ovs3wKN_aGaaK_Uz1mH2e11gaK0o", calculatedId2)
14911492
}
14921493

1494+
func TestAllocationId(t *testing.T) {
1495+
var featureFlag1 map[string]interface{}
1496+
NoPercentileAndSeed := `{
1497+
"enabled": true,
1498+
"telemetry": { "enabled": true },
1499+
"variants": [ { "name": "Control" }, { "name": "Test" } ],
1500+
"allocation": {
1501+
"default_when_disabled": "Control",
1502+
"user": [ {"users": ["Jeff"], "variant": "Test"} ]
1503+
}
1504+
}`
1505+
_ = json.Unmarshal([]byte(NoPercentileAndSeed), &featureFlag1)
1506+
allocationId1 := generateAllocationId(featureFlag1)
1507+
assert.Equal(t, "", allocationId1)
1508+
1509+
var featureFlag2 map[string]interface{}
1510+
SeedOnly := `{
1511+
"enabled": true,
1512+
"telemetry": { "enabled": true },
1513+
"variants": [ { "name": "Control" }, { "name": "Test" } ],
1514+
"allocation": {
1515+
"default_when_disabled": "Control",
1516+
"user": [ {"users": ["Jeff"], "variant": "Test"} ],
1517+
"seed": "123"
1518+
}
1519+
}`
1520+
_ = json.Unmarshal([]byte(SeedOnly), &featureFlag2)
1521+
allocationId2 := generateAllocationId(featureFlag2)
1522+
assert.Equal(t, "qZApcKdfXscxpgn_8CMf", allocationId2)
1523+
1524+
var featureFlag3 map[string]interface{}
1525+
DefaultWhenEnabledOnly := `{
1526+
"enabled": true,
1527+
"telemetry": { "enabled": true },
1528+
"variants": [ { "name": "Control" }, { "name": "Test" } ],
1529+
"allocation": {
1530+
"default_when_enabled": "Control"
1531+
}
1532+
}`
1533+
_ = json.Unmarshal([]byte(DefaultWhenEnabledOnly), &featureFlag3)
1534+
allocationId3 := generateAllocationId(featureFlag3)
1535+
assert.Equal(t, "k486zJjud_HkKaL1C4qB", allocationId3)
1536+
1537+
var featureFlag4 map[string]interface{}
1538+
PercentileOnly := `{
1539+
"enabled": true,
1540+
"telemetry": { "enabled": true },
1541+
"variants": [ ],
1542+
"allocation": {
1543+
"percentile": [ { "from": 0, "to": 50, "variant": "Control" }, { "from": 50, "to": 100, "variant": "Test" } ]
1544+
}
1545+
}`
1546+
_ = json.Unmarshal([]byte(PercentileOnly), &featureFlag4)
1547+
allocationId4 := generateAllocationId(featureFlag4)
1548+
assert.Equal(t, "5YUbmP0P5s47zagO_LvI", allocationId4)
1549+
1550+
var featureFlag5 map[string]interface{}
1551+
SimpleConfigurationValue := `{
1552+
"enabled": true,
1553+
"telemetry": { "enabled": true },
1554+
"variants": [ { "name": "Control", "configuration_value": "standard" }, { "name": "Test", "configuration_value": "special" } ],
1555+
"allocation": {
1556+
"default_when_enabled": "Control",
1557+
"percentile": [ { "from": 0, "to": 50, "variant": "Control" }, { "from": 50, "to": 100, "variant": "Test" } ],
1558+
"seed": "123"
1559+
}
1560+
}`
1561+
_ = json.Unmarshal([]byte(SimpleConfigurationValue), &featureFlag5)
1562+
allocationId5 := generateAllocationId(featureFlag5)
1563+
assert.Equal(t, "QIOEOTQJr2AXo4dkFFqy", allocationId5)
1564+
1565+
var featureFlag6 map[string]interface{}
1566+
ComplexConfigurationValue := `{
1567+
"enabled": true,
1568+
"telemetry": { "enabled": true },
1569+
"variants": [ { "name": "Control", "configuration_value": { "title": { "size": 100, "color": "red" }, "options": [ 1, 2, 3 ]} }, { "name": "Test", "configuration_value": { "title": { "size": 200, "color": "blue" }, "options": [ "1", "2", "3" ]} } ],
1570+
"allocation": {
1571+
"default_when_enabled": "Control",
1572+
"percentile": [ { "from": 0, "to": 50, "variant": "Control" }, { "from": 50, "to": 100, "variant": "Test" } ],
1573+
"seed": "123"
1574+
}
1575+
}`
1576+
_ = json.Unmarshal([]byte(ComplexConfigurationValue), &featureFlag6)
1577+
allocationId6 := generateAllocationId(featureFlag6)
1578+
assert.Equal(t, "4Bes0AlwuO8kYX-YkBWs", allocationId6)
1579+
1580+
var featureFlag7 map[string]interface{}
1581+
TelemetryVariantPercentile := `{
1582+
"enabled": true,
1583+
"telemetry": { "enabled": true },
1584+
"variants": [
1585+
{
1586+
"name": "True_Override",
1587+
"configuration_value": {
1588+
"someOtherKey": {
1589+
"someSubKey": "someSubValue"
1590+
},
1591+
"someKey4": [3, 1, 4, true],
1592+
"someKey": "someValue",
1593+
"someKey3": 3.14,
1594+
"someKey2": 3
1595+
}
1596+
}
1597+
],
1598+
"allocation": {
1599+
"default_when_enabled": "True_Override",
1600+
"percentile": [
1601+
{
1602+
"variant": "True_Override",
1603+
"from": 0,
1604+
"to": 100
1605+
}
1606+
]
1607+
}
1608+
}`
1609+
_ = json.Unmarshal([]byte(TelemetryVariantPercentile), &featureFlag7)
1610+
allocationId7 := generateAllocationId(featureFlag7)
1611+
assert.Equal(t, "YsdJ4pQpmhYa8KEhRLUn", allocationId7)
1612+
}
1613+
14931614
func TestCreateSecretClients(t *testing.T) {
14941615
configProvider := &acpv1.AzureAppConfigurationProvider{
14951616
TypeMeta: metav1.TypeMeta{

internal/loader/configuration_setting_loader.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"fmt"
1515
"net/url"
1616
"os"
17+
"sort"
1718
"strconv"
1819
"strings"
1920
"sync"
@@ -99,6 +100,17 @@ const (
99100
FeatureFlagETagKey string = "ETag"
100101
FeatureFlagIdKey string = "FeatureFlagId"
101102
FeatureFlagReferenceKey string = "FeatureFlagReference"
103+
NameKey string = "name"
104+
AllocationKey string = "allocation"
105+
AllocationIdKey string = "AllocationId"
106+
DefaultWhenEnabledKey string = "default_when_enabled"
107+
PercentileKey string = "percentile"
108+
FromKey string = "from"
109+
ToKey string = "to"
110+
SeedKey string = "seed"
111+
VariantKey string = "variant"
112+
VariantsKey string = "variants"
113+
ConfigurationValueKey string = "configuration_value"
102114
PreservedSecretTypeTag string = ".kubernetes.secret.type"
103115
CertTypePem string = "application/x-pem-file"
104116
CertTypePfx string = "application/x-pkcs12"
@@ -943,6 +955,98 @@ func createFeatureFlagReference(setting azappconfig.Setting, endpoint string) st
943955
return featureFlagReference
944956
}
945957

958+
func generateAllocationId(featureFlag map[string]interface{}) string {
959+
var rawAllocationId strings.Builder
960+
var variantsForExperimentation []string
961+
962+
allocationSection, ok := featureFlag[AllocationKey].(map[string]interface{})
963+
if !ok {
964+
return ""
965+
}
966+
967+
// Seed
968+
seedValue := ""
969+
if allocationSection[SeedKey] != nil {
970+
seedValue = allocationSection[SeedKey].(string)
971+
}
972+
rawAllocationId.WriteString(fmt.Sprintf("seed=%v\ndefault_when_enabled=", seedValue))
973+
974+
// Default variant when enabled
975+
if defaultVariant, exists := allocationSection[DefaultWhenEnabledKey]; exists {
976+
variantsForExperimentation = append(variantsForExperimentation, defaultVariant.(string))
977+
rawAllocationId.WriteString(fmt.Sprintf("%v", defaultVariant.(string)))
978+
}
979+
980+
// Percentiles
981+
rawAllocationId.WriteString("\npercentiles=")
982+
if percentiles, exists := allocationSection[PercentileKey].([]interface{}); exists {
983+
var sortedPercentiles []map[string]interface{}
984+
985+
// Filter and sort percentiles
986+
for _, p := range percentiles {
987+
pMap := p.(map[string]interface{})
988+
from, fromExist := pMap[FromKey]
989+
to, toExist := pMap[ToKey]
990+
_, variantExist := pMap[VariantKey]
991+
if fromExist && toExist && variantExist && from != to {
992+
sortedPercentiles = append(sortedPercentiles, pMap)
993+
}
994+
}
995+
996+
sort.Slice(sortedPercentiles, func(i, j int) bool {
997+
return sortedPercentiles[i][FromKey].(float64) < sortedPercentiles[j][FromKey].(float64)
998+
})
999+
1000+
for i, percentile := range sortedPercentiles {
1001+
variantsForExperimentation = append(variantsForExperimentation, percentile[VariantKey].(string))
1002+
rawAllocationId.WriteString(fmt.Sprintf("%v,%s,%v", percentile[FromKey].(float64), base64.StdEncoding.EncodeToString([]byte(percentile[VariantKey].(string))), percentile[ToKey].(float64)))
1003+
if i != len(sortedPercentiles)-1 {
1004+
rawAllocationId.WriteString(";")
1005+
}
1006+
}
1007+
}
1008+
1009+
// Short-circuit if no valid data
1010+
if len(variantsForExperimentation) == 0 && allocationSection[SeedKey] == nil {
1011+
return ""
1012+
}
1013+
1014+
rawAllocationId.WriteString("\nvariants=")
1015+
if len(variantsForExperimentation) != 0 {
1016+
variants, variantsExist := featureFlag[VariantsKey].([]interface{})
1017+
sortedVariants := make([]map[string]interface{}, 0)
1018+
if variantsExist {
1019+
for _, v := range variants {
1020+
if contains(variantsForExperimentation, v.(map[string]interface{})[NameKey].(string)) {
1021+
sortedVariants = append(sortedVariants, v.(map[string]interface{}))
1022+
}
1023+
}
1024+
}
1025+
1026+
sort.Slice(sortedVariants, func(i, j int) bool {
1027+
return sortedVariants[i][NameKey].(string) < sortedVariants[j][NameKey].(string)
1028+
})
1029+
1030+
for i, variant := range sortedVariants {
1031+
configurationValue := []byte("")
1032+
if _, exist := variant[ConfigurationValueKey]; exist {
1033+
configurationValue, _ = json.Marshal(jsonSorter(variant[ConfigurationValueKey]))
1034+
}
1035+
rawAllocationId.WriteString(fmt.Sprintf("%s,%s", base64.StdEncoding.EncodeToString([]byte(variant[NameKey].(string))), string(configurationValue)))
1036+
if i != len(sortedVariants)-1 {
1037+
rawAllocationId.WriteString(";")
1038+
}
1039+
}
1040+
}
1041+
// Hash the raw allocation ID
1042+
hash := sha256.Sum256([]byte(rawAllocationId.String()))
1043+
first15Bytes := hash[:15]
1044+
1045+
// Convert to base64 URL-safe string
1046+
allocationId := base64.RawURLEncoding.EncodeToString(first15Bytes)
1047+
return allocationId
1048+
}
1049+
9461050
func updateFeatureFlagTelemetry(featureFlag map[string]interface{}, setting azappconfig.Setting, endpoint string) {
9471051
if telemetry, ok := featureFlag[FeatureFlagTelemetryKey].(map[string]interface{}); ok {
9481052
if enabled, ok := telemetry[FeatureFlagEnabledKey].(bool); ok && enabled {
@@ -955,7 +1059,43 @@ func updateFeatureFlagTelemetry(featureFlag map[string]interface{}, setting azap
9551059
metadata[FeatureFlagETagKey] = *setting.ETag
9561060
metadata[FeatureFlagIdKey] = calculateFeatureFlagId(setting)
9571061
metadata[FeatureFlagReferenceKey] = createFeatureFlagReference(setting, endpoint)
1062+
if allocationId := generateAllocationId(featureFlag); allocationId != "" {
1063+
metadata[AllocationIdKey] = allocationId
1064+
}
9581065
telemetry[FeatureFlagMetadataKey] = metadata
9591066
}
9601067
}
9611068
}
1069+
1070+
func contains(arr []string, target string) bool {
1071+
for _, a := range arr {
1072+
if a == target {
1073+
return true
1074+
}
1075+
}
1076+
return false
1077+
}
1078+
1079+
func jsonSorter(v interface{}) interface{} {
1080+
switch value := v.(type) {
1081+
case nil:
1082+
return nil
1083+
case []interface{}:
1084+
return value // Return arrays as-is
1085+
case map[string]interface{}:
1086+
sortedMap := make(map[string]interface{})
1087+
// Get keys and sort them
1088+
keys := make([]string, 0, len(value))
1089+
for k := range value {
1090+
keys = append(keys, k)
1091+
}
1092+
sort.Strings(keys)
1093+
// Populate the sorted map
1094+
for _, k := range keys {
1095+
sortedMap[k] = jsonSorter(value[k]) // Recursively sort values
1096+
}
1097+
return sortedMap
1098+
default:
1099+
return v // Return other types (strings, numbers, etc.) as-is
1100+
}
1101+
}

0 commit comments

Comments
 (0)