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
17 changes: 9 additions & 8 deletions azureappconfiguration/azureappconfiguration.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import (
"github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration/internal/tracing"
"github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration/internal/tree"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig"
"github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2"
decoder "github.com/go-viper/mapstructure/v2"
"golang.org/x/sync/errgroup"
)
Expand All @@ -55,8 +55,8 @@ type AzureAppConfiguration struct {
// Settings used for refresh scenarios
sentinelETags map[WatchedSetting]*azcore.ETag
watchAll bool
kvETags map[Selector][]*azcore.ETag
ffETags map[Selector][]*azcore.ETag
kvETags map[comparableSelector][]*azcore.ETag
ffETags map[comparableSelector][]*azcore.ETag
keyVaultRefs map[string]string // unversioned Key Vault references
kvRefreshTimer refresh.Condition
secretRefreshTimer refresh.Condition
Expand Down Expand Up @@ -121,7 +121,7 @@ func Load(ctx context.Context, authentication AuthenticationOptions, options *Op
azappcfg.kvRefreshTimer = refresh.NewTimer(options.RefreshOptions.Interval)
azappcfg.watchedSettings = normalizedWatchedSettings(options.RefreshOptions.WatchedSettings)
azappcfg.sentinelETags = make(map[WatchedSetting]*azcore.ETag)
azappcfg.kvETags = make(map[Selector][]*azcore.ETag)
azappcfg.kvETags = make(map[comparableSelector][]*azcore.ETag)
if len(options.RefreshOptions.WatchedSettings) == 0 {
azappcfg.watchAll = true
}
Expand All @@ -137,7 +137,7 @@ func Load(ctx context.Context, authentication AuthenticationOptions, options *Op
azappcfg.ffSelectors = getFeatureFlagSelectors(deduplicateSelectors(options.FeatureFlagOptions.Selectors))
if options.FeatureFlagOptions.RefreshOptions.Enabled {
azappcfg.ffRefreshTimer = refresh.NewTimer(options.FeatureFlagOptions.RefreshOptions.Interval)
azappcfg.ffETags = make(map[Selector][]*azcore.ETag)
azappcfg.ffETags = make(map[comparableSelector][]*azcore.ETag)
}
}

Expand Down Expand Up @@ -759,7 +759,7 @@ func deduplicateSelectors(selectors []Selector) []Selector {
}

// Create a map to track unique selectors
seen := make(map[Selector]struct{})
seen := make(map[comparableSelector]struct{})
var result []Selector

// Process the selectors in reverse order to maintain the behavior
Expand All @@ -771,8 +771,9 @@ func deduplicateSelectors(selectors []Selector) []Selector {
}

// Check if we've seen this selector before
if _, exists := seen[selectors[i]]; !exists {
seen[selectors[i]] = struct{}{}
key := selectors[i].comparableKey()
if _, exists := seen[key]; !exists {
seen[key] = struct{}{}
result = append(result, selectors[i])
}
}
Expand Down
206 changes: 203 additions & 3 deletions azureappconfiguration/azureappconfiguration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
"github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration/internal/fm"
"github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration/internal/tracing"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig"
"github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
Expand Down Expand Up @@ -128,7 +128,7 @@ func TestLoadFeatureFlags_Success(t *testing.T) {
{Key: toPtr(".appconfig.featureflag/Beta"), Value: &value1, ContentType: toPtr(featureFlagContentType)},
{Key: toPtr(".appconfig.featureflag/Alpha"), Value: &value2, ContentType: toPtr(featureFlagContentType)},
},
pageETags: map[Selector][]*azcore.ETag{},
pageETags: map[comparableSelector][]*azcore.ETag{},
}

mockClient.On("getSettings", ctx).Return(mockResponse, nil)
Expand Down Expand Up @@ -1545,7 +1545,7 @@ func TestLoadFeatureFlags_TracingUpdated(t *testing.T) {
ContentType: toPtr(featureFlagContentType),
},
},
pageETags: map[Selector][]*azcore.ETag{},
pageETags: map[comparableSelector][]*azcore.ETag{},
}

