@@ -36,20 +36,25 @@ import (
3636// An AzureAppConfiguration is a configuration provider that stores and manages settings sourced from Azure App Configuration.
3737type 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
210226func (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
415484func (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+
495600func (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
543656func (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
552670func 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