Skip to content

Commit f3d48ce

Browse files
Tag filter support (#56)
* tag filter support * update dependency * update * update * dedup tagFilters * rename * update * update
1 parent bafb4b1 commit f3d48ce

13 files changed

Lines changed: 415 additions & 32 deletions

azureappconfiguration/azureappconfiguration.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import (
3333
"github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration/internal/tracing"
3434
"github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration/internal/tree"
3535
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
36-
"github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig"
36+
"github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2"
3737
decoder "github.com/go-viper/mapstructure/v2"
3838
"golang.org/x/sync/errgroup"
3939
)
@@ -55,8 +55,8 @@ type AzureAppConfiguration struct {
5555
// Settings used for refresh scenarios
5656
sentinelETags map[WatchedSetting]*azcore.ETag
5757
watchAll bool
58-
kvETags map[Selector][]*azcore.ETag
59-
ffETags map[Selector][]*azcore.ETag
58+
kvETags map[comparableSelector][]*azcore.ETag
59+
ffETags map[comparableSelector][]*azcore.ETag
6060
keyVaultRefs map[string]string // unversioned Key Vault references
6161
kvRefreshTimer refresh.Condition
6262
secretRefreshTimer refresh.Condition
@@ -121,7 +121,7 @@ func Load(ctx context.Context, authentication AuthenticationOptions, options *Op
121121
azappcfg.kvRefreshTimer = refresh.NewTimer(options.RefreshOptions.Interval)
122122
azappcfg.watchedSettings = normalizedWatchedSettings(options.RefreshOptions.WatchedSettings)
123123
azappcfg.sentinelETags = make(map[WatchedSetting]*azcore.ETag)
124-
azappcfg.kvETags = make(map[Selector][]*azcore.ETag)
124+
azappcfg.kvETags = make(map[comparableSelector][]*azcore.ETag)
125125
if len(options.RefreshOptions.WatchedSettings) == 0 {
126126
azappcfg.watchAll = true
127127
}
@@ -137,7 +137,7 @@ func Load(ctx context.Context, authentication AuthenticationOptions, options *Op
137137
azappcfg.ffSelectors = getFeatureFlagSelectors(deduplicateSelectors(options.FeatureFlagOptions.Selectors))
138138
if options.FeatureFlagOptions.RefreshOptions.Enabled {
139139
azappcfg.ffRefreshTimer = refresh.NewTimer(options.FeatureFlagOptions.RefreshOptions.Interval)
140-
azappcfg.ffETags = make(map[Selector][]*azcore.ETag)
140+
azappcfg.ffETags = make(map[comparableSelector][]*azcore.ETag)
141141
}
142142
}
143143

@@ -759,7 +759,7 @@ func deduplicateSelectors(selectors []Selector) []Selector {
759759
}
760760

761761
// Create a map to track unique selectors
762-
seen := make(map[Selector]struct{})
762+
seen := make(map[comparableSelector]struct{})
763763
var result []Selector
764764

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

773773
// Check if we've seen this selector before
774-
if _, exists := seen[selectors[i]]; !exists {
775-
seen[selectors[i]] = struct{}{}
774+
key := selectors[i].comparableKey()
775+
if _, exists := seen[key]; !exists {
776+
seen[key] = struct{}{}
776777
result = append(result, selectors[i])
777778
}
778779
}

azureappconfiguration/azureappconfiguration_test.go

Lines changed: 203 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
"github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration/internal/fm"
1717
"github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration/internal/tracing"
1818
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
19-
"github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig"
19+
"github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2"
2020
"github.com/stretchr/testify/assert"
2121
"github.com/stretchr/testify/mock"
2222
)
@@ -128,7 +128,7 @@ func TestLoadFeatureFlags_Success(t *testing.T) {
128128
{Key: toPtr(".appconfig.featureflag/Beta"), Value: &value1, ContentType: toPtr(featureFlagContentType)},
129129
{Key: toPtr(".appconfig.featureflag/Alpha"), Value: &value2, ContentType: toPtr(featureFlagContentType)},
130130
},
131-
pageETags: map[Selector][]*azcore.ETag{},
131+
pageETags: map[comparableSelector][]*azcore.ETag{},
132132
}
133133

134134
mockClient.On("getSettings", ctx).Return(mockResponse, nil)
@@ -1545,7 +1545,7 @@ func TestLoadFeatureFlags_TracingUpdated(t *testing.T) {
15451545
ContentType: toPtr(featureFlagContentType),
15461546
},
15471547
},
1548-
pageETags: map[Selector][]*azcore.ETag{},
1548+
pageETags: map[comparableSelector][]*azcore.ETag{},
15491549
}
15501550