mockClient.On("getSettings", ctx).Return(mockResponse, nil)
Expand Down Expand Up @@ -1601,3 +1601,203 @@ func TestLoadFeatureFlags_TracingUpdated(t *testing.T) {
// Verify max variants is included
assert.Contains(t, correlationCtx, tracing.FFMaxVariantsKey+"=3")
}

func TestLoadKeyValues_WithTagFilter(t *testing.T) {
ctx := context.Background()
mockClient := new(mockSettingsClient)

// Create mock settings with different tags
value1 := "value1"
value3 := "value3"
value4 := "value4"

mockResponse := &settingsResponse{
settings: []azappconfig.Setting{
{
Key: toPtr("app:key1"),
Value: &value1,
Tags: map[string]*string{
"env": toPtr("production"),
"team": toPtr("backend"),
},
},
{
Key: toPtr("app:key3"),
Value: &value3,
Tags: map[string]*string{
"env": toPtr("production"),
"team": toPtr("frontend"),
},
},
{
Key: toPtr("app:key4"),
Value: &value4,
Tags: map[string]*string{
"env": toPtr("production"),
"team": toPtr("backend"),
"feature": toPtr("new"),
},
},
},
pageETags: map[comparableSelector][]*azcore.ETag{},
}

mockClient.On("getSettings", ctx).Return(mockResponse, nil)

// Test with single tag filter
azappcfg := &AzureAppConfiguration{
clientManager: &configurationClientManager{
staticClient: &configurationClientWrapper{client: &azappconfig.Client{}},
},
kvSelectors: []Selector{
{
KeyFilter: "*",
TagFilters: []string{"env=production"},
},
},
keyValues: make(map[string]any),
}

err := azappcfg.loadKeyValues(ctx, mockClient)
assert.NoError(t, err)

// Should load keys with env=production tag (key1, key3, key4)
assert.Equal(t, &value1, azappcfg.keyValues["app:key1"])
assert.Equal(t, &value3, azappcfg.keyValues["app:key3"])
assert.Equal(t, &value4, azappcfg.keyValues["app:key4"])
assert.NotContains(t, azappcfg.keyValues, "app:key2") // staging env, should be filtered out
}

func TestLoadKeyValues_WithMultipleTagFilters(t *testing.T) {
ctx := context.Background()
mockClient := new(mockSettingsClient)

value1 := "value1"
value4 := "value4"

mockResponse := &settingsResponse{
settings: []azappconfig.Setting{
{
Key: toPtr("app:key1"),
Value: &value1,
Tags: map[string]*string{
"env": toPtr("production"),
"team": toPtr("backend"),
},
},
{
Key: toPtr("app:key4"),
Value: &value4,
Tags: map[string]*string{
"env": toPtr("production"),
"team": toPtr("backend"),
"feature": toPtr("new"),
},
},
},
pageETags: map[comparableSelector][]*azcore.ETag{},
}

mockClient.On("getSettings", ctx).Return(mockResponse, nil)

// Test with multiple tag filters (must match ALL)
azappcfg := &AzureAppConfiguration{
clientManager: &configurationClientManager{
staticClient: &configurationClientWrapper{client: &azappconfig.Client{}},
},
kvSelectors: []Selector{
{
KeyFilter: "*",
TagFilters: []string{"env=production", "team=backend"},
},
},
keyValues: make(map[string]any),
}

err := azappcfg.loadKeyValues(ctx, mockClient)
assert.NoError(t, err)

// Should load only keys that match BOTH env=production AND team=backend (key1, key4)
assert.Equal(t, &value1, azappcfg.keyValues["app:key1"])
assert.Equal(t, &value4, azappcfg.keyValues["app:key4"])
}

func TestSelectorComparableKey_WithTagFilter(t *testing.T) {
// Test that selectors with same TagFilter (but different order) produce the same comparable key
selector1 := Selector{
KeyFilter: "app*",
LabelFilter: "prod",
TagFilters: []string{"env=production", "team=backend"},
}

selector2 := Selector{
KeyFilter: "app*",
LabelFilter: "prod",
TagFilters: []string{"team=backend", "env=production"}, // Different order
}

key1 := selector1.comparableKey()
key2 := selector2.comparableKey()

// Should produce the same comparable key due to sorting
assert.Equal(t, key1, key2)
assert.Equal(t, `["env=production","team=backend"]`, key1.TagFilters)
assert.Equal(t, `["env=production","team=backend"]`, key2.TagFilters)
}

