From ef50588e105ff42485dd5f2bdb5616f8b15545ed Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Fri, 9 May 2025 17:03:26 +0800 Subject: [PATCH 1/5] pageETage based refresh support --- .../azureappconfiguration.go | 30 ++++++++--- azureappconfiguration/go.mod | 2 +- azureappconfiguration/go.sum | 2 + azureappconfiguration/refresh_test.go | 15 ------ azureappconfiguration/settings_client.go | 52 ++++++++++++++++++- azureappconfiguration/utils.go | 4 -- 6 files changed, 78 insertions(+), 27 deletions(-) diff --git a/azureappconfiguration/azureappconfiguration.go b/azureappconfiguration/azureappconfiguration.go index 84f3ed0..f9dc277 100644 --- a/azureappconfiguration/azureappconfiguration.go +++ b/azureappconfiguration/azureappconfiguration.go @@ -42,6 +42,8 @@ type AzureAppConfiguration struct { watchedSettings []WatchedSetting sentinelETags map[WatchedSetting]*azcore.ETag + refreshAll bool + pageETags map[Selector][]*azcore.ETag keyVaultRefs map[string]string // unversioned Key Vault references kvRefreshTimer refresh.Condition secretRefreshTimer refresh.Condition @@ -99,6 +101,10 @@ 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.pageETags = make(map[Selector][]*azcore.ETag) + if len(options.RefreshOptions.WatchedSettings) == 0 { + azappcfg.refreshAll = true + } } if options.KeyVaultOptions.RefreshOptions.Enabled { @@ -355,6 +361,7 @@ func (azappcfg *AzureAppConfiguration) loadKeyValues(ctx context.Context, settin maps.Copy(kvSettings, secrets) azappcfg.keyValues = kvSettings azappcfg.keyVaultRefs = getUnversionedKeyVaultRefs(keyVaultRefs) + azappcfg.pageETags = settingsResponse.pageETags return nil } @@ -407,7 +414,7 @@ func (azappcfg *AzureAppConfiguration) refreshKeyValues(ctx context.Context, ref // Check if any ETags have changed eTagChanged, err := refreshClient.monitor.checkIfETagChanged(ctx) if err != nil { - return false, fmt.Errorf("failed to check if watched settings have changed: %w", err) + return false, fmt.Errorf("failed to check if key value settings have changed: %w", err) } if !eTagChanged { @@ -572,17 +579,28 @@ func normalizedWatchedSettings(s []WatchedSetting) []WatchedSetting { } func (azappcfg *AzureAppConfiguration) newKeyValueRefreshClient() refreshClient { - return refreshClient{ - loader: &selectorSettingsClient{ - selectors: azappcfg.kvSelectors, + var monitor eTagsClient + if azappcfg.refreshAll { + monitor = &selectorSettingsClient{ + client: azappcfg.clientManager.staticClient.client, + tracingOptions: azappcfg.tracingOptions, + pageETags: azappcfg.pageETags, + } + } else { + monitor = &watchedSettingClient{ client: azappcfg.clientManager.staticClient.client, tracingOptions: azappcfg.tracingOptions, - }, - monitor: &watchedSettingClient{ eTags: azappcfg.sentinelETags, + } + } + + return refreshClient{ + loader: &selectorSettingsClient{ + selectors: azappcfg.kvSelectors, client: azappcfg.clientManager.staticClient.client, tracingOptions: azappcfg.tracingOptions, }, + monitor: monitor, sentinels: &watchedSettingClient{ watchedSettings: azappcfg.watchedSettings, client: azappcfg.clientManager.staticClient.client, diff --git a/azureappconfiguration/go.mod b/azureappconfiguration/go.mod index 702cfb8..b220608 100644 --- a/azureappconfiguration/go.mod +++ b/azureappconfiguration/go.mod @@ -2,7 +2,7 @@ module github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration go 1.23.0 -require github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.1.0 +require github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.2.0-beta.1 require ( github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect diff --git a/azureappconfiguration/go.sum b/azureappconfiguration/go.sum index ab81167..5157467 100644 --- a/azureappconfiguration/go.sum +++ b/azureappconfiguration/go.sum @@ -4,6 +4,8 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqq github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE= github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.1.0 h1:AdaGDU3FgoUC2tsd3vsd9JblRrpFLUsS38yh1eLYfwM= github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.1.0/go.mod h1:6tpINME7dnF7bLlb8Ubj6FtM9CFZrCn7aT02pcYrklM= +github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.2.0-beta.1 h1:wSwUNd/Tbq0e0zZjWuRQL4tsBxoZ0tYIJe+rBZZQApY= +github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.2.0-beta.1/go.mod h1:0uyyPvSFLlPiPzoTTLXN6wR9sFFqL6iPVd4FAugCooo= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.0 h1:Bg8m3nq/X1DeePkAbCfb6ml6F3F0IunEhE8TMh+lY48= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.0/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 h1:mrkDCdkMsD4l9wjFGhofFHFrV43Y3c53RSLKOCJ5+Ow= diff --git a/azureappconfiguration/refresh_test.go b/azureappconfiguration/refresh_test.go index d8a3ef4..87a9f60 100644 --- a/azureappconfiguration/refresh_test.go +++ b/azureappconfiguration/refresh_test.go @@ -60,21 +60,6 @@ func TestRefresh_NotConfigured(t *testing.T) { assert.Contains(t, err.Error(), "refresh is not enabled for either key values or Key Vault secrets") } -func TestRefreshEnabled_EmptyWatchedSettings(t *testing.T) { - // Test verifying validation when refresh is enabled but no watched settings - options := &Options{ - RefreshOptions: KeyValueRefreshOptions{ - Enabled: true, // Enabled but without watched settings - WatchedSettings: []WatchedSetting{}, - }, - } - - // Verify error - err := verifyOptions(options) - require.Error(t, err) - assert.Contains(t, err.Error(), "watched settings cannot be empty") -} - func TestRefreshEnabled_IntervalTooShort(t *testing.T) { // Test verifying validation when refresh interval is too short options := &Options{ diff --git a/azureappconfiguration/settings_client.go b/azureappconfiguration/settings_client.go index 212177b..6b9dc14 100644 --- a/azureappconfiguration/settings_client.go +++ b/azureappconfiguration/settings_client.go @@ -18,11 +18,13 @@ import ( type settingsResponse struct { settings []azappconfig.Setting watchedETags map[WatchedSetting]*azcore.ETag + pageETags map[Selector][]*azcore.ETag } type selectorSettingsClient struct { selectors []Selector client *azappconfig.Client + pageETags map[Selector][]*azcore.ETag tracingOptions tracing.Options } @@ -53,6 +55,7 @@ func (s *selectorSettingsClient) getSettings(ctx context.Context) (*settingsResp } settings := make([]azappconfig.Setting, 0) + pageETags := make(map[Selector][]*azcore.ETag) for _, filter := range s.selectors { selector := azappconfig.SettingSelector{ KeyFilter: to.Ptr(filter.KeyFilter), @@ -61,18 +64,23 @@ func (s *selectorSettingsClient) getSettings(ctx context.Context) (*settingsResp } pager := s.client.NewListSettingsPager(selector, nil) + latestETags := make([]*azcore.ETag, 0) for pager.More() { page, err := pager.NextPage(ctx) if err != nil { return nil, err } else if page.Settings != nil { settings = append(settings, page.Settings...) + latestETags = append(latestETags, page.ETag) } } + + pageETags[filter] = latestETags } return &settingsResponse{ - settings: settings, + settings: settings, + pageETags: pageETags, }, nil } @@ -130,3 +138,45 @@ func (c *watchedSettingClient) checkIfETagChanged(ctx context.Context) (bool, er return false, nil } + +func (c *selectorSettingsClient) checkIfETagChanged(ctx context.Context) (bool, error) { + if c.tracingOptions.Enabled { + ctx = policy.WithHTTPHeader(ctx, tracing.CreateCorrelationContextHeader(ctx, c.tracingOptions)) + } + + for filter, pageETags := range c.pageETags { + selector := azappconfig.SettingSelector{ + KeyFilter: to.Ptr(filter.KeyFilter), + LabelFilter: to.Ptr(filter.LabelFilter), + Fields: azappconfig.AllSettingFields(), + } + + conditions := make([]azcore.MatchConditions, 0) + for _, eTag := range pageETags { + conditions = append(conditions, azcore.MatchConditions{IfNoneMatch: eTag}) + } + + pager := c.client.NewListSettingsPager(selector, &azappconfig.ListSettingsOptions{ + MatchConditions: conditions, + }) + + pageCount := 0 + for pager.More() { + pageCount++ + page, err := pager.NextPage(context.Background()) + if err != nil { + return false, err + } + // ETag changed + if page.ETag != nil { + return true, nil + } + } + + if pageCount != len(pageETags) { + return true, nil + } + } + + return false, nil +} diff --git a/azureappconfiguration/utils.go b/azureappconfiguration/utils.go index 6bb473a..104b9fb 100644 --- a/azureappconfiguration/utils.go +++ b/azureappconfiguration/utils.go @@ -35,10 +35,6 @@ func verifyOptions(options *Options) error { return fmt.Errorf("key value refresh interval cannot be less than %s", minimalRefreshInterval) } - if len(options.RefreshOptions.WatchedSettings) == 0 { - return fmt.Errorf("watched settings cannot be empty") - } - for _, watchedSetting := range options.RefreshOptions.WatchedSettings { if watchedSetting.Key == "" { return fmt.Errorf("watched setting key cannot be empty") From 46465f9f98e04d1de002dbd372b7983060c76007 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Tue, 13 May 2025 12:54:24 +0800 Subject: [PATCH 2/5] update dependencies --- azureappconfiguration/go.mod | 12 ++++++------ azureappconfiguration/go.sum | 38 +++++++++++++++++------------------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/azureappconfiguration/go.mod b/azureappconfiguration/go.mod index b220608..9b3e86c 100644 --- a/azureappconfiguration/go.mod +++ b/azureappconfiguration/go.mod @@ -2,7 +2,7 @@ module github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration go 1.23.0 -require github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.2.0-beta.1 +require github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.2.0 require ( github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect @@ -13,12 +13,12 @@ require ( ) require ( - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 - github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 github.com/go-viper/mapstructure/v2 v2.2.1 github.com/stretchr/testify v1.10.0 - golang.org/x/net v0.38.0 // indirect - golang.org/x/sync v0.12.0 - golang.org/x/text v0.23.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/sync v0.13.0 + golang.org/x/text v0.24.0 // indirect ) diff --git a/azureappconfiguration/go.sum b/azureappconfiguration/go.sum index 5157467..e25e595 100644 --- a/azureappconfiguration/go.sum +++ b/azureappconfiguration/go.sum @@ -1,13 +1,11 @@ -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 h1:DSDNVxqkoXJiko6x8a90zidoYqnYYa6c1MTzDKzKkTo= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE= -github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.1.0 h1:AdaGDU3FgoUC2tsd3vsd9JblRrpFLUsS38yh1eLYfwM= -github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.1.0/go.mod h1:6tpINME7dnF7bLlb8Ubj6FtM9CFZrCn7aT02pcYrklM= -github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.2.0-beta.1 h1:wSwUNd/Tbq0e0zZjWuRQL4tsBxoZ0tYIJe+rBZZQApY= -github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.2.0-beta.1/go.mod h1:0uyyPvSFLlPiPzoTTLXN6wR9sFFqL6iPVd4FAugCooo= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.0 h1:Bg8m3nq/X1DeePkAbCfb6ml6F3F0IunEhE8TMh+lY48= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.0/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4= +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/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 h1:mrkDCdkMsD4l9wjFGhofFHFrV43Y3c53RSLKOCJ5+Ow= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1/go.mod h1:hPv41DbqMmnxcGralanA/kVlfdH5jv3T4LxGku2E1BY= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI= @@ -38,16 +36,16 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From c2b151c12300a41e4cfe4c98a25c5645a575f79f Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Thu, 15 May 2025 11:07:43 +0800 Subject: [PATCH 3/5] update --- .../azureappconfiguration.go | 8 +++--- azureappconfiguration/settings_client.go | 25 +++++++++++-------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/azureappconfiguration/azureappconfiguration.go b/azureappconfiguration/azureappconfiguration.go index f9dc277..0e690e7 100644 --- a/azureappconfiguration/azureappconfiguration.go +++ b/azureappconfiguration/azureappconfiguration.go @@ -42,7 +42,7 @@ type AzureAppConfiguration struct { watchedSettings []WatchedSetting sentinelETags map[WatchedSetting]*azcore.ETag - refreshAll bool + watchAll bool pageETags map[Selector][]*azcore.ETag keyVaultRefs map[string]string // unversioned Key Vault references kvRefreshTimer refresh.Condition @@ -103,7 +103,7 @@ func Load(ctx context.Context, authentication AuthenticationOptions, options *Op azappcfg.sentinelETags = make(map[WatchedSetting]*azcore.ETag) azappcfg.pageETags = make(map[Selector][]*azcore.ETag) if len(options.RefreshOptions.WatchedSettings) == 0 { - azappcfg.refreshAll = true + azappcfg.watchAll = true } } @@ -580,8 +580,8 @@ func normalizedWatchedSettings(s []WatchedSetting) []WatchedSetting { func (azappcfg *AzureAppConfiguration) newKeyValueRefreshClient() refreshClient { var monitor eTagsClient - if azappcfg.refreshAll { - monitor = &selectorSettingsClient{ + if azappcfg.watchAll { + monitor = &pageETagsClient{ client: azappcfg.clientManager.staticClient.client, tracingOptions: azappcfg.tracingOptions, pageETags: azappcfg.pageETags, diff --git a/azureappconfiguration/settings_client.go b/azureappconfiguration/settings_client.go index 6b9dc14..e10235a 100644 --- a/azureappconfiguration/settings_client.go +++ b/azureappconfiguration/settings_client.go @@ -24,7 +24,6 @@ type settingsResponse struct { type selectorSettingsClient struct { selectors []Selector client *azappconfig.Client - pageETags map[Selector][]*azcore.ETag tracingOptions tracing.Options } @@ -35,6 +34,12 @@ type watchedSettingClient struct { tracingOptions tracing.Options } +type pageETagsClient struct { + pageETags map[Selector][]*azcore.ETag + client *azappconfig.Client + tracingOptions tracing.Options +} + type settingsClient interface { getSettings(ctx context.Context) (*settingsResponse, error) } @@ -64,18 +69,18 @@ func (s *selectorSettingsClient) getSettings(ctx context.Context) (*settingsResp } pager := s.client.NewListSettingsPager(selector, nil) - latestETags := make([]*azcore.ETag, 0) + eTags := make([]*azcore.ETag, 0) for pager.More() { page, err := pager.NextPage(ctx) if err != nil { return nil, err } else if page.Settings != nil { settings = append(settings, page.Settings...) - latestETags = append(latestETags, page.ETag) + eTags = append(eTags, page.ETag) } } - pageETags[filter] = latestETags + pageETags[filter] = eTags } return &settingsResponse{ @@ -139,15 +144,15 @@ func (c *watchedSettingClient) checkIfETagChanged(ctx context.Context) (bool, er return false, nil } -func (c *selectorSettingsClient) checkIfETagChanged(ctx context.Context) (bool, error) { +func (c *pageETagsClient) checkIfETagChanged(ctx context.Context) (bool, error) { if c.tracingOptions.Enabled { ctx = policy.WithHTTPHeader(ctx, tracing.CreateCorrelationContextHeader(ctx, c.tracingOptions)) } - for filter, pageETags := range c.pageETags { - selector := azappconfig.SettingSelector{ - KeyFilter: to.Ptr(filter.KeyFilter), - LabelFilter: to.Ptr(filter.LabelFilter), + for selector, pageETags := range c.pageETags { + s := azappconfig.SettingSelector{ + KeyFilter: to.Ptr(selector.KeyFilter), + LabelFilter: to.Ptr(selector.LabelFilter), Fields: azappconfig.AllSettingFields(), } @@ -156,7 +161,7 @@ func (c *selectorSettingsClient) checkIfETagChanged(ctx context.Context) (bool, conditions = append(conditions, azcore.MatchConditions{IfNoneMatch: eTag}) } - pager := c.client.NewListSettingsPager(selector, &azappconfig.ListSettingsOptions{ + pager := c.client.NewListSettingsPager(s, &azappconfig.ListSettingsOptions{ MatchConditions: conditions, }) From df613a1f714a3af899fa83b26ae1beaaf1e68096 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Thu, 15 May 2025 15:27:19 +0800 Subject: [PATCH 4/5] add tests --- azureappconfiguration/refresh_test.go | 96 +++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/azureappconfiguration/refresh_test.go b/azureappconfiguration/refresh_test.go index 87a9f60..e4bdb1d 100644 --- a/azureappconfiguration/refresh_test.go +++ b/azureappconfiguration/refresh_test.go @@ -16,6 +16,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -548,3 +549,98 @@ func TestRefreshKeyVaultSecrets_WithMockResolver_Scenarios(t *testing.T) { }) } } + +func TestRefresh_SettingsUpdated_WatchAll(t *testing.T) { + // Create initial cached values + initialKeyValues := map[string]any{ + "setting1": "initial-value1", + "setting2": "initial-value2", + "setting3": "value-unchanged", + } + + // Set up mock etags client that will detect changes + mockETags := &mockETagsClient{ + changed: true, // Simulate that etags have changed + } + + // Set up mock settings client that will return updated values + mockSettings := new(mockSettingsClient) + updatedValue1 := "updated-value1" + updatedValue2 := "new-value" + mockResponse := &settingsResponse{ + settings: []azappconfig.Setting{ + {Key: toPtr("setting1"), Value: &updatedValue1, ContentType: toPtr("")}, + {Key: toPtr("setting3"), Value: toPtr("value-unchanged"), ContentType: toPtr("")}, + {Key: toPtr("setting4"), Value: &updatedValue2, ContentType: toPtr("")}, // New setting + // Note: setting2 is missing - will be removed + }, + } + mockSettings.On("getSettings", mock.Anything).Return(mockResponse, nil) + + // Create refresh client wrapping the mocks + mockRefreshClient := refreshClient{ + monitor: mockETags, + loader: mockSettings, + } + + // Set up AzureAppConfiguration with initial values and refresh capabilities + azappcfg := &AzureAppConfiguration{ + keyValues: make(map[string]any), + kvRefreshTimer: &mockRefreshCondition{shouldRefresh: true}, + watchAll: true, // Enable watching all settings + } + + // Copy initial values + for k, v := range initialKeyValues { + azappcfg.keyValues[k] = v + } + + // Call Refresh + changed, err := azappcfg.refreshKeyValues(context.Background(), mockRefreshClient) + + // Verify results + require.NoError(t, err) + assert.True(t, changed, "Expected cache to be updated") + + // Verify cache was updated correctly + assert.Equal(t, "updated-value1", *azappcfg.keyValues["setting1"].(*string), "Setting1 should be updated") + assert.Equal(t, "value-unchanged", *azappcfg.keyValues["setting3"].(*string), "Setting3 should remain unchanged") + assert.Equal(t, "new-value", *azappcfg.keyValues["setting4"].(*string), "Setting4 should be added") + + // Verify setting2 was removed + _, exists := azappcfg.keyValues["setting2"] + assert.False(t, exists, "Setting2 should be removed") + + // Verify mocks were called as expected + mockSettings.AssertExpectations(t) + assert.Equal(t, 1, mockETags.checkCallCount, "ETag check should be called once") +} + +// TestRefreshKeyValues_NoChanges tests when no ETags change is detected +func TestRefreshKeyValues_NoChanges_WatchAll(t *testing.T) { + // Setup mocks + mockTimer := &mockRefreshCondition{shouldRefresh: true} + mockMonitor := &mockETagsClient{changed: false} + mockLoader := &mockKvRefreshClient{} + + mockClient := refreshClient{ + loader: mockLoader, + monitor: mockMonitor, + } + + // Setup provider + azappcfg := &AzureAppConfiguration{ + kvRefreshTimer: mockTimer, + watchAll: true, + } + + // Call refreshKeyValues + refreshed, err := azappcfg.refreshKeyValues(context.Background(), mockClient) + + // Verify results + assert.NoError(t, err) + assert.False(t, refreshed, "Should return false when no changes detected") + assert.Equal(t, 1, mockMonitor.checkCallCount, "Monitor should be called exactly once") + assert.Equal(t, 0, mockLoader.getCallCount, "Loader should not be called when no changes") + assert.True(t, mockTimer.resetCalled, "Timer should be reset even when no changes") +} From 65ccc504f3280b1a4316c95560f8ff4a172e9d7b Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Thu, 15 May 2025 17:08:31 +0800 Subject: [PATCH 5/5] udpate --- azureappconfiguration/azureappconfiguration.go | 4 ++++ azureappconfiguration/options.go | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/azureappconfiguration/azureappconfiguration.go b/azureappconfiguration/azureappconfiguration.go index 0e690e7..713d9c4 100644 --- a/azureappconfiguration/azureappconfiguration.go +++ b/azureappconfiguration/azureappconfiguration.go @@ -35,12 +35,15 @@ import ( // An AzureAppConfiguration is a configuration provider that stores and manages settings sourced from Azure App Configuration. type AzureAppConfiguration struct { + // Settings loaded from Azure App Configuration keyValues map[string]any + // Settings configured from Options kvSelectors []Selector trimPrefixes []string watchedSettings []WatchedSetting + // Settings used for refresh scenarios sentinelETags map[WatchedSetting]*azcore.ETag watchAll bool pageETags map[Selector][]*azcore.ETag @@ -50,6 +53,7 @@ type AzureAppConfiguration struct { onRefreshSuccess []func() tracingOptions tracing.Options + // Clients talking to Azure App Configuration/Azure Key Vault service clientManager *configurationClientManager resolver *keyVaultReferenceResolver diff --git a/azureappconfiguration/options.go b/azureappconfiguration/options.go index 9716786..750f537 100644 --- a/azureappconfiguration/options.go +++ b/azureappconfiguration/options.go @@ -23,6 +23,7 @@ type Options struct { // Each selector combines a key filter and label filter // If selectors are not provided, all key-values with no label are loaded by default. Selectors []Selector + // RefreshOptions contains optional parameters to configure the behavior of key-value settings refresh RefreshOptions KeyValueRefreshOptions @@ -36,7 +37,7 @@ type Options struct { // AuthenticationOptions contains parameters for authenticating with the Azure App Configuration service. // Either a connection string or an endpoint with credential must be provided. type AuthenticationOptions struct { - // Credential is a token credential for Azure EntraID Authenticaiton. + // Credential is a token credential for Azure EntraID Authentication. // Required when Endpoint is provided. Credential azcore.TokenCredential