15511551
mockClient.On("getSettings", ctx).Return(mockResponse, nil)
@@ -1601,3 +1601,203 @@ func TestLoadFeatureFlags_TracingUpdated(t *testing.T) {
16011601
// Verify max variants is included
16021602
assert.Contains(t, correlationCtx, tracing.FFMaxVariantsKey+"=3")
16031603
}
1604+
1605+
func TestLoadKeyValues_WithTagFilter(t *testing.T) {
1606+
ctx := context.Background()
1607+
mockClient := new(mockSettingsClient)
1608+
1609+
// Create mock settings with different tags
1610+
value1 := "value1"
1611+
value3 := "value3"
1612+
value4 := "value4"
1613+
1614+
mockResponse := &settingsResponse{
1615+
settings: []azappconfig.Setting{
1616+
{
1617+
Key: toPtr("app:key1"),
1618+
Value: &value1,
1619+
Tags: map[string]*string{
1620+
"env": toPtr("production"),
1621+
"team": toPtr("backend"),
1622+
},
1623+
},
1624+
{
1625+
Key: toPtr("app:key3"),
1626+
Value: &value3,
1627+
Tags: map[string]*string{
1628+
"env": toPtr("production"),
1629+
"team": toPtr("frontend"),
1630+
},
1631+
},
1632+
{
1633+
Key: toPtr("app:key4"),
1634+
Value: &value4,
1635+
Tags: map[string]*string{
1636+
"env": toPtr("production"),
1637+
"team": toPtr("backend"),
1638+
"feature": toPtr("new"),
1639+
},
1640+
},
1641+
},
1642+
pageETags: map[comparableSelector][]*azcore.ETag{},
1643+
}
1644+
1645+
mockClient.On("getSettings", ctx).Return(mockResponse, nil)
1646+
1647+
// Test with single tag filter
1648+
azappcfg := &AzureAppConfiguration{
1649+
clientManager: &configurationClientManager{
1650+
staticClient: &configurationClientWrapper{client: &azappconfig.Client{}},
1651+
},
1652+
kvSelectors: []Selector{
1653+
{
1654+
KeyFilter: "*",
1655+
TagFilters: []string{"env=production"},
1656+
},
1657+
},
1658+
keyValues: make(map[string]any),
1659+
}
1660+
1661+
err := azappcfg.loadKeyValues(ctx, mockClient)
1662+
assert.NoError(t, err)
1663+
1664+
// Should load keys with env=production tag (key1, key3, key4)
1665+
assert.Equal(t, &value1, azappcfg.keyValues["app:key1"])
1666+
assert.Equal(t, &value3, azappcfg.keyValues["app:key3"])
1667+
assert.Equal(t, &value4, azappcfg.keyValues["app:key4"])
1668+
assert.NotContains(t, azappcfg.keyValues, "app:key2") // staging env, should be filtered out
1669+
}
1670+
1671+
func TestLoadKeyValues_WithMultipleTagFilters(t *testing.T) {
1672+
ctx := context.Background()
1673+
mockClient := new(mockSettingsClient)
1674+
1675+
value1 := "value1"
1676+
value4 := "value4"
1677+
1678+
mockResponse := &settingsResponse{
1679+
settings: []azappconfig.Setting{
1680+
{
1681+
Key: toPtr("app:key1"),
1682+
Value: &value1,
1683+
Tags: map[string]*string{
1684+
"env": toPtr("production"),
1685+
"team": toPtr("backend"),
1686+
},
1687+
},
1688+
{
1689+
Key: toPtr("app:key4"),
1690+
Value: &value4,
1691+
Tags: map[string]*string{
1692+
"env": toPtr("production"),
1693+
"team": toPtr("backend"),
1694+
"feature": toPtr("new"),
1695+
},
1696+
},
1697+
},
1698+
pageETags: map[comparableSelector][]*azcore.ETag{},
1699+
}
1700+
1701+
mockClient.On("getSettings", ctx).Return(mockResponse, nil)
1702+
1703+
// Test with multiple tag filters (must match ALL)
1704+
azappcfg := &AzureAppConfiguration{
1705+
clientManager: &configurationClientManager{
1706+
staticClient: &configurationClientWrapper{client: &azappconfig.Client{}},
1707+
},
1708+
kvSelectors: []Selector{
1709+
{
1710+
KeyFilter: "*",
1711+
TagFilters: []string{"env=production", "team=backend"},
1712+
},
1713+
},
1714+
keyValues: make(map[string]any),
1715+
}
1716+
1717+
err := azappcfg.loadKeyValues(ctx, mockClient)
1718+
assert.NoError(t, err)
1719+
1720+
// Should load only keys that match BOTH env=production AND team=backend (key1, key4)
1721+
assert.Equal(t, &value1, azappcfg.keyValues["app:key1"])
1722+
assert.Equal(t, &value4, azappcfg.keyValues["app:key4"])
1723+
}
1724+
1725+
func TestSelectorComparableKey_WithTagFilter(t *testing.T) {
1726+
// Test that selectors with same TagFilter (but different order) produce the same comparable key
1727+
selector1 := Selector{
1728+
KeyFilter: "app*",
1729+
LabelFilter: "prod",
1730+
TagFilters: []string{"env=production", "team=backend"},
1731+
}
1732+
1733+
selector2 := Selector{
1734+
KeyFilter: "app*",
1735+
LabelFilter: "prod",
1736+
TagFilters: []string{"team=backend", "env=production"}, // Different order
1737+
}
1738+
1739+
key1 := selector1.comparableKey()
1740+
key2 := selector2.comparableKey()
1741+
1742+
// Should produce the same comparable key due to sorting
1743+
assert.Equal(t, key1, key2)
1744+
assert.Equal(t, `["env=production","team=backend"]`, key1.TagFilters)
1745+
assert.Equal(t, `["env=production","team=backend"]`, key2.TagFilters)
1746+
}
1747+
1748+
func TestSelectorComparableKey_WithSpecialCharacters(t *testing.T) {
1749+
// Test that selectors handle special characters in tag values correctly
1750+
selector := Selector{
1751+
KeyFilter: "app*",
1752+
LabelFilter: "prod",
1753+
TagFilters: []string{
1754+
`env=prod,staging`, // Comma in value
1755+
`description="test,with,quotes"`, // Quotes and commas
1756+
`path=c:\windows\system32`, // Backslashes
1757+
`json={"key":"value"}`, // JSON in value
1758+
},
1759+
}
1760+
1761+
key := selector.comparableKey()
1762+
1763+
// Verify JSON encoding handles all special characters properly
1764+
expected := `["description=\"test,with,quotes\"","env=prod,staging","json={\"key\":\"value\"}","path=c:\\windows\\system32"]`
1765+
assert.Equal(t, expected, key.TagFilters)
1766+
}
1767+
1768+
func TestSelectorComparableKey_WithEmptyAndNilTagFilter(t *testing.T) {
1769+
// Test empty TagFilter
1770+
selector1 := Selector{
1771+
KeyFilter: "app*",
1772+
LabelFilter: "prod",
1773+
TagFilters: []string{},
1774+
}
1775+
1776+
key1 := selector1.comparableKey()
1777+
assert.Equal(t, "", key1.TagFilters)
1778+
1779+
// Test nil TagFilter (should be handled the same as empty)
1780+
selector2 := Selector{
1781+
KeyFilter: "app*",
1782+
LabelFilter: "prod",
1783+
TagFilters: nil,
1784+
}
1785+
1786+
key2 := selector2.comparableKey()
1787+
assert.Equal(t, "", key2.TagFilters)
1788+
}
1789+
1790+
func TestSelectorComparableKey_Deterministic(t *testing.T) {
1791+
// Test that the same selector always produces the same key
1792+
selector := Selector{
1793+
KeyFilter: "app*",
1794+
LabelFilter: "prod",
1795+
TagFilters: []string{"z=last", "a=first", "m=middle"},
1796+
}
1797+
1798+
key1 := selector.comparableKey()
1799+
key2 := selector.comparableKey()
1800+
1801+
assert.Equal(t, key1, key2)
1802+
assert.Equal(t, `["a=first","m=middle","z=last"]`, key1.TagFilters) // Should be sorted
1803+
}