func TestSelectorComparableKey_WithSpecialCharacters(t *testing.T) {
// Test that selectors handle special characters in tag values correctly
selector := Selector{
KeyFilter: "app*",
LabelFilter: "prod",
TagFilters: []string{
`env=prod,staging`, // Comma in value
`description="test,with,quotes"`, // Quotes and commas
`path=c:\windows\system32`, // Backslashes
`json={"key":"value"}`, // JSON in value
},
}

key := selector.comparableKey()

// Verify JSON encoding handles all special characters properly
expected := `["description=\"test,with,quotes\"","env=prod,staging","json={\"key\":\"value\"}","path=c:\\windows\\system32"]`
assert.Equal(t, expected, key.TagFilters)
}

func TestSelectorComparableKey_WithEmptyAndNilTagFilter(t *testing.T) {
// Test empty TagFilter
selector1 := Selector{
KeyFilter: "app*",
LabelFilter: "prod",
TagFilters: []string{},
}

key1 := selector1.comparableKey()
assert.Equal(t, "", key1.TagFilters)

// Test nil TagFilter (should be handled the same as empty)
selector2 := Selector{
KeyFilter: "app*",
LabelFilter: "prod",
TagFilters: nil,
}

key2 := selector2.comparableKey()
assert.Equal(t, "", key2.TagFilters)
}

func TestSelectorComparableKey_Deterministic(t *testing.T) {
// Test that the same selector always produces the same key
selector := Selector{
KeyFilter: "app*",
LabelFilter: "prod",
TagFilters: []string{"z=last", "a=first", "m=middle"},
}

key1 := selector.comparableKey()
key2 := selector.comparableKey()

assert.Equal(t, key1, key2)
assert.Equal(t, `["a=first","m=middle","z=last"]`, key1.TagFilters) // Should be sorted
}
2 changes: 1 addition & 1 deletion azureappconfiguration/client_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,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/data/azappconfig/v2"
)

// configurationClientManager handles creation and management of app configuration clients
Expand Down
2 changes: 1 addition & 1 deletion azureappconfiguration/failover_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"time"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig"
"github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
Expand Down
2 changes: 1 addition & 1 deletion azureappconfiguration/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration

go 1.24.0

require github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.2.0
require github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2 v2.0.0

require (
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions azureappconfiguration/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2 h1:Hr5FTipp7SL07o2FvoVOX9HR
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2/go.mod h1:QyVsSSN64v5TGltphKLQ2sQxe4OBQg0J1eKRcVBnfgE=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.2.0 h1:uU4FujKFQAz31AbWOO3INV9qfIanHeIUSsGhRlcJJmg=
github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.2.0/go.mod h1:qr3M3Oy6V98VR0c5tCHKUpaeJTRQh6KYzJewRtFWqfc=
github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2 v2.0.0 h1:K7LqZL3VW+DElZhW+5tY/cp2RRFrB3W45WUG/9fhhls=
github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2 v2.0.0/go.mod h1:4IPby+BYf0rPMnMur/mNtowysFd4NoEW5U1vhrkhARA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 h1:/g8S6wk65vfC6m3FIxJ+i5QDyN9JWwXI8Hb0Img10hU=
Expand Down
4 changes: 2 additions & 2 deletions azureappconfiguration/internal/tracing/tracing.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ const (
LoadBalancingEnabledTag = "LB"

// Feature flag usage tracing
FMGoVerEnv = "MS_FEATURE_MANAGEMENT_GO_VERSION"
FMGoVerKey = "FMGoVer"
FMGoVerEnv = "MS_FEATURE_MANAGEMENT_GO_VERSION"
FMGoVerKey = "FMGoVer"
FeatureFilterTypeKey = "Filter"
CustomFilterKey = "CSTM"
TimeWindowFilterKey = "TIME"
Expand Down
Loading
Loading