Skip to content

Commit dcbe67d

Browse files
Merge pull request #38 from Azure/release/v1.1.0-beta.1
Merge release/v1.1.0 beta.1 to main 07/01
2 parents 399e08e + 53a5129 commit dcbe67d

22 files changed

Lines changed: 2080 additions & 114 deletions

File tree

azureappconfiguration/azureappconfiguration.go

Lines changed: 185 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,25 @@ import (
3636
// An AzureAppConfiguration is a configuration provider that stores and manages settings sourced from Azure App Configuration.
3737
type AzureAppConfiguration struct {
3838
// Settings loaded from Azure App Configuration
39-
keyValues map[string]any
39+
keyValues map[string]any
40+
featureFlags map[string]any
4041

4142
// Settings configured from Options
4243
kvSelectors []Selector
44+
ffEnabled bool
45+
ffSelectors []Selector
4346
trimPrefixes []string
4447
watchedSettings []WatchedSetting
4548

4649
// Settings used for refresh scenarios
4750
sentinelETags map[WatchedSetting]*azcore.ETag
4851
watchAll bool
49-
pageETags map[Selector][]*azcore.ETag
52+
kvETags map[Selector][]*azcore.ETag
53+
ffETags map[Selector][]*azcore.ETag
5054
keyVaultRefs map[string]string // unversioned Key Vault references
5155
kvRefreshTimer refresh.Condition
5256
secretRefreshTimer refresh.Condition
57+
ffRefreshTimer refresh.Condition
5358
onRefreshSuccess []func()
5459
tracingOptions tracing.Options
5560

@@ -92,7 +97,10 @@ func Load(ctx context.Context, authentication AuthenticationOptions, options *Op
9297
azappcfg := new(AzureAppConfiguration)
9398
azappcfg.tracingOptions = configureTracingOptions(options)
9499
azappcfg.keyValues = make(map[string]any)
100+
azappcfg.featureFlags = make(map[string]any)
95101
azappcfg.kvSelectors = deduplicateSelectors(options.Selectors)
102+
azappcfg.ffEnabled = options.FeatureFlagOptions.Enabled
103+
96104
azappcfg.trimPrefixes = options.TrimKeyPrefixes
97105
azappcfg.clientManager = clientManager
98106
azappcfg.resolver = &keyVaultReferenceResolver{
@@ -105,7 +113,7 @@ func Load(ctx context.Context, authentication AuthenticationOptions, options *Op
105113
azappcfg.kvRefreshTimer = refresh.NewTimer(options.RefreshOptions.Interval)
106114
azappcfg.watchedSettings = normalizedWatchedSettings(options.RefreshOptions.WatchedSettings)
107115
azappcfg.sentinelETags = make(map[WatchedSetting]*azcore.ETag)
108-
azappcfg.pageETags = make(map[Selector][]*azcore.ETag)
116+
azappcfg.kvETags = make(map[Selector][]*azcore.ETag)
109117
if len(options.RefreshOptions.WatchedSettings) == 0 {
110118
azappcfg.watchAll = true
111119
}
@@ -117,6 +125,14 @@ func Load(ctx context.Context, authentication AuthenticationOptions, options *Op
117125
azappcfg.tracingOptions.KeyVaultRefreshConfigured = true
118126
}
119127

128+
if azappcfg.ffEnabled {
129+
azappcfg.ffSelectors = getFeatureFlagSelectors(deduplicateSelectors(options.FeatureFlagOptions.Selectors))
130+
if options.FeatureFlagOptions.RefreshOptions.Enabled {
131+
azappcfg.ffRefreshTimer = refresh.NewTimer(options.FeatureFlagOptions.RefreshOptions.Interval)
132+
azappcfg.ffETags = make(map[Selector][]*azcore.ETag)
133+
}
134+
}
135+
120136
if err := azappcfg.load(ctx); err != nil {
121137
return nil, err
122138
}
@@ -208,8 +224,8 @@ func (azappcfg *AzureAppConfiguration) GetBytes(options *ConstructionOptions) ([
208224
// Returns:
209225
// - An error if refresh is not configured, or if the refresh operation fails
210226
func (azappcfg *AzureAppConfiguration) Refresh(ctx context.Context) error {
211-
if azappcfg.kvRefreshTimer == nil && azappcfg.secretRefreshTimer == nil {
212-
return fmt.Errorf("refresh is not enabled for either key values or Key Vault secrets")
227+
if azappcfg.kvRefreshTimer == nil && azappcfg.secretRefreshTimer == nil && azappcfg.ffRefreshTimer == nil {
228+
return fmt.Errorf("refresh is not configured for key values, Key Vault secrets, or feature flags")
213229
}
214230

215231
// Try to set refreshInProgress to true, returning false if it was already true
@@ -236,8 +252,13 @@ func (azappcfg *AzureAppConfiguration) Refresh(ctx context.Context) error {
236252
}
237253
}
238254

255+
featureFlagRefreshed, err := azappcfg.refreshFeatureFlags(ctx, azappcfg.newFeatureFlagRefreshClient())
256+
if err != nil {
257+
return fmt.Errorf("failed to refresh feature flags: %w", err)
258+
}
259+
239260
// Only execute callbacks if actual changes were applied
240-
if keyValueRefreshed || secretRefreshed {
261+
if keyValueRefreshed || secretRefreshed || featureFlagRefreshed {
241262
for _, callback := range azappcfg.onRefreshSuccess {
242263
if callback != nil {
243264
callback()
@@ -287,6 +308,17 @@ func (azappcfg *AzureAppConfiguration) load(ctx context.Context) error {
287308
})
288309
}
289310

311+
if azappcfg.ffEnabled {
312+
eg.Go(func() error {
313+
ffClient := &selectorSettingsClient{
314+
selectors: azappcfg.ffSelectors,
315+
client: azappcfg.clientManager.staticClient.client,
316+
tracingOptions: azappcfg.tracingOptions,
317+
}
318+
return azappcfg.loadFeatureFlags(egCtx, ffClient)
319+
})
320+
}
321+
290322
return eg.Wait()
291323
}
292324

@@ -369,7 +401,7 @@ func (azappcfg *AzureAppConfiguration) loadKeyValues(ctx context.Context, settin
369401
maps.Copy(kvSettings, secrets)
370402
azappcfg.keyValues = kvSettings
371403
azappcfg.keyVaultRefs = getUnversionedKeyVaultRefs(keyVaultRefs)
372-
azappcfg.pageETags = settingsResponse.pageETags
404+
azappcfg.kvETags = settingsResponse.pageETags
373405

374406
return nil
375407
}
@@ -402,14 +434,51 @@ func (azappcfg *AzureAppConfiguration) loadKeyVaultSecrets(ctx context.Context,
402434
return secrets, fmt.Errorf("failed to resolve Key Vault references: %w", err)
403435
}
404436

405-
resolvedSecrets.Range(func(key, value interface{}) bool {
437+
resolvedSecrets.Range(func(key, value any) bool {
406438
secrets[key.(string)] = value.(string)
407439
return true
408440
})
409441

410442
return secrets, nil
411443
}
412444

445+
func (azappcfg *AzureAppConfiguration) loadFeatureFlags(ctx context.Context, settingsClient settingsClient) error {
446+
settingsResponse, err := settingsClient.getSettings(ctx)
447+
if err != nil {
448+
return err
449+
}
450+
451+
dedupFeatureFlags := make(map[string]any, len(settingsResponse.settings))
452+
for _, setting := range settingsResponse.settings {
453+
if setting.Key != nil {
454+
var v map[string]any
455+
if err := json.Unmarshal([]byte(*setting.Value), &v); err != nil {
456+
log.Printf("Invalid feature flag setting: key=%s, error=%s, just ignore", *setting.Key, err.Error())
457+
continue
458+
}
459+
azappcfg.updateFeatureFlagTracing(v)
460+
dedupFeatureFlags[*setting.Key] = v
461+
}
462+
}
463+
464+
featureFlags := make([]any, 0, len(dedupFeatureFlags))
465+
for _, v := range dedupFeatureFlags {
466+
featureFlags = append(featureFlags, v)
467+
}
468+
469+
// "feature_management": {"feature_flags": [{...}, {...}]}
470+
ffSettings := map[string]any{
471+
featureManagementSectionKey: map[string]any{
472+
featureFlagSectionKey: featureFlags,
473+
},
474+
}
475+
476+
azappcfg.ffETags = settingsResponse.pageETags
477+
azappcfg.featureFlags = ffSettings
478+
479+
return nil
480+
}
481+
413482
// refreshKeyValues checks if any watched settings have changed and reloads configuration if needed
414483
// Returns true if configuration was actually refreshed, false otherwise
415484
func (azappcfg *AzureAppConfiguration) refreshKeyValues(ctx context.Context, refreshClient refreshClient) (bool, error) {
@@ -492,6 +561,42 @@ func (azappcfg *AzureAppConfiguration) refreshKeyVaultSecrets(ctx context.Contex
492561
return changed, nil
493562
}
494563

564+
func (azappcfg *AzureAppConfiguration) refreshFeatureFlags(ctx context.Context, refreshClient refreshClient) (bool, error) {
565+
if azappcfg.ffRefreshTimer == nil ||
566+
!azappcfg.ffRefreshTimer.ShouldRefresh() {
567+
// Timer not expired, no need to refresh
568+
return false, nil
569+
}
570+
571+
// Check if any ETags have changed
572+
eTagChanged, err := refreshClient.monitor.checkIfETagChanged(ctx)
573+
if err != nil {
574+
return false, fmt.Errorf("failed to check if feature flag settings have changed: %w", err)
575+
}
576+
577+
if !eTagChanged {
578+
// No changes detected, reset timer and return
579+
azappcfg.ffRefreshTimer.Reset()
580+
return false, nil
581+
}
582+
583+
// Reload feature flags
584+
eg, egCtx := errgroup.WithContext(ctx)
585+
eg.Go(func() error {
586+
settingsClient := refreshClient.loader
587+
return azappcfg.loadFeatureFlags(egCtx, settingsClient)
588+
})
589+
590+
if err := eg.Wait(); err != nil {
591+
// Don't reset the timer if reload failed
592+
return false, fmt.Errorf("failed to reload feature flag configuration: %w", err)
593+
}
594+
595+
// Reset the timer only after successful refresh
596+
azappcfg.ffRefreshTimer.Reset()
597+
return true, nil
598+
}
599+
495600
func (azappcfg *AzureAppConfiguration) trimPrefix(key string) string {
496601
result := key
497602
for _, prefix := range azappcfg.trimPrefixes {
@@ -539,14 +644,27 @@ func deduplicateSelectors(selectors []Selector) []Selector {
539644
return result
540645
}
541646

647+
func getFeatureFlagSelectors(selectors []Selector) []Selector {
648+
for i := range selectors {
649+
selectors[i].KeyFilter = featureFlagKeyPrefix + selectors[i].KeyFilter
650+
}
651+
652+
return selectors
653+
}
654+
542655
// constructHierarchicalMap converts a flat map with delimited keys to a hierarchical structure
543656
func (azappcfg *AzureAppConfiguration) constructHierarchicalMap(separator string) map[string]any {
544657
tree := &tree.Tree{}
545658
for k, v := range azappcfg.keyValues {
546659
tree.Insert(strings.Split(k, separator), v)
547660
}
548661

549-
return tree.Build()
662+
constructedMap := tree.Build()
663+
if azappcfg.ffEnabled {
664+
maps.Copy(constructedMap, azappcfg.featureFlags)
665+
}
666+
667+
return constructedMap
550668
}
551669

552670
func configureTracingOptions(options *Options) tracing.Options {
@@ -568,6 +686,10 @@ func configureTracingOptions(options *Options) tracing.Options {
568686
tracingOption.KeyVaultConfigured = true
569687
}
570688

689+
if options.FeatureFlagOptions.Enabled {
690+
tracingOption.FeatureFlagTracing = &tracing.FeatureFlagTracing{}
691+
}
692+
571693
return tracingOption
572694
}
573695

@@ -592,7 +714,7 @@ func (azappcfg *AzureAppConfiguration) newKeyValueRefreshClient() refreshClient
592714
monitor = &pageETagsClient{
593715
client: azappcfg.clientManager.staticClient.client,
594716
tracingOptions: azappcfg.tracingOptions,
595-
pageETags: azappcfg.pageETags,
717+
pageETags: azappcfg.kvETags,
596718
}
597719
} else {
598720
monitor = &watchedSettingClient{
@@ -616,3 +738,56 @@ func (azappcfg *AzureAppConfiguration) newKeyValueRefreshClient() refreshClient
616738
},
617739
}
618740
}
741+
742+
func (azappcfg *AzureAppConfiguration) newFeatureFlagRefreshClient() refreshClient {
743+
return refreshClient{
744+
loader: &selectorSettingsClient{
745+
selectors: azappcfg.ffSelectors,
746+
client: azappcfg.clientManager.staticClient.client,
747+
tracingOptions: azappcfg.tracingOptions,
748+
},
749+
monitor: &pageETagsClient{
750+
client: azappcfg.clientManager.staticClient.client,
751+
tracingOptions: azappcfg.tracingOptions,
752+
pageETags: azappcfg.ffETags,
753+
},
754+
}
755+
}
756+
757+
func (azappcfg *AzureAppConfiguration) updateFeatureFlagTracing(featureFlag map[string]any) {
758+
if azappcfg.tracingOptions.FeatureFlagTracing == nil {
759+
return
760+
}
761+
762+
// Check for client filters and update filter tracing
763+
if conditions, ok := featureFlag[conditionsKeyName].(map[string]any); ok {
764+
if clientFilters, ok := conditions[clientFiltersKeyName].([]any); ok {
765+
for _, filter := range clientFilters {
766+
if filterMap, ok := filter.(map[string]any); ok {
767+
if filterName, ok := filterMap[nameKey].(string); ok {
768+
azappcfg.tracingOptions.FeatureFlagTracing.UpdateFeatureFilterTracing(filterName)
769+
}
770+
}
771+
}
772+
}
773+
}
774+
775+
// Update max variants count
776+
if variants, ok := featureFlag[variantsKeyName].([]any); ok {
777+
azappcfg.tracingOptions.FeatureFlagTracing.UpdateMaxVariants(len(variants))
778+
}
779+
780+
// Check if telemetry is enabled
781+
if telemetry, ok := featureFlag[telemetryKey].(map[string]any); ok {
782+
if enabled, ok := telemetry[enabledKey].(bool); ok && enabled {
783+
azappcfg.tracingOptions.FeatureFlagTracing.UsesTelemetry = true
784+
}
785+
}
786+
787+
// Check if allocation has a seed
788+
if allocation, ok := featureFlag[allocationKeyName].(map[string]any); ok {
789+
if _, hasSeed := allocation[seedKeyName]; hasSeed {
790+
azappcfg.tracingOptions.FeatureFlagTracing.UsesSeed = true
791+
}
792+
}
793+
}

0 commit comments

Comments
 (0)