azureappconfiguration/client_manager.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717

1818
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
1919
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
20-
"github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig"
20+
"github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2"
2121
)
2222

2323
// configurationClientManager handles creation and management of app configuration clients

azureappconfiguration/failover_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
"time"
1515

1616
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
17-
"github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig"
17+
"github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2"
1818
"github.com/stretchr/testify/assert"
1919
"github.com/stretchr/testify/mock"
2020
)

azureappconfiguration/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration
22

33
go 1.24.0
44

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

77
require (
88
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect

azureappconfiguration/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2 h1:Hr5FTipp7SL07o2FvoVOX9HR
22
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2/go.mod h1:QyVsSSN64v5TGltphKLQ2sQxe4OBQg0J1eKRcVBnfgE=
33
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
44
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
5-
github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.2.0 h1:uU4FujKFQAz31AbWOO3INV9qfIanHeIUSsGhRlcJJmg=
6-
github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.2.0/go.mod h1:qr3M3Oy6V98VR0c5tCHKUpaeJTRQh6KYzJewRtFWqfc=
5+
github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2 v2.0.0 h1:K7LqZL3VW+DElZhW+5tY/cp2RRFrB3W45WUG/9fhhls=
6+
github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2 v2.0.0/go.mod h1:4IPby+BYf0rPMnMur/mNtowysFd4NoEW5U1vhrkhARA=
77
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
88
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
99
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 h1:/g8S6wk65vfC6m3FIxJ+i5QDyN9JWwXI8Hb0Img10hU=

azureappconfiguration/internal/tracing/tracing.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ const (
4545
LoadBalancingEnabledTag = "LB"
4646

4747
// Feature flag usage tracing
48-
FMGoVerEnv = "MS_FEATURE_MANAGEMENT_GO_VERSION"
49-
FMGoVerKey = "FMGoVer"
48+
FMGoVerEnv = "MS_FEATURE_MANAGEMENT_GO_VERSION"
49+
FMGoVerKey = "FMGoVer"
5050
FeatureFilterTypeKey = "Filter"
5151
CustomFilterKey = "CSTM"
5252
TimeWindowFilterKey = "TIME"

0 commit comments

Comments
 (0)