32
32
AzureAppConfigurationClient ,
33
33
FeatureFlagConfigurationSetting ,
34
34
SecretReferenceConfigurationSetting ,
35
+ ConfigurationSetting ,
35
36
)
36
37
from azure .core import MatchConditions
37
38
from azure .core .exceptions import HttpResponseError , ServiceRequestError , ServiceResponseError
38
39
from azure .keyvault .secrets import SecretClient , KeyVaultSecretIdentifier
39
40
from ._models import AzureAppConfigurationKeyVaultOptions , SettingSelector
40
41
from ._constants import (
41
42
FEATURE_MANAGEMENT_KEY ,
43
+ FEATURE_FLAG_KEY ,
42
44
FEATURE_FLAG_PREFIX ,
43
45
REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE ,
44
46
ServiceFabricEnvironmentVariable ,
@@ -107,6 +109,14 @@ def load(
107
109
:paramtype on_refresh_error: Optional[Callable[[Exception], None]]
108
110
:keyword on_refresh_error: Optional callback to be invoked when an error occurs while refreshing settings. If not
109
111
specified, errors will be raised.
112
+ :paramtype feature_flag_enabled: bool
113
+ :keyword feature_flag_enabled: Optional flag to enable or disable the loading of feature flags. Default is False.
114
+ :paramtype feature_flag_selectors: List[SettingSelector]
115
+ :keyword feature_flag_selectors: Optional list of selectors to filter feature flags. By default will load all
116
+ feature flags without a label.
117
+ :paramtype feature_flag_refresh_enabled: bool
118
+ :keyword feature_flag_refresh_enabled: Optional flag to enable or disable the refresh of feature flags. Default is
119
+ False.
110
120
"""
111
121
112
122
@@ -154,6 +164,14 @@ def load(
154
164
:paramtype on_refresh_error: Optional[Callable[[Exception], None]]
155
165
:keyword on_refresh_error: Optional callback to be invoked when an error occurs while refreshing settings. If not
156
166
specified, errors will be raised.
167
+ :paramtype feature_flag_enabled: bool
168
+ :keyword feature_flag_enabled: Optional flag to enable or disable the loading of feature flags. Default is False.
169
+ :paramtype feature_flag_selectors: List[SettingSelector]
170
+ :keyword feature_flag_selectors: Optional list of selectors to filter feature flags. By default will load all
171
+ feature flags without a label.
172
+ :paramtype feature_flag_refresh_enabled: bool
173
+ :keyword feature_flag_refresh_enabled: Optional flag to enable or disable the refresh of feature flags. Default is
174
+ False.
157
175
"""
158
176
159
177
@@ -201,6 +219,7 @@ def load(*args, **kwargs) -> "AzureAppConfigurationProvider":
201
219
provider = _buildprovider (
202
220
connection_string , endpoint , credential , uses_key_vault = "UsesKeyVault" in headers , ** kwargs
203
221
)
222
+
204
223
try :
205
224
provider ._load_all (headers = headers )
206
225
except Exception as e :
@@ -221,6 +240,7 @@ def load(*args, **kwargs) -> "AzureAppConfigurationProvider":
221
240
key ,
222
241
label ,
223
242
)
243
+ provider ._refresh_on [(key , label )] = None # type: ignore
224
244
else :
225
245
_delay_failure (start_time )
226
246
raise e
@@ -453,12 +473,17 @@ def __init__(self, **kwargs) -> None:
453
473
or self ._keyvault_client_configs is not None
454
474
or self ._secret_resolver is not None
455
475
)
476
+ self ._feature_flag_enabled = kwargs .pop ("feature_flag_enabled" , False )
477
+ self ._feature_flag_selectors = kwargs .pop ("feature_flag_selectors" , [SettingSelector (key_filter = "*" )])
478
+ self ._refresh_on_feature_flags : Mapping [Tuple [str , str ], Optional [str ]] = {}
479
+ self ._feature_flag_refresh_timer : _RefreshTimer = _RefreshTimer (** kwargs )
480
+ self ._feature_flag_refresh_enabled = kwargs .pop ("feature_flag_refresh_enabled" , False )
456
481
self ._update_lock = Lock ()
457
482
self ._refresh_lock = Lock ()
458
483
459
484
def refresh (self , ** kwargs ) -> None :
460
- if not self ._refresh_on :
461
- logging .debug ("Refresh called but no refresh options set ." )
485
+ if not self ._refresh_on and not self . _feature_flag_refresh_enabled :
486
+ logging .debug ("Refresh called but no refresh enabled ." )
462
487
return
463
488
if not self ._refresh_timer .needs_refresh ():
464
489
logging .debug ("Refresh called but refresh interval not elapsed." )
@@ -469,39 +494,10 @@ def refresh(self, **kwargs) -> None:
469
494
success = False
470
495
need_refresh = False
471
496
try :
472
- updated_sentinel_keys = dict (self ._refresh_on )
473
- headers = _get_headers ("Watch" , uses_key_vault = self ._uses_key_vault , ** kwargs )
474
- for (key , label ), etag in updated_sentinel_keys .items ():
475
- try :
476
- updated_sentinel = self ._client .get_configuration_setting ( # type:ignore
477
- key = key ,
478
- label = label ,
479
- etag = etag ,
480
- match_condition = MatchConditions .IfModified ,
481
- headers = headers ,
482
- ** kwargs
483
- )
484
- if updated_sentinel is not None :
485
- logging .debug (
486
- "Refresh all triggered by key: %s label %s." ,
487
- key ,
488
- label ,
489
- )
490
- need_refresh = True
491
-
492
- updated_sentinel_keys [(key , label )] = updated_sentinel .etag
493
- except HttpResponseError as e :
494
- if e .status_code == 404 :
495
- if etag is not None :
496
- # If the sentinel is not found, it means the key/label was deleted, so we should refresh
497
- logging .debug ("Refresh all triggered by key: %s label %s." , key , label )
498
- need_refresh = True
499
- updated_sentinel_keys [(key , label )] = None
500
- else :
501
- raise e
502
- # Need to only update once, no matter how many sentinels are updated
503
- if need_refresh :
504
- self ._load_all (headers = headers , sentinel_keys = updated_sentinel_keys , ** kwargs )
497
+ if self ._refresh_on :
498
+ need_refresh = self ._refresh_configuration_settings (** kwargs )
499
+ if self ._feature_flag_refresh_enabled :
500
+ need_refresh = self ._refresh_feature_flags (** kwargs ) or need_refresh
505
501
# Even if we don't need to refresh, we should reset the timer
506
502
self ._refresh_timer .reset ()
507
503
success = True
@@ -518,31 +514,132 @@ def refresh(self, **kwargs) -> None:
518
514
elif need_refresh and self ._on_refresh_success :
519
515
self ._on_refresh_success ()
520
516
517
+ def _refresh_configuration_settings (self , ** kwargs ) -> bool :
518
+ need_refresh = False
519
+ updated_sentinel_keys = dict (self ._refresh_on )
520
+ headers = _get_headers ("Watch" , uses_key_vault = self ._uses_key_vault , ** kwargs )
521
+ for (key , label ), etag in updated_sentinel_keys .items ():
522
+ changed , updated_sentinel = self ._check_configuration_setting (
523
+ key = key , label = label , etag = etag , headers = headers , ** kwargs
524
+ )
525
+ if changed :
526
+ need_refresh = True
527
+ if updated_sentinel is not None :
528
+ updated_sentinel_keys [(key , label )] = updated_sentinel .etag
529
+ # Need to only update once, no matter how many sentinels are updated
530
+ if need_refresh :
531
+ configuration_settings , sentinel_keys = self ._load_configuration_settings (** kwargs )
532
+ if self ._feature_flag_enabled :
533
+ configuration_settings [FEATURE_MANAGEMENT_KEY ] = self ._dict [FEATURE_MANAGEMENT_KEY ]
534
+ with self ._update_lock :
535
+ self ._refresh_on = sentinel_keys
536
+ self ._dict = configuration_settings
537
+ return need_refresh
538
+
539
+ def _refresh_feature_flags (self , ** kwargs ) -> bool :
540
+ feature_flag_sentinel_keys = dict (self ._refresh_on_feature_flags )
541
+ headers = _get_headers ("Watch" , uses_key_vault = self ._uses_key_vault , ** kwargs )
542
+ for (key , label ), etag in feature_flag_sentinel_keys .items ():
543
+ changed = self ._check_configuration_setting (key = key , label = label , etag = etag , headers = headers , ** kwargs )
544
+ if changed :
545
+ feature_flags , feature_flag_sentinel_keys = self ._load_feature_flags (** kwargs )
546
+ with self ._update_lock :
547
+ updated_configurations : Dict [str , Any ] = {}
548
+ updated_configurations [FEATURE_MANAGEMENT_KEY ] = {}
549
+ updated_configurations [FEATURE_MANAGEMENT_KEY ][FEATURE_FLAG_KEY ] = feature_flags
550
+ self ._dict .update (updated_configurations )
551
+ self ._refresh_on_feature_flags = feature_flag_sentinel_keys
552
+ return True
553
+ return False
554
+
555
+ def _check_configuration_setting (
556
+ self , key , label , etag , headers , ** kwargs
557
+ ) -> Tuple [bool , Union [ConfigurationSetting , None ]]:
558
+ """
559
+ Checks if the configuration setting have been updated since the last refresh.
560
+
561
+ :keyword key: key to check for chances
562
+ :paramtype key: str
563
+ :keyword label: label to check for changes
564
+ :paramtype label: str
565
+ :keyword etag: etag to check for changes
566
+ :paramtype etag: str
567
+ :keyword headers: headers to use for the request
568
+ :paramtype headers: Mapping[str, str]
569
+ :return: A tuple with the first item being true/false if a change is detected. The second item is the updated
570
+ value if a change was detected.
571
+ :rtype: Tuple[bool, Union[ConfigurationSetting, None]]
572
+ """
573
+ try :
574
+ updated_sentinel = self ._client .get_configuration_setting ( # type: ignore
575
+ key = key , label = label , etag = etag , match_condition = MatchConditions .IfModified , headers = headers , ** kwargs
576
+ )
577
+ if updated_sentinel is not None :
578
+ logging .debug (
579
+ "Refresh all triggered by key: %s label %s." ,
580
+ key ,
581
+ label ,
582
+ )
583
+ return True , updated_sentinel
584
+ except HttpResponseError as e :
585
+ if e .status_code == 404 :
586
+ if etag is not None :
587
+ # If the sentinel is not found, it means the key/label was deleted, so we should refresh
588
+ logging .debug ("Refresh all triggered by key: %s label %s." , key , label )
589
+ return True , None
590
+ else :
591
+ raise e
592
+ return False , None
593
+
521
594
def _load_all (self , ** kwargs ):
595
+ configuration_settings , sentinel_keys = self ._load_configuration_settings (** kwargs )
596
+ if self ._feature_flag_enabled :
597
+ feature_flags , feature_flag_sentinel_keys = self ._load_feature_flags (** kwargs )
598
+ configuration_settings [FEATURE_MANAGEMENT_KEY ] = {}
599
+ configuration_settings [FEATURE_MANAGEMENT_KEY ][FEATURE_FLAG_KEY ] = feature_flags
600
+ self ._refresh_on_feature_flags = feature_flag_sentinel_keys
601
+ with self ._update_lock :
602
+ self ._refresh_on = sentinel_keys
603
+ self ._dict = configuration_settings
604
+
605
+ def _load_configuration_settings (self , ** kwargs ):
522
606
configuration_settings = {}
523
607
sentinel_keys = kwargs .pop ("sentinel_keys" , self ._refresh_on )
524
608
for select in self ._selects :
525
609
configurations = self ._client .list_configuration_settings (
526
610
key_filter = select .key_filter , label_filter = select .label_filter , ** kwargs
527
611
)
528
612
for config in configurations :
529
- key = self ._process_key_name (config )
530
- value = self ._process_key_value (config )
531
613
if isinstance (config , FeatureFlagConfigurationSetting ):
532
- feature_management = configuration_settings .get (FEATURE_MANAGEMENT_KEY , {})
533
- feature_management [key ] = value
534
- if FEATURE_MANAGEMENT_KEY not in configuration_settings :
535
- configuration_settings [FEATURE_MANAGEMENT_KEY ] = feature_management
614
+ # Feature flags are ignored when loaded by Selects, as they are selected from
615
+ # `feature_flag_selectors`
616
+ pass
536
617
else :
618
+ key = self ._process_key_name (config )
619
+ value = self ._process_key_value (config )
537
620
configuration_settings [key ] = value
538
621
# Every time we run load_all, we should update the etag of our refresh sentinels
539
622
# so they stay up-to-date.
540
623
# Sentinel keys will have unprocessed key names, so we need to use the original key.
541
624
if (config .key , config .label ) in self ._refresh_on :
542
625
sentinel_keys [(config .key , config .label )] = config .etag
543
- self ._refresh_on = sentinel_keys
544
- with self ._update_lock :
545
- self ._dict = configuration_settings
626
+ return configuration_settings , sentinel_keys
627
+
628
+ def _load_feature_flags (self , ** kwargs ):
629
+ feature_flag_sentinel_keys = {}
630
+ loaded_feature_flags = []
631
+ # Needs to be removed unknown keyword argument for list_configuration_settings
632
+ kwargs .pop ("sentinel_keys" , None )
633
+ for select in self ._feature_flag_selectors :
634
+ feature_flags = self ._client .list_configuration_settings (
635
+ key_filter = FEATURE_FLAG_PREFIX + select .key_filter , label_filter = select .label_filter , ** kwargs
636
+ )
637
+ for feature_flag in feature_flags :
638
+ loaded_feature_flags .append (json .loads (feature_flag .value ))
639
+
640
+ if self ._feature_flag_refresh_enabled :
641
+ feature_flag_sentinel_keys [(feature_flag .key , feature_flag .label )] = feature_flag .etag
642
+ return loaded_feature_flags , feature_flag_sentinel_keys
546
643
547
644
def _process_key_name (self , config ):
548
645
trimmed_key = config .key
@@ -551,8 +648,6 @@ def _process_key_name(self, config):
551
648
if config .key .startswith (trim ):
552
649
trimmed_key = config .key [len (trim ) :]
553
650
break
554
- if isinstance (config , FeatureFlagConfigurationSetting ) and trimmed_key .startswith (FEATURE_FLAG_PREFIX ):
555
- return trimmed_key [len (FEATURE_FLAG_PREFIX ) :]
556
651
return trimmed_key
557
652
558
653
def _process_key_value (self , config ):
0 commit comments