From 89c195d6aa73df7fe5c96e13e0ed9ad29414a935 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 25 Jun 2025 17:13:59 -0700 Subject: [PATCH 01/43] Sync refresh changes --- .../_azureappconfigurationprovider.py | 209 ++++++++++++------ .../_azureappconfigurationproviderbase.py | 7 +- 2 files changed, 146 insertions(+), 70 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py index b177a882ec8a..f494480728e1 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -323,15 +323,81 @@ def __init__(self, **kwargs: Any) -> None: self._on_refresh_success: Optional[Callable] = kwargs.pop("on_refresh_success", None) self._on_refresh_error: Optional[Callable[[Exception], None]] = kwargs.pop("on_refresh_error", None) - def refresh(self, **kwargs) -> None: # pylint: disable=too-many-statements - if not self._refresh_on and not self._feature_flag_refresh_enabled: + + def _refresh_configuration_settings(self, force: bool = False, **kwargs: Any) -> None: + if not self._refresh_on: logger.debug("Refresh called but no refresh enabled.") return - if not self._refresh_timer.needs_refresh(): - logger.debug("Refresh called but refresh interval not elapsed.") - return - if not self._refresh_lock.acquire(blocking=False): # pylint: disable= consider-using-with - logger.debug("Refresh called but refresh already in progress.") + success = False + need_refresh = False + error_message = """ + Failed to refresh configuration settings from Azure App Configuration. + """ + exception: Exception = RuntimeError(error_message) + is_failover_request = False + self._replica_client_manager.refresh_clients() + self._replica_client_manager.find_active_clients() + replica_count = self._replica_client_manager.get_client_count() - 1 + + while client := self._replica_client_manager.get_next_active_client(): + headers = update_correlation_context_header( + kwargs.pop("headers", {}), + "Watch", + replica_count, + self._feature_flag_enabled, + self._feature_filter_usage, + self._uses_key_vault, + self._uses_load_balancing, + is_failover_request, + self._uses_ai_configuration, + self._uses_aicc_configuration, + ) + + try: + configuration_settings: Optional[List[ConfigurationSetting]] = None + if not force: + need_refresh, self._refresh_on, configuration_settings = client.refresh_configuration_settings( + self._selects, self._refresh_on, headers=headers, **kwargs + ) + else: + # Force a refresh to make sure secrets are up to date + configuration_settings, sentinel_keys = client.load_configuration_settings(self._selects, self._refresh_on, **kwargs) + need_refresh = True + if configuration_settings is not None: + configuration_settings_processed = self._process_configurations(configuration_settings) + if need_refresh: + feature_flags = [] + uses_feature_flags = False + if self._dict.get(FEATURE_MANAGEMENT_KEY, {}).get(FEATURE_FLAG_KEY): + uses_feature_flags = True + feature_flags = self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] + self._dict = configuration_settings_processed + if uses_feature_flags: + # If feature flags were already loaded, we need to keep them + self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags + # Even if we don't need to refresh, we should reset the timer + self._refresh_timer.reset() + if self._secret_refresh_timer: + self._secret_refresh_timer.reset() + success = True + break + except AzureError as e: + exception = e + logger.warning("Failed to refresh configurations from endpoint %s", client.endpoint) + self._replica_client_manager.backoff(client) + is_failover_request = True + if not success: + self._refresh_timer.backoff() + if self._on_refresh_error: + self._on_refresh_error(exception) + return + raise exception + if self._on_refresh_success: + self._on_refresh_success() + + def _refresh_feature_flags(self, **kwargs) -> None: # pylint: disable=too-many-statements + if not self._feature_flag_refresh_enabled: + logger.debug("Feature flag refresh not enabled.") return success = False need_refresh = False @@ -340,67 +406,73 @@ def refresh(self, **kwargs) -> None: # pylint: disable=too-many-statements """ exception: Exception = RuntimeError(error_message) is_failover_request = False - try: - self._replica_client_manager.refresh_clients() - self._replica_client_manager.find_active_clients() - replica_count = self._replica_client_manager.get_client_count() - 1 - - while client := self._replica_client_manager.get_next_active_client(): - headers = update_correlation_context_header( - kwargs.pop("headers", {}), - "Watch", - replica_count, - self._feature_flag_enabled, - self._feature_filter_usage, - self._uses_key_vault, - self._uses_load_balancing, - is_failover_request, - self._uses_ai_configuration, - self._uses_aicc_configuration, + self._replica_client_manager.refresh_clients() + self._replica_client_manager.find_active_clients() + replica_count = self._replica_client_manager.get_client_count() - 1 + + while client := self._replica_client_manager.get_next_active_client(): + headers = update_correlation_context_header( + kwargs.pop("headers", {}), + "Watch", + replica_count, + self._feature_flag_enabled, + self._feature_filter_usage, + self._uses_key_vault, + self._uses_load_balancing, + is_failover_request, + self._uses_ai_configuration, + self._uses_aicc_configuration, + ) + try: + need_ff_refresh, refresh_on_feature_flags, feature_flags, filters_used = ( + client.refresh_feature_flags( + self._refresh_on_feature_flags, + self._feature_flag_selectors, + headers, + self._origin_endpoint or "", + **kwargs, + ) ) + if refresh_on_feature_flags: + self._refresh_on_feature_flags = refresh_on_feature_flags + self._feature_filter_usage = filters_used + + if need_refresh or need_ff_refresh: + self._dict[FEATURE_MANAGEMENT_KEY] = {} + self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags + # Even if we don't need to refresh, we should reset the timer + self._feature_flag_refresh_timer.reset() + success = True + break + except AzureError as e: + exception = e + logger.warning("Failed to refresh feature flags from endpoint %s", client.endpoint) + self._replica_client_manager.backoff(client) + is_failover_request = True + if not success: + self._feature_flag_refresh_timer.backoff() + if self._on_refresh_error: + self._on_refresh_error(exception) + return + raise exception + if self._on_refresh_success: + self._on_refresh_success() + + - try: - if self._refresh_on: - need_refresh, self._refresh_on, configuration_settings = client.refresh_configuration_settings( - self._selects, self._refresh_on, headers=headers, **kwargs - ) - configuration_settings_processed = self._process_configurations(configuration_settings) - if need_refresh: - self._dict = configuration_settings_processed - if self._feature_flag_refresh_enabled: - need_ff_refresh, refresh_on_feature_flags, feature_flags, filters_used = ( - client.refresh_feature_flags( - self._refresh_on_feature_flags, - self._feature_flag_selectors, - headers, - self._origin_endpoint, - **kwargs, - ) - ) - if refresh_on_feature_flags: - self._refresh_on_feature_flags = refresh_on_feature_flags - self._feature_filter_usage = filters_used - - if need_refresh or need_ff_refresh: - self._dict[FEATURE_MANAGEMENT_KEY] = {} - self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags - # Even if we don't need to refresh, we should reset the timer - self._refresh_timer.reset() - success = True - break - except AzureError as e: - exception = e - logger.warning("Failed to refresh configurations from endpoint %s", client.endpoint) - self._replica_client_manager.backoff(client) - is_failover_request = True - if not success: - self._refresh_timer.backoff() - if self._on_refresh_error: - self._on_refresh_error(exception) - return - raise exception - if self._on_refresh_success: - self._on_refresh_success() + + def refresh(self, **kwargs) -> None: # pylint: disable=too-many-statements + if not self._refresh_on and not self._feature_flag_refresh_enabled: + logger.debug("Refresh called but no refresh enabled.") + return + if not self._refresh_lock.acquire(blocking=False): # pylint: disable= consider-using-with + logger.debug("Refresh called but refresh already in progress.") + return + try: + if (self._secret_refresh_timer and self._secret_refresh_timer.needs_refresh()) or (self._refresh_timer and self._refresh_timer.needs_refresh()): + self._refresh_configuration_settings(**kwargs) + if self._feature_flag_refresh_enabled and (self._feature_flag_refresh_timer and self._feature_flag_refresh_timer.needs_refresh()): + self._refresh_feature_flags(**kwargs) finally: self._refresh_lock.release() @@ -433,11 +505,10 @@ def _load_all(self, **kwargs): self._selects, self._refresh_on, headers=headers, **kwargs ) configuration_settings_processed = self._process_configurations(configuration_settings) - if self._feature_flag_enabled: - feature_flags, feature_flag_sentinel_keys, used_filters = client.load_feature_flags( + if self._feature_flag_enabled: feature_flags, feature_flag_sentinel_keys, used_filters = client.load_feature_flags( self._feature_flag_selectors, self._feature_flag_refresh_enabled, - self._origin_endpoint, + self._origin_endpoint or "", headers=headers, **kwargs, ) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py index 736307be7154..675cf6fe092f 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -271,7 +271,7 @@ def __init__(self, **kwargs: Any) -> None: refresh_on: List[Tuple[str, str]] = kwargs.pop("refresh_on", None) or [] self._refresh_on: Mapping[Tuple[str, str], Optional[str]] = {_build_sentinel(s): None for s in refresh_on} - self._refresh_timer: _RefreshTimer = _RefreshTimer(**kwargs) + self._refresh_timer: _RefreshTimer = _RefreshTimer(refresh_interval=kwargs.pop("secret_refresh_interval", 60)) self._keyvault_credential = kwargs.pop("keyvault_credential", None) self._secret_resolver = kwargs.pop("secret_resolver", None) self._keyvault_client_configs = kwargs.pop("keyvault_client_configs", {}) @@ -280,6 +280,11 @@ def __init__(self, **kwargs: Any) -> None: or (self._keyvault_client_configs is not None and len(self._keyvault_client_configs) > 0) or self._secret_resolver is not None ) + self._secret_refresh_timer: Optional[_RefreshTimer] = ( + _RefreshTimer(refresh_interval=kwargs.pop("secret_refresh_interval", 60)) + if self._uses_key_vault and "secret_refresh_interval" in kwargs + else None + ) self._feature_flag_enabled = kwargs.pop("feature_flag_enabled", False) self._feature_flag_selectors = kwargs.pop("feature_flag_selectors", [SettingSelector(key_filter="*")]) self._refresh_on_feature_flags: Mapping[Tuple[str, str], Optional[str]] = {} From 961efda595b045c7167671278a908b26c1728e16 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 30 Jun 2025 12:32:57 -0700 Subject: [PATCH 02/43] Key Vault Refresh --- .../_azureappconfigurationprovider.py | 191 ++++++++-------- .../_azureappconfigurationproviderbase.py | 2 +- .../_azureappconfigurationproviderasync.py | 210 ++++++++++++------ .../tests/test_async_provider_refresh.py | 8 +- .../tests/test_provider_backoff.py | 14 +- .../tests/test_provider_refresh.py | 8 +- 6 files changed, 253 insertions(+), 180 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py index f494480728e1..5d157d650fb8 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -323,13 +323,19 @@ def __init__(self, **kwargs: Any) -> None: self._on_refresh_success: Optional[Callable] = kwargs.pop("on_refresh_success", None) self._on_refresh_error: Optional[Callable[[Exception], None]] = kwargs.pop("on_refresh_error", None) - - def _refresh_configuration_settings(self, force: bool = False, **kwargs: Any) -> None: - if not self._refresh_on: + def _common_refresh( + self, + refresh_operation: Callable, + error_log_message: str, + timer, # Type is _RefreshTimer but avoiding import + refresh_condition: bool, + **kwargs, + ) -> None: + if not refresh_condition: logger.debug("Refresh called but no refresh enabled.") return + success = False - need_refresh = False error_message = """ Failed to refresh configuration settings from Azure App Configuration. """ @@ -354,112 +360,104 @@ def _refresh_configuration_settings(self, force: bool = False, **kwargs: Any) -> ) try: - configuration_settings: Optional[List[ConfigurationSetting]] = None - if not force: - need_refresh, self._refresh_on, configuration_settings = client.refresh_configuration_settings( - self._selects, self._refresh_on, headers=headers, **kwargs - ) - else: - # Force a refresh to make sure secrets are up to date - configuration_settings, sentinel_keys = client.load_configuration_settings(self._selects, self._refresh_on, **kwargs) - need_refresh = True - if configuration_settings is not None: - configuration_settings_processed = self._process_configurations(configuration_settings) - if need_refresh: - feature_flags = [] - uses_feature_flags = False - if self._dict.get(FEATURE_MANAGEMENT_KEY, {}).get(FEATURE_FLAG_KEY): - uses_feature_flags = True - feature_flags = self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] - self._dict = configuration_settings_processed - if uses_feature_flags: - # If feature flags were already loaded, we need to keep them - self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags - # Even if we don't need to refresh, we should reset the timer - self._refresh_timer.reset() - if self._secret_refresh_timer: - self._secret_refresh_timer.reset() - success = True - break + # Execute the specific refresh operation + success = refresh_operation(client, headers, **kwargs) + if success: + break except AzureError as e: exception = e - logger.warning("Failed to refresh configurations from endpoint %s", client.endpoint) + logger.warning(error_log_message, client.endpoint) self._replica_client_manager.backoff(client) is_failover_request = True + if not success: - self._refresh_timer.backoff() + timer.backoff() if self._on_refresh_error: self._on_refresh_error(exception) return raise exception + if self._on_refresh_success: self._on_refresh_success() - def _refresh_feature_flags(self, **kwargs) -> None: # pylint: disable=too-many-statements - if not self._feature_flag_refresh_enabled: - logger.debug("Feature flag refresh not enabled.") - return - success = False - need_refresh = False - error_message = """ - Failed to refresh configuration settings from Azure App Configuration. - """ - exception: Exception = RuntimeError(error_message) - is_failover_request = False - self._replica_client_manager.refresh_clients() - self._replica_client_manager.find_active_clients() - replica_count = self._replica_client_manager.get_client_count() - 1 + def _refresh_configuration_settings(self, force: bool = False, **kwargs: Any) -> None: - while client := self._replica_client_manager.get_next_active_client(): - headers = update_correlation_context_header( - kwargs.pop("headers", {}), - "Watch", - replica_count, - self._feature_flag_enabled, - self._feature_filter_usage, - self._uses_key_vault, - self._uses_load_balancing, - is_failover_request, - self._uses_ai_configuration, - self._uses_aicc_configuration, - ) - try: - need_ff_refresh, refresh_on_feature_flags, feature_flags, filters_used = ( - client.refresh_feature_flags( - self._refresh_on_feature_flags, - self._feature_flag_selectors, - headers, - self._origin_endpoint or "", - **kwargs, - ) + def refresh_operation(client, headers, **inner_kwargs): + configuration_settings: Optional[List[ConfigurationSetting]] = None + need_refresh = False + + if not force: + need_refresh, self._refresh_on, configuration_settings = client.refresh_configuration_settings( + self._selects, self._refresh_on, headers=headers, **inner_kwargs ) - if refresh_on_feature_flags: - self._refresh_on_feature_flags = refresh_on_feature_flags - self._feature_filter_usage = filters_used + else: + # Force a refresh to make sure secrets are up to date + configuration_settings = client.load_configuration_settings( + self._selects, self._refresh_on, headers=headers, **inner_kwargs + ) + need_refresh = True + + configuration_settings_processed: Dict[str, Any] = {} + if configuration_settings is not None: + configuration_settings_processed = self._process_configurations(configuration_settings) - if need_refresh or need_ff_refresh: - self._dict[FEATURE_MANAGEMENT_KEY] = {} + if need_refresh: + feature_flags = [] + uses_feature_flags = False + if self._dict.get(FEATURE_MANAGEMENT_KEY, {}).get(FEATURE_FLAG_KEY): + uses_feature_flags = True + feature_flags = self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] + self._dict = configuration_settings_processed + if uses_feature_flags: + # If feature flags were already loaded, we need to keep them self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags - # Even if we don't need to refresh, we should reset the timer - self._feature_flag_refresh_timer.reset() - success = True - break - except AzureError as e: - exception = e - logger.warning("Failed to refresh feature flags from endpoint %s", client.endpoint) - self._replica_client_manager.backoff(client) - is_failover_request = True - if not success: - self._feature_flag_refresh_timer.backoff() - if self._on_refresh_error: - self._on_refresh_error(exception) - return - raise exception - if self._on_refresh_success: - self._on_refresh_success() + # Even if we don't need to refresh, we should reset the timer + self._refresh_timer.reset() + if self._secret_refresh_timer: + self._secret_refresh_timer.reset() + return True + self._common_refresh( + refresh_operation=refresh_operation, + error_log_message="Failed to refresh configurations from endpoint %s", + timer=self._refresh_timer, + refresh_condition=bool(self._refresh_on), + **kwargs, + ) + + def _refresh_feature_flags(self, **kwargs) -> None: # pylint: disable=too-many-statements + """Refresh feature flags from Azure App Configuration.""" + + def refresh_operation(client, headers, **inner_kwargs): + need_ff_refresh, refresh_on_feature_flags, feature_flags, filters_used = client.refresh_feature_flags( + self._refresh_on_feature_flags, + self._feature_flag_selectors, + headers, + self._origin_endpoint or "", + **inner_kwargs, + ) + + if refresh_on_feature_flags: + self._refresh_on_feature_flags = refresh_on_feature_flags + self._feature_filter_usage = filters_used + + if need_ff_refresh: + self._dict[FEATURE_MANAGEMENT_KEY] = {} + self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags + + # Even if we don't need to refresh, we should reset the timer + self._feature_flag_refresh_timer.reset() + return True + + self._common_refresh( + refresh_operation=refresh_operation, + error_log_message="Failed to refresh feature flags from endpoint %s", + timer=self._feature_flag_refresh_timer, + refresh_condition=self._feature_flag_refresh_enabled, + **kwargs, + ) def refresh(self, **kwargs) -> None: # pylint: disable=too-many-statements if not self._refresh_on and not self._feature_flag_refresh_enabled: @@ -469,9 +467,13 @@ def refresh(self, **kwargs) -> None: # pylint: disable=too-many-statements logger.debug("Refresh called but refresh already in progress.") return try: - if (self._secret_refresh_timer and self._secret_refresh_timer.needs_refresh()) or (self._refresh_timer and self._refresh_timer.needs_refresh()): + if (self._secret_refresh_timer and self._secret_refresh_timer.needs_refresh()) or ( + self._refresh_timer and self._refresh_timer.needs_refresh() + ): self._refresh_configuration_settings(**kwargs) - if self._feature_flag_refresh_enabled and (self._feature_flag_refresh_timer and self._feature_flag_refresh_timer.needs_refresh()): + if self._feature_flag_refresh_enabled and ( + self._feature_flag_refresh_timer and self._feature_flag_refresh_timer.needs_refresh() + ): self._refresh_feature_flags(**kwargs) finally: self._refresh_lock.release() @@ -505,7 +507,8 @@ def _load_all(self, **kwargs): self._selects, self._refresh_on, headers=headers, **kwargs ) configuration_settings_processed = self._process_configurations(configuration_settings) - if self._feature_flag_enabled: feature_flags, feature_flag_sentinel_keys, used_filters = client.load_feature_flags( + if self._feature_flag_enabled: + feature_flags, feature_flag_sentinel_keys, used_filters = client.load_feature_flags( self._feature_flag_selectors, self._feature_flag_refresh_enabled, self._origin_endpoint or "", diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py index 675cf6fe092f..170eafbb68cb 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -271,7 +271,7 @@ def __init__(self, **kwargs: Any) -> None: refresh_on: List[Tuple[str, str]] = kwargs.pop("refresh_on", None) or [] self._refresh_on: Mapping[Tuple[str, str], Optional[str]] = {_build_sentinel(s): None for s in refresh_on} - self._refresh_timer: _RefreshTimer = _RefreshTimer(refresh_interval=kwargs.pop("secret_refresh_interval", 60)) + self._refresh_timer: _RefreshTimer = _RefreshTimer(**kwargs) self._keyvault_credential = kwargs.pop("keyvault_credential", None) self._secret_resolver = kwargs.pop("secret_resolver", None) self._keyvault_client_configs = kwargs.pop("keyvault_client_configs", {}) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py index 878a525ee139..4508979aca4a 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -338,86 +338,156 @@ def __init__(self, **kwargs: Any) -> None: "on_refresh_error", None ) - async def refresh(self, **kwargs) -> None: # pylint: disable=too-many-statements - if not self._refresh_on and not self._feature_flag_refresh_enabled: + async def _common_refresh( + self, + refresh_operation: Callable, + error_log_message: str, + timer, # Type is _RefreshTimer but avoiding import + refresh_condition: bool, + **kwargs, + ) -> None: + if not refresh_condition: logger.debug("Refresh called but no refresh enabled.") return - if not self._refresh_timer.needs_refresh(): - logger.debug("Refresh called but refresh interval not elapsed.") - return - if not self._refresh_lock.acquire(blocking=False): # pylint: disable= consider-using-with - logger.debug("Refresh called but refresh already in progress.") - return + success = False - need_refresh = False error_message = """ Failed to refresh configuration settings from Azure App Configuration. """ exception: Exception = RuntimeError(error_message) is_failover_request = False - try: - await self._replica_client_manager.refresh_clients() - self._replica_client_manager.find_active_clients() - replica_count = self._replica_client_manager.get_client_count() - 1 - - while client := self._replica_client_manager.get_next_active_client(): - headers = update_correlation_context_header( - kwargs.pop("headers", {}), - "Watch", - replica_count, - self._feature_flag_enabled, - self._feature_filter_usage, - self._uses_key_vault, - self._uses_load_balancing, - is_failover_request, - self._uses_ai_configuration, - self._uses_aicc_configuration, - ) + await self._replica_client_manager.refresh_clients() + self._replica_client_manager.find_active_clients() + replica_count = self._replica_client_manager.get_client_count() - 1 + + while client := self._replica_client_manager.get_next_active_client(): + headers = update_correlation_context_header( + kwargs.pop("headers", {}), + "Watch", + replica_count, + self._feature_flag_enabled, + self._feature_filter_usage, + self._uses_key_vault, + self._uses_load_balancing, + is_failover_request, + self._uses_ai_configuration, + self._uses_aicc_configuration, + ) - try: - if self._refresh_on: - need_refresh, self._refresh_on, configuration_settings = ( - await client.refresh_configuration_settings( - self._selects, self._refresh_on, headers=headers, **kwargs - ) - ) - configuration_settings_processed = await self._process_configurations(configuration_settings) - if need_refresh: - self._dict = configuration_settings_processed - if self._feature_flag_refresh_enabled: - need_ff_refresh, refresh_on_feature_flags, feature_flags, filters_used = ( - await client.refresh_feature_flags( - self._refresh_on_feature_flags, - self._feature_flag_selectors, - headers, - self._origin_endpoint, - **kwargs, - ) - ) - if refresh_on_feature_flags: - self._refresh_on_feature_flags = refresh_on_feature_flags - self._feature_filter_usage = filters_used - - if need_refresh or need_ff_refresh: - self._dict[FEATURE_MANAGEMENT_KEY] = {} - self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags - # Even if we don't need to refresh, we should reset the timer - self._refresh_timer.reset() - success = True + try: + # Execute the specific refresh operation + success = await refresh_operation(client, headers, **kwargs) + if success: break - except AzureError as e: - exception = e - logger.warning("Failed to refresh configurations from endpoint %s", client.endpoint) - self._replica_client_manager.backoff(client) - is_failover_request = True - if not success: - self._refresh_timer.backoff() - if self._on_refresh_error: - await self._on_refresh_error(exception) - return - raise exception - if self._on_refresh_success: - await self._on_refresh_success() + except AzureError as e: + exception = e + logger.warning(error_log_message, client.endpoint) + self._replica_client_manager.backoff(client) + is_failover_request = True + + if not success: + timer.backoff() + if self._on_refresh_error: + self._on_refresh_error(exception) + return + raise exception + + if self._on_refresh_success: + await self._on_refresh_success() + + async def _refresh_configuration_settings(self, force: bool = False, **kwargs: Any) -> None: + + async def refresh_operation(client, headers, **inner_kwargs): + configuration_settings: Optional[List[ConfigurationSetting]] = None + need_refresh = False + if not force: + need_refresh, self._refresh_on, configuration_settings = await client.refresh_configuration_settings( + self._selects, self._refresh_on, headers=headers, **inner_kwargs + ) + else: + # Force a refresh to make sure secrets are up to date + configuration_settings = await client.load_configuration_settings( + self._selects, self._refresh_on, headers=headers, **inner_kwargs + ) + need_refresh = True + configuration_settings_processed: Dict[str, Any] = {} + if configuration_settings is not None: + configuration_settings_processed = await self._process_configurations(configuration_settings) + + if need_refresh: + feature_flags = [] + uses_feature_flags = False + if self._dict.get(FEATURE_MANAGEMENT_KEY, {}).get(FEATURE_FLAG_KEY): + uses_feature_flags = True + feature_flags = self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] + self._dict = configuration_settings_processed + if uses_feature_flags: + # If feature flags were already loaded, we need to keep them + self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags + + # Even if we don't need to refresh, we should reset the timer + self._refresh_timer.reset() + if self._secret_refresh_timer: + self._secret_refresh_timer.reset() + + return True + + await self._common_refresh( + refresh_operation=refresh_operation, + error_log_message="Failed to refresh configurations from endpoint %s", + timer=self._refresh_timer, + refresh_condition=bool(self._refresh_on), + **kwargs, + ) + + async def _refresh_feature_flags(self, **kwargs) -> None: # pylint: disable=too-many-statements + """Refresh feature flags from Azure App Configuration.""" + + async def refresh_operation(client, headers, **inner_kwargs): + need_ff_refresh, refresh_on_feature_flags, feature_flags, filters_used = await client.refresh_feature_flags( + self._refresh_on_feature_flags, + self._feature_flag_selectors, + headers, + self._origin_endpoint or "", + **inner_kwargs, + ) + + if refresh_on_feature_flags: + self._refresh_on_feature_flags = refresh_on_feature_flags + self._feature_filter_usage = filters_used + + if need_ff_refresh: + self._dict[FEATURE_MANAGEMENT_KEY] = {} + self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags + + # Even if we don't need to refresh, we should reset the timer + self._feature_flag_refresh_timer.reset() + return True + + await self._common_refresh( + refresh_operation=refresh_operation, + error_log_message="Failed to refresh feature flags from endpoint %s", + timer=self._feature_flag_refresh_timer, + refresh_condition=self._feature_flag_refresh_enabled, + **kwargs, + ) + + async def refresh(self, **kwargs) -> None: # pylint: disable=too-many-statements + if not self._refresh_on and not self._feature_flag_refresh_enabled: + logger.debug("Refresh called but no refresh enabled.") + return + if not self._refresh_lock.acquire(blocking=False): # pylint: disable= consider-using-with + logger.debug("Refresh called but refresh already in progress.") + return + try: + if (self._secret_refresh_timer and self._secret_refresh_timer.needs_refresh()) or ( + self._refresh_timer and self._refresh_timer.needs_refresh() + ): + await self._refresh_configuration_settings(**kwargs) + if self._feature_flag_refresh_enabled and ( + self._feature_flag_refresh_timer and self._feature_flag_refresh_timer.needs_refresh() + ): + await self._refresh_feature_flags(**kwargs) finally: self._refresh_lock.release() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_refresh.py index 0517b7a0cdcb..797373421839 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_refresh.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_refresh.py @@ -55,7 +55,7 @@ async def test_refresh(self, appconfiguration_endpoint_string, appconfiguration_ await client.refresh() assert client["refresh_message"] == "updated value" assert has_feature_flag(client, "Alpha", True) - assert mock_callback.call_count == 1 + assert mock_callback.call_count == 2 setting.value = "original value" feature_flag.enabled = False @@ -68,7 +68,7 @@ async def test_refresh(self, appconfiguration_endpoint_string, appconfiguration_ await client.refresh() assert client["refresh_message"] == "original value" assert has_feature_flag(client, "Alpha", False) - assert mock_callback.call_count == 2 + assert mock_callback.call_count == 4 setting.value = "updated value 2" feature_flag.enabled = True @@ -79,14 +79,14 @@ async def test_refresh(self, appconfiguration_endpoint_string, appconfiguration_ await client.refresh() assert client["refresh_message"] == "original value" assert has_feature_flag(client, "Alpha", False) - assert mock_callback.call_count == 2 + assert mock_callback.call_count == 4 setting.value = "original value" await appconfig_client.set_configuration_setting(setting) await client.refresh() assert client["refresh_message"] == "original value" - assert mock_callback.call_count == 2 + assert mock_callback.call_count == 4 # method: refresh @app_config_decorator_async diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_backoff.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_backoff.py index fd925b59c502..734b369e90e1 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_backoff.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_backoff.py @@ -20,12 +20,12 @@ def test_backoff(self, appconfiguration_connection_string, appconfiguration_keyv assert min_backoff == client._refresh_timer._calculate_backoff() attempts = 2 - client._refresh_timer.attempts = attempts + client._refresh_timer._attempts = attempts backoff = client._refresh_timer._calculate_backoff() assert backoff >= min_backoff and backoff <= (min_backoff * (1 << attempts)) attempts = 3 - client._refresh_timer.attempts = attempts + client._refresh_timer._attempts = attempts backoff = client._refresh_timer._calculate_backoff() assert backoff >= min_backoff and backoff <= (min_backoff * (1 << attempts)) @@ -41,12 +41,12 @@ def test_backoff_max_attempts(self, appconfiguration_connection_string, appconfi # When attempts is > 30 then it acts as if it was 30 attempts = 30 - client._refresh_timer.attempts = attempts + client._refresh_timer._attempts = attempts backoff = client._refresh_timer._calculate_backoff() assert backoff >= min_backoff and backoff <= (min_backoff * (1 << attempts)) attempts = 31 - client._refresh_timer.attempts = attempts + client._refresh_timer._attempts = attempts backoff = client._refresh_timer._calculate_backoff() assert backoff >= min_backoff and backoff <= (min_backoff * (1 << 30)) @@ -92,12 +92,12 @@ def test_backoff_invalid_attempts(self, appconfiguration_connection_string, appc # When attempts is < 1 then it acts as if it was 1 attempts = 0 - client._refresh_timer.attempts = attempts + client._refresh_timer._attempts = attempts backoff = client._refresh_timer._calculate_backoff() assert backoff == min_backoff attempts = -1 - client._refresh_timer.attempts = attempts + client._refresh_timer._attempts = attempts backoff = client._refresh_timer._calculate_backoff() assert backoff == min_backoff @@ -111,6 +111,6 @@ def test_backoff_missmatch_settings(self, appconfiguration_connection_string, ap ) # When attempts is < 1 then it acts as if it was 1 - client._refresh_timer.attempts = 0 + client._refresh_timer._attempts = 0 backoff = client._refresh_timer._calculate_backoff() assert backoff == min_backoff diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_refresh.py index a60a83d661fa..dcd6a4efd447 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_refresh.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_refresh.py @@ -48,7 +48,7 @@ def test_refresh(self, appconfiguration_endpoint_string, appconfiguration_keyvau client.refresh() assert client["refresh_message"] == "updated value" assert has_feature_flag(client, "Alpha", True) - assert mock_callback.call_count == 1 + assert mock_callback.call_count == 2 setting.value = "original value" feature_flag.enabled = False @@ -61,7 +61,7 @@ def test_refresh(self, appconfiguration_endpoint_string, appconfiguration_keyvau client.refresh() assert client["refresh_message"] == "original value" assert has_feature_flag(client, "Alpha", False) - assert mock_callback.call_count == 2 + assert mock_callback.call_count == 4 setting.value = "updated value 2" feature_flag.enabled = True @@ -72,14 +72,14 @@ def test_refresh(self, appconfiguration_endpoint_string, appconfiguration_keyvau client.refresh() assert client["refresh_message"] == "original value" assert has_feature_flag(client, "Alpha", False) - assert mock_callback.call_count == 2 + assert mock_callback.call_count == 4 setting.value = "original value" appconfig_client.set_configuration_setting(setting) client.refresh() assert client["refresh_message"] == "original value" - assert mock_callback.call_count == 2 + assert mock_callback.call_count == 4 # method: refresh @recorded_by_proxy From be0ffa7b9015236da0775ce87d99e9adacc2bc4f Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 1 Jul 2025 16:48:14 -0700 Subject: [PATCH 03/43] adding tests and fixing sync refresh --- .../_azureappconfigurationprovider.py | 17 +- .../tests/async_preparers.py | 6 + .../tests/asynctestcase.py | 10 +- .../tests/preparers.py | 8 + .../test_async_provider_feature_management.py | 2 +- .../tests/test_provider_feature_management.py | 2 +- .../tests/test_secret_refresh.py | 197 ++++++++++++++++++ .../tests/testcase.py | 93 +++++---- sdk/appconfiguration/test-resources.json | 12 ++ 9 files changed, 297 insertions(+), 50 deletions(-) create mode 100644 sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py index 5d157d650fb8..5c82e3caa046 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -344,7 +344,6 @@ def _common_refresh( self._replica_client_manager.refresh_clients() self._replica_client_manager.find_active_clients() replica_count = self._replica_client_manager.get_client_count() - 1 - while client := self._replica_client_manager.get_next_active_client(): headers = update_correlation_context_header( kwargs.pop("headers", {}), @@ -385,7 +384,7 @@ def _refresh_configuration_settings(self, force: bool = False, **kwargs: Any) -> def refresh_operation(client, headers, **inner_kwargs): configuration_settings: Optional[List[ConfigurationSetting]] = None need_refresh = False - + force = inner_kwargs.pop("force", False) if not force: need_refresh, self._refresh_on, configuration_settings = client.refresh_configuration_settings( self._selects, self._refresh_on, headers=headers, **inner_kwargs @@ -394,7 +393,7 @@ def refresh_operation(client, headers, **inner_kwargs): # Force a refresh to make sure secrets are up to date configuration_settings = client.load_configuration_settings( self._selects, self._refresh_on, headers=headers, **inner_kwargs - ) + )[0] need_refresh = True configuration_settings_processed: Dict[str, Any] = {} @@ -418,12 +417,12 @@ def refresh_operation(client, headers, **inner_kwargs): self._secret_refresh_timer.reset() return True - self._common_refresh( refresh_operation=refresh_operation, error_log_message="Failed to refresh configurations from endpoint %s", timer=self._refresh_timer, - refresh_condition=bool(self._refresh_on), + refresh_condition=bool(self._refresh_on or force), + force=force, **kwargs, ) @@ -460,16 +459,16 @@ def refresh_operation(client, headers, **inner_kwargs): ) def refresh(self, **kwargs) -> None: # pylint: disable=too-many-statements - if not self._refresh_on and not self._feature_flag_refresh_enabled: + if not self._refresh_on and not self._feature_flag_refresh_enabled and not self._secret_refresh_timer: logger.debug("Refresh called but no refresh enabled.") return if not self._refresh_lock.acquire(blocking=False): # pylint: disable= consider-using-with logger.debug("Refresh called but refresh already in progress.") return try: - if (self._secret_refresh_timer and self._secret_refresh_timer.needs_refresh()) or ( - self._refresh_timer and self._refresh_timer.needs_refresh() - ): + if (self._secret_refresh_timer and self._secret_refresh_timer.needs_refresh()): + self._refresh_configuration_settings(force=True, **kwargs) + elif(self._refresh_timer and self._refresh_timer.needs_refresh()): self._refresh_configuration_settings(**kwargs) if self._feature_flag_refresh_enabled and ( self._feature_flag_refresh_timer and self._feature_flag_refresh_timer.needs_refresh() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/async_preparers.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/async_preparers.py index ae9882aabd91..4c6c1698eebc 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/async_preparers.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/async_preparers.py @@ -15,6 +15,9 @@ async def wrapper(*args, **kwargs): appconfiguration_keyvault_secret_url = kwargs.pop("appconfiguration_keyvault_secret_url") kwargs["appconfiguration_keyvault_secret_url"] = appconfiguration_keyvault_secret_url + appconfiguration_keyvault_secret_url2 = kwargs.pop("appconfiguration_keyvault_secret_url2") + kwargs["appconfiguration_keyvault_secret_url2"] = appconfiguration_keyvault_secret_url2 + trimmed_kwargs = {k: v for k, v in kwargs.items()} trim_kwargs_from_test_function(func, trimmed_kwargs) @@ -32,6 +35,9 @@ async def wrapper(*args, **kwargs): appconfiguration_keyvault_secret_url = kwargs.pop("appconfiguration_keyvault_secret_url") kwargs["appconfiguration_keyvault_secret_url"] = appconfiguration_keyvault_secret_url + appconfiguration_keyvault_secret_url2 = kwargs.pop("appconfiguration_keyvault_secret_url2") + kwargs["appconfiguration_keyvault_secret_url2"] = appconfiguration_keyvault_secret_url2 + trimmed_kwargs = {k: v for k, v in kwargs.items()} trim_kwargs_from_test_function(func, trimmed_kwargs) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py index 58f282484a19..b9dfcfe99468 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py @@ -19,6 +19,7 @@ async def create_aad_client( trim_prefixes=[], selects={SettingSelector(key_filter="*", label_filter="\0")}, keyvault_secret_url=None, + keyvault_secret_url2=None, refresh_on=None, refresh_interval=30, secret_resolver=None, @@ -35,7 +36,7 @@ async def create_aad_client( keyvault_cred = None client = AzureAppConfigurationClient(appconfiguration_endpoint_string, cred) - await setup_configs(client, keyvault_secret_url) + await setup_configs(client, keyvault_secret_url, keyvault_secret_url2) if not secret_resolver and keyvault_secret_url and not key_vault_options: keyvault_cred = cred @@ -88,6 +89,7 @@ async def create_client( trim_prefixes=[], selects={SettingSelector(key_filter="*", label_filter="\0")}, keyvault_secret_url=None, + keyvault_secret_url2=None, refresh_on=None, refresh_interval=30, secret_resolver=None, @@ -97,7 +99,7 @@ async def create_client( feature_flag_refresh_enabled=False, ): client = AzureAppConfigurationClient.from_connection_string(appconfiguration_connection_string) - await setup_configs(client, keyvault_secret_url) + await setup_configs(client, keyvault_secret_url, keyvault_secret_url2) if not secret_resolver and keyvault_secret_url and not key_vault_options: return await load( @@ -153,9 +155,9 @@ def create_aad_sdk_client(self, appconfiguration_endpoint_string): return AzureAppConfigurationClient(appconfiguration_endpoint_string, cred, user_agent="SDK/Integration") -async def setup_configs(client, keyvault_secret_url): +async def setup_configs(client, keyvault_secret_url, keyvault_secret_url2): async with client: - for config in get_configs(keyvault_secret_url): + for config in get_configs(keyvault_secret_url, keyvault_secret_url2): await client.set_configuration_setting(config) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/preparers.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/preparers.py index d7512a12f5ce..ec0621a42203 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/preparers.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/preparers.py @@ -12,6 +12,8 @@ "appconfiguration", keyvault_secret_url="https://Sanitized.vault.azure.net/secrets/fake-secret/", appconfiguration_keyvault_secret_url="https://Sanitized.vault.azure.net/secrets/fake-secret/", + keyvault_secret_url2="https://Sanitized.vault.azure.net/secrets/fake-secret2/", + appconfiguration_keyvault_secret_url2="https://Sanitized.vault.azure.net/secrets/fake-secret2/", appconfiguration_connection_string="Endpoint=https://Sanitized.azconfig.io;Id=0-l4-s0:h5htBaY5Z1LwFz50bIQv;Secret=lamefakesecretlamefakesecretlamefakesecrett=", appconfiguration_endpoint_string="https://Sanitized.azconfig.io", ) @@ -26,6 +28,9 @@ def wrapper(*args, **kwargs): appconfiguration_keyvault_secret_url = kwargs.pop("appconfiguration_keyvault_secret_url") kwargs["appconfiguration_keyvault_secret_url"] = appconfiguration_keyvault_secret_url + appconfiguration_keyvault_secret_url2 = kwargs.pop("appconfiguration_keyvault_secret_url2") + kwargs["appconfiguration_keyvault_secret_url2"] = appconfiguration_keyvault_secret_url2 + trimmed_kwargs = {k: v for k, v in kwargs.items()} trim_kwargs_from_test_function(func, trimmed_kwargs) @@ -43,6 +48,9 @@ def wrapper(*args, **kwargs): appconfiguration_keyvault_secret_url = kwargs.pop("appconfiguration_keyvault_secret_url") kwargs["appconfiguration_keyvault_secret_url"] = appconfiguration_keyvault_secret_url + appconfiguration_keyvault_secret_url2 = kwargs.pop("appconfiguration_keyvault_secret_url2") + kwargs["appconfiguration_keyvault_secret_url2"] = appconfiguration_keyvault_secret_url2 + trimmed_kwargs = {k: v for k, v in kwargs.items()} trim_kwargs_from_test_function(func, trimmed_kwargs) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_feature_management.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_feature_management.py index 60833d449212..931b05593c43 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_feature_management.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_feature_management.py @@ -31,7 +31,7 @@ async def test_load_only_feature_flags(self, appconfiguration_connection_string) @recorded_by_proxy_async async def test_select_feature_flags(self, appconfiguration_connection_string): client = AzureAppConfigurationClient.from_connection_string(appconfiguration_connection_string) - await setup_configs(client, None) + await setup_configs(client, None, None) async with await load( connection_string=appconfiguration_connection_string, diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_feature_management.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_feature_management.py index c1ef1e8c7b40..5f66c8ed8240 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_feature_management.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_feature_management.py @@ -31,7 +31,7 @@ def test_load_only_feature_flags(self, appconfiguration_connection_string): @app_config_decorator def test_select_feature_flags(self, appconfiguration_connection_string): client = AzureAppConfigurationClient.from_connection_string(appconfiguration_connection_string) - setup_configs(client, None) + setup_configs(client, None, None) client = load( connection_string=appconfiguration_connection_string, diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py new file mode 100644 index 000000000000..54edd46bdd2e --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py @@ -0,0 +1,197 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import time +import unittest +from unittest.mock import Mock, patch +from azure.appconfiguration import SecretReferenceConfigurationSetting +from azure.appconfiguration.provider import SettingSelector +from devtools_testutils import recorded_by_proxy +from preparers import app_config_decorator_aad +from testcase import AppConfigTestCase + + +class TestSecretRefresh(AppConfigTestCase, unittest.TestCase): + @recorded_by_proxy + @app_config_decorator_aad + def test_secret_refresh_timer(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): + """Test that secrets are refreshed based on the secret_refresh_interval.""" + mock_callback = Mock() + + # Create client with key vault reference and secret refresh interval + client = self.create_aad_client( + appconfiguration_endpoint_string, + selects={SettingSelector(key_filter="*", label_filter="prod")}, + keyvault_secret_url=appconfiguration_keyvault_secret_url, + keyvault_secret_url2=appconfiguration_keyvault_secret_url2, + on_refresh_success=mock_callback, + refresh_interval=999999, + secret_refresh_interval=1 + ) + + # Verify initial state + assert client["secret"] == "Very secret value" + assert mock_callback.call_count == 0 + + # Mock the refresh method to track calls + with patch.object(client, 'refresh') as mock_refresh: + # Wait for the secret refresh interval to pass + time.sleep(2) + + client.refresh() + + # Verify refresh was called + assert mock_refresh.call_count >= 1 + + # Wait again to ensure multiple refreshes + time.sleep(2) + client.refresh() + + # Should have been called at least twice now + assert mock_refresh.call_count >= 2 + + @recorded_by_proxy + @app_config_decorator_aad + def test_secret_refresh_with_updated_values(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): + """Test that secrets are refreshed with updated values.""" + mock_callback = Mock() + + # Create client with the mock secret resolver + client = self.create_aad_client( + appconfiguration_endpoint_string, + selects={SettingSelector(key_filter="*", label_filter="prod")}, + keyvault_secret_url=appconfiguration_keyvault_secret_url, + keyvault_secret_url2=appconfiguration_keyvault_secret_url2, + on_refresh_success=mock_callback, + refresh_interval=999999, + secret_refresh_interval=1 # Using a short interval for testing + ) + + # Add a key vault reference to the client (this will use mock resolver) + appconfig_client = self.create_aad_sdk_client(appconfiguration_endpoint_string) + + # Get and modify a key vault reference setting + kv_setting = appconfig_client.get_configuration_setting(key="secret", label="prod") + assert kv_setting is not None + + # Verify initial value from mock resolver + assert client["secret"] == "Very secret value" + assert kv_setting is not None + assert isinstance(kv_setting, SecretReferenceConfigurationSetting) + # Update the secret_id (which is the value for SecretReferenceConfigurationSetting) + kv_setting.secret_id = appconfiguration_keyvault_secret_url2 + appconfig_client.set_configuration_setting(kv_setting) + + # Wait for the secret refresh interval to pass + time.sleep(2) + + # Access the value again to trigger refresh + client.refresh() + + # Verify the value was updated + assert client["secret"] == "Very secret value2" + assert mock_callback.call_count >= 1 + + @recorded_by_proxy + @app_config_decorator_aad + def test_no_secret_refresh_without_timer(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): + """Test that secrets are not refreshed if secret_refresh_interval is not set.""" + mock_callback = Mock() + + # Create client without specifying secret_refresh_interval + client = self.create_aad_client( + appconfiguration_endpoint_string, + selects={SettingSelector(key_filter="*", label_filter="prod")}, + keyvault_secret_url=appconfiguration_keyvault_secret_url, + keyvault_secret_url2=appconfiguration_keyvault_secret_url2, + on_refresh_success=mock_callback, + refresh_interval=999999, + ) + + # Verify initial state + assert client["secret"] == "Very secret value" + + # Mock the refresh method to track calls + with patch('time.time') as mock_time: + # Make time.time() return increasing values to simulate passage of time + mock_time.side_effect = [time.time(), time.time() + 100] + + # Access the key vault reference - this shouldn't trigger an auto-refresh since + # we didn't set a secret_refresh_interval + client.refresh() + + # Access it again to verify no auto-refresh due to secrets timer + client.refresh() + + # The mock_time should have been called twice (for our side_effect setup) + # but there should be no automatic refresh caused by the secret timer + assert mock_time.call_count == 2 + + @recorded_by_proxy + @app_config_decorator_aad + def test_secret_refresh_timer_triggers_refresh(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): + """Test that the secret refresh timer triggers a refresh after the specified interval.""" + mock_callback = Mock() + + # Create client with key vault reference and separate refresh intervals + client = self.create_aad_client( + appconfiguration_endpoint_string, + selects={SettingSelector(key_filter="*", label_filter="prod")}, + keyvault_secret_url=appconfiguration_keyvault_secret_url, + keyvault_secret_url2=appconfiguration_keyvault_secret_url2, + on_refresh_success=mock_callback, + refresh_interval=999999, + secret_refresh_interval=5 # Secret refresh interval is short + ) + + # Now patch the refresh method and _secret_refresh_timer to control behavior + with patch.object(client, 'refresh') as mock_refresh: + # Now patch the _secret_refresh_timer to control its behavior + with patch.object(client, '_secret_refresh_timer') as mock_timer: + # Make needs_refresh() return True to simulate timer expiration + mock_timer.needs_refresh.return_value = True + + # Access a key vault reference which should trigger refresh due to timer + client.refresh() + + # Verify refresh was called + assert mock_refresh.call_count > 0 + + @recorded_by_proxy + @app_config_decorator_aad + def test_secret_refresh_interval_parameter(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): + """Test that secret_refresh_interval parameter is correctly passed and used.""" + mock_callback = Mock() + + # Create client with specific secret_refresh_interval + client = self.create_aad_client( + appconfiguration_endpoint_string, + selects={SettingSelector(key_filter="*", label_filter="prod")}, + keyvault_secret_url=appconfiguration_keyvault_secret_url, + keyvault_secret_url2=appconfiguration_keyvault_secret_url2, + on_refresh_success=mock_callback, + refresh_interval=999999, + secret_refresh_interval=42 # Use a specific value we can check for + ) + + # Verify the secret refresh timer exists + assert client._secret_refresh_timer is not None + + # We can only verify that it exists, but can't directly access the internal refresh_interval + # as it's a protected attribute + + # Check with no refresh interval to ensure it's properly handled + client2 = self.create_aad_client( + appconfiguration_endpoint_string, + selects={SettingSelector(key_filter="*", label_filter="prod")}, + keyvault_secret_url=appconfiguration_keyvault_secret_url, + keyvault_secret_url2=appconfiguration_keyvault_secret_url2, + on_refresh_success=mock_callback + # No secret_refresh_interval specified + ) + + # Verify timer is created only when secret_refresh_interval is provided + assert client._secret_refresh_timer is not None + assert client2._secret_refresh_timer is None diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py index 07f570527ac3..14b7ef263c5a 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py @@ -5,7 +5,7 @@ # license information. # -------------------------------------------------------------------------- from devtools_testutils import AzureRecordedTestCase -from azure.appconfiguration import AzureAppConfigurationClient, ConfigurationSetting, FeatureFlagConfigurationSetting +from azure.appconfiguration import AzureAppConfigurationClient, ConfigurationSetting, FeatureFlagConfigurationSetting, SecretReferenceConfigurationSetting from azure.appconfiguration.provider import SettingSelector, load, AzureAppConfigurationKeyVaultOptions from test_constants import FEATURE_MANAGEMENT_KEY, FEATURE_FLAG_KEY @@ -17,6 +17,7 @@ def create_aad_client( trim_prefixes=[], selects={SettingSelector(key_filter="*", label_filter="\0")}, keyvault_secret_url=None, + keyvault_secret_url2=None, refresh_on=None, refresh_interval=30, secret_resolver=None, @@ -24,42 +25,49 @@ def create_aad_client( on_refresh_success=None, feature_flag_enabled=False, feature_flag_refresh_enabled=False, + secret_refresh_interval=None, ): cred = self.get_credential(AzureAppConfigurationClient) client = AzureAppConfigurationClient(appconfiguration_endpoint_string, cred) - setup_configs(client, keyvault_secret_url) + setup_configs(client, keyvault_secret_url, keyvault_secret_url2) if not secret_resolver and keyvault_secret_url and not key_vault_options: keyvault_cred = cred - return load( - credential=cred, - endpoint=appconfiguration_endpoint_string, - trim_prefixes=trim_prefixes, - selects=selects, - refresh_on=refresh_on, - refresh_interval=refresh_interval, - user_agent="SDK/Integration", - keyvault_credential=keyvault_cred, - on_refresh_success=on_refresh_success, - feature_flag_enabled=feature_flag_enabled, - feature_flag_refresh_enabled=feature_flag_refresh_enabled, - ) + kwargs = { + "credential": cred, + "endpoint": appconfiguration_endpoint_string, + "trim_prefixes": trim_prefixes, + "selects": selects, + "refresh_on": refresh_on, + "refresh_interval": refresh_interval, + "user_agent": "SDK/Integration", + "keyvault_credential": keyvault_cred, + "on_refresh_success": on_refresh_success, + "feature_flag_enabled": feature_flag_enabled, + "feature_flag_refresh_enabled": feature_flag_refresh_enabled, + } + if secret_refresh_interval is not None: + kwargs["secret_refresh_interval"] = secret_refresh_interval + return load(**kwargs) if key_vault_options: if not key_vault_options.secret_resolver: key_vault_options = AzureAppConfigurationKeyVaultOptions(credential=cred) - return load( - credential=cred, - endpoint=appconfiguration_endpoint_string, - trim_prefixes=trim_prefixes, - selects=selects, - refresh_on=refresh_on, - refresh_interval=refresh_interval, - user_agent="SDK/Integration", - key_vault_options=key_vault_options, - on_refresh_success=on_refresh_success, - feature_flag_enabled=feature_flag_enabled, - feature_flag_refresh_enabled=feature_flag_refresh_enabled, - ) + kwargs = { + "credential": cred, + "endpoint": appconfiguration_endpoint_string, + "trim_prefixes": trim_prefixes, + "selects": selects, + "refresh_on": refresh_on, + "refresh_interval": refresh_interval, + "user_agent": "SDK/Integration", + "key_vault_options": key_vault_options, + "on_refresh_success": on_refresh_success, + "feature_flag_enabled": feature_flag_enabled, + "feature_flag_refresh_enabled": feature_flag_refresh_enabled, + } + if secret_refresh_interval is not None: + kwargs["secret_refresh_interval"] = secret_refresh_interval + return load(**kwargs) return load( credential=cred, endpoint=appconfiguration_endpoint_string, @@ -80,6 +88,7 @@ def create_client( trim_prefixes=[], selects={SettingSelector(key_filter="*", label_filter="\0")}, keyvault_secret_url=None, + keyvault_secret_url2=None, refresh_on=None, refresh_interval=30, secret_resolver=None, @@ -89,7 +98,7 @@ def create_client( feature_flag_refresh_enabled=False, ): client = AzureAppConfigurationClient.from_connection_string(appconfiguration_connection_string) - setup_configs(client, keyvault_secret_url) + setup_configs(client, keyvault_secret_url, keyvault_secret_url2) if not secret_resolver and keyvault_secret_url and not key_vault_options: return load( @@ -145,12 +154,12 @@ def create_aad_sdk_client(self, appconfiguration_endpoint_string): return AzureAppConfigurationClient(appconfiguration_endpoint_string, cred, user_agent="SDK/Integration") -def setup_configs(client, keyvault_secret_url): - for config in get_configs(keyvault_secret_url): +def setup_configs(client, keyvault_secret_url, keyvault_secret_url2): + for config in get_configs(keyvault_secret_url, keyvault_secret_url2): client.set_configuration_setting(config) -def get_configs(keyvault_secret_url): +def get_configs(keyvault_secret_url, keyvault_secret_url2): configs = [] configs.append(create_config_setting("message", "\0", "hi")) configs.append(create_config_setting("message", "dev", "test")) @@ -168,11 +177,18 @@ def get_configs(keyvault_secret_url): ) if keyvault_secret_url: configs.append( - create_config_setting( + create_secret_config_setting( "secret", "prod", - '{"uri":"' + keyvault_secret_url + '"}', - "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8", + keyvault_secret_url, + ) + ) + if keyvault_secret_url2: + configs.append( + create_secret_config_setting( + "secret2", + "prod", + keyvault_secret_url2, ) ) return configs @@ -186,6 +202,13 @@ def create_config_setting(key, label, value, content_type="text/plain"): content_type=content_type, ) +def create_secret_config_setting(key, label, value): + return SecretReferenceConfigurationSetting( + key=key, + label=label, + secret_id=value, + ) + def create_feature_flag_config_setting(key, label, enabled): return FeatureFlagConfigurationSetting( diff --git a/sdk/appconfiguration/test-resources.json b/sdk/appconfiguration/test-resources.json index 4769b7120403..f4feb12f19e3 100644 --- a/sdk/appconfiguration/test-resources.json +++ b/sdk/appconfiguration/test-resources.json @@ -121,6 +121,18 @@ "value": "Very secret value" } }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "name": "[concat(parameters('baseName'), '/TestSecret2')]", + "apiVersion": "2016-10-01", + "location": "[parameters('location')]", + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', parameters('baseName'))]" + ], + "properties": { + "value": "Very secret value 2" + } + }, { "type": "Microsoft.AppConfiguration/configurationStores/keyValues", "apiVersion": "2020-07-01-preview", From 1b0f5543d16d51646597ebd3d33937b3f7047868 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 2 Jul 2025 09:51:13 -0700 Subject: [PATCH 04/43] Updating Async --- .../_azureappconfigurationprovider.py | 2 + .../_azureappconfigurationproviderasync.py | 19 +- .../tests/asynctestcase.py | 77 +++---- .../tests/test_async_secret_refresh.py | 199 ++++++++++++++++++ 4 files changed, 247 insertions(+), 50 deletions(-) create mode 100644 sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py index 5c82e3caa046..7d425bf9dfad 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -344,6 +344,7 @@ def _common_refresh( self._replica_client_manager.refresh_clients() self._replica_client_manager.find_active_clients() replica_count = self._replica_client_manager.get_client_count() - 1 + while client := self._replica_client_manager.get_next_active_client(): headers = update_correlation_context_header( kwargs.pop("headers", {}), @@ -417,6 +418,7 @@ def refresh_operation(client, headers, **inner_kwargs): self._secret_refresh_timer.reset() return True + self._common_refresh( refresh_operation=refresh_operation, error_log_message="Failed to refresh configurations from endpoint %s", diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py index 4508979aca4a..844b70c883a3 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -400,16 +400,18 @@ async def _refresh_configuration_settings(self, force: bool = False, **kwargs: A async def refresh_operation(client, headers, **inner_kwargs): configuration_settings: Optional[List[ConfigurationSetting]] = None need_refresh = False + force = inner_kwargs.pop("force", False) if not force: need_refresh, self._refresh_on, configuration_settings = await client.refresh_configuration_settings( self._selects, self._refresh_on, headers=headers, **inner_kwargs ) else: # Force a refresh to make sure secrets are up to date - configuration_settings = await client.load_configuration_settings( + configuration_settings = (await client.load_configuration_settings( self._selects, self._refresh_on, headers=headers, **inner_kwargs - ) + ))[0] need_refresh = True + configuration_settings_processed: Dict[str, Any] = {} if configuration_settings is not None: configuration_settings_processed = await self._process_configurations(configuration_settings) @@ -436,7 +438,8 @@ async def refresh_operation(client, headers, **inner_kwargs): refresh_operation=refresh_operation, error_log_message="Failed to refresh configurations from endpoint %s", timer=self._refresh_timer, - refresh_condition=bool(self._refresh_on), + refresh_condition=bool(self._refresh_on or force), + force=force, **kwargs, ) @@ -473,16 +476,16 @@ async def refresh_operation(client, headers, **inner_kwargs): ) async def refresh(self, **kwargs) -> None: # pylint: disable=too-many-statements - if not self._refresh_on and not self._feature_flag_refresh_enabled: + if not self._refresh_on and not self._feature_flag_refresh_enabled and not self._secret_refresh_timer: logger.debug("Refresh called but no refresh enabled.") return if not self._refresh_lock.acquire(blocking=False): # pylint: disable= consider-using-with logger.debug("Refresh called but refresh already in progress.") return try: - if (self._secret_refresh_timer and self._secret_refresh_timer.needs_refresh()) or ( - self._refresh_timer and self._refresh_timer.needs_refresh() - ): + if (self._secret_refresh_timer and self._secret_refresh_timer.needs_refresh()): + await self._refresh_configuration_settings(force=True, **kwargs) + elif (self._refresh_timer and self._refresh_timer.needs_refresh()): await self._refresh_configuration_settings(**kwargs) if self._feature_flag_refresh_enabled and ( self._feature_flag_refresh_timer and self._feature_flag_refresh_timer.needs_refresh() @@ -524,7 +527,7 @@ async def _load_all(self, **kwargs): feature_flags, feature_flag_sentinel_keys, used_filters = await client.load_feature_flags( self._feature_flag_selectors, self._feature_flag_refresh_enabled, - self._origin_endpoint, + self._origin_endpoint or "", headers=headers, **kwargs, ) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py index b9dfcfe99468..aa0a600c22b2 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py @@ -9,7 +9,6 @@ from testcase import get_configs from azure.appconfiguration.provider.aio import load from azure.appconfiguration.provider import SettingSelector, AzureAppConfigurationKeyVaultOptions -from test_constants import FEATURE_MANAGEMENT_KEY, FEATURE_FLAG_KEY class AppConfigTestCase(AzureRecordedTestCase): @@ -27,48 +26,49 @@ async def create_aad_client( on_refresh_success=None, feature_flag_enabled=False, feature_flag_refresh_enabled=False, + secret_refresh_interval=None, ): cred = self.get_credential(AzureAppConfigurationClient, is_async=True) - - if not secret_resolver and keyvault_secret_url: - keyvault_cred = cred - else: - keyvault_cred = None - client = AzureAppConfigurationClient(appconfiguration_endpoint_string, cred) await setup_configs(client, keyvault_secret_url, keyvault_secret_url2) if not secret_resolver and keyvault_secret_url and not key_vault_options: keyvault_cred = cred - return await load( - credential=cred, - endpoint=appconfiguration_endpoint_string, - trim_prefixes=trim_prefixes, - selects=selects, - refresh_on=refresh_on, - refresh_interval=refresh_interval, - user_agent="SDK/Integration", - keyvault_credential=keyvault_cred, - on_refresh_success=on_refresh_success, - feature_flag_enabled=feature_flag_enabled, - feature_flag_refresh_enabled=feature_flag_refresh_enabled, - ) + kwargs = { + "credential": cred, + "endpoint": appconfiguration_endpoint_string, + "trim_prefixes": trim_prefixes, + "selects": selects, + "refresh_on": refresh_on, + "refresh_interval": refresh_interval, + "user_agent": "SDK/Integration", + "keyvault_credential": keyvault_cred, + "on_refresh_success": on_refresh_success, + "feature_flag_enabled": feature_flag_enabled, + "feature_flag_refresh_enabled": feature_flag_refresh_enabled, + } + if secret_refresh_interval is not None: + kwargs["secret_refresh_interval"] = secret_refresh_interval + return await load(**kwargs) if key_vault_options: if not key_vault_options.secret_resolver: key_vault_options = AzureAppConfigurationKeyVaultOptions(credential=cred) - return await load( - credential=cred, - endpoint=appconfiguration_endpoint_string, - trim_prefixes=trim_prefixes, - selects=selects, - refresh_on=refresh_on, - refresh_interval=refresh_interval, - user_agent="SDK/Integration", - key_vault_options=key_vault_options, - on_refresh_success=on_refresh_success, - feature_flag_enabled=feature_flag_enabled, - feature_flag_refresh_enabled=feature_flag_refresh_enabled, - ) + kwargs = { + "credential": cred, + "endpoint": appconfiguration_endpoint_string, + "trim_prefixes": trim_prefixes, + "selects": selects, + "refresh_on": refresh_on, + "refresh_interval": refresh_interval, + "user_agent": "SDK/Integration", + "key_vault_options": key_vault_options, + "on_refresh_success": on_refresh_success, + "feature_flag_enabled": feature_flag_enabled, + "feature_flag_refresh_enabled": feature_flag_refresh_enabled, + } + if secret_refresh_interval is not None: + kwargs["secret_refresh_interval"] = secret_refresh_interval + return await load(**kwargs) return await load( credential=cred, endpoint=appconfiguration_endpoint_string, @@ -150,8 +150,8 @@ def create_sdk_client(appconfiguration_connection_string): appconfiguration_connection_string, user_agent="SDK/Integration" ) - def create_aad_sdk_client(self, appconfiguration_endpoint_string): - cred = self.get_credential(AzureAppConfigurationClient) + async def create_aad_sdk_client(self, appconfiguration_endpoint_string): + cred = self.get_credential(AzureAppConfigurationClient, is_async=True) return AzureAppConfigurationClient(appconfiguration_endpoint_string, cred, user_agent="SDK/Integration") @@ -159,10 +159,3 @@ async def setup_configs(client, keyvault_secret_url, keyvault_secret_url2): async with client: for config in get_configs(keyvault_secret_url, keyvault_secret_url2): await client.set_configuration_setting(config) - - -def has_feature_flag(client, feature_id, enabled=False): - for feature_flag in client[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY]: - if feature_flag["id"] == feature_id: - return feature_flag["enabled"] == enabled - return False diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py new file mode 100644 index 000000000000..47fa14e4b04b --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py @@ -0,0 +1,199 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import time +import asyncio +import unittest +from unittest.mock import Mock, patch +from azure.appconfiguration import SecretReferenceConfigurationSetting +from azure.appconfiguration.provider import SettingSelector +from devtools_testutils import recorded_by_proxy +from async_preparers import app_config_aad_decorator_async +from asynctestcase import AppConfigTestCase + + +class TestAsyncSecretRefresh(AppConfigTestCase, unittest.TestCase): + @recorded_by_proxy + @app_config_aad_decorator_async + async def test_secret_refresh_timer(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): + """Test that secrets are refreshed based on the secret_refresh_interval.""" + mock_callback = Mock() + + # Create client with key vault reference and secret refresh interval + client = await self.create_aad_client( + appconfiguration_endpoint_string, + selects={SettingSelector(key_filter="*", label_filter="prod")}, + keyvault_secret_url=appconfiguration_keyvault_secret_url, + keyvault_secret_url2=appconfiguration_keyvault_secret_url2, + on_refresh_success=mock_callback, + refresh_interval=999999, + secret_refresh_interval=1 + ) + + # Verify initial state + assert client["secret"] == "Very secret value" + assert mock_callback.call_count == 0 + + # Mock the refresh method to track calls + with patch.object(client, 'refresh') as mock_refresh: + # Wait for the secret refresh interval to pass + await asyncio.sleep(2) + + await client.refresh() + + # Verify refresh was called + assert mock_refresh.call_count >= 1 + + # Wait again to ensure multiple refreshes + await asyncio.sleep(2) + await client.refresh() + + # Should have been called at least twice now + assert mock_refresh.call_count >= 2 + + @recorded_by_proxy + @app_config_aad_decorator_async + async def test_secret_refresh_with_updated_values(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): + """Test that secrets are refreshed with updated values.""" + mock_callback = Mock() + + # Create client with the mock secret resolver + client = await self.create_aad_client( + appconfiguration_endpoint_string, + selects={SettingSelector(key_filter="*", label_filter="prod")}, + keyvault_secret_url=appconfiguration_keyvault_secret_url, + keyvault_secret_url2=appconfiguration_keyvault_secret_url2, + on_refresh_success=mock_callback, + refresh_interval=999999, + secret_refresh_interval=1 # Using a short interval for testing + ) + + # Add a key vault reference to the client (this will use mock resolver) + appconfig_client = await self.create_aad_sdk_client(appconfiguration_endpoint_string) + + # Get and modify a key vault reference setting + kv_setting = await appconfig_client.get_configuration_setting(key="secret", label="prod") + assert kv_setting is not None + + # Verify initial value from mock resolver + assert client["secret"] == "Very secret value" + assert kv_setting is not None + assert isinstance(kv_setting, SecretReferenceConfigurationSetting) + # Update the secret_id (which is the value for SecretReferenceConfigurationSetting) + kv_setting.secret_id = appconfiguration_keyvault_secret_url2 + await appconfig_client.set_configuration_setting(kv_setting) + + # Wait for the secret refresh interval to pass + await asyncio.sleep(2) + + # Access the value again to trigger refresh + await client.refresh() + + # Verify the value was updated + breakpoint() + assert client["secret"] == "Very secret value2" + assert mock_callback.call_count >= 1 + + @recorded_by_proxy + @app_config_aad_decorator_async + async def test_no_secret_refresh_without_timer(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): + """Test that secrets are not refreshed if secret_refresh_interval is not set.""" + mock_callback = Mock() + + # Create client without specifying secret_refresh_interval + client = await self.create_aad_client( + appconfiguration_endpoint_string, + selects={SettingSelector(key_filter="*", label_filter="prod")}, + keyvault_secret_url=appconfiguration_keyvault_secret_url, + keyvault_secret_url2=appconfiguration_keyvault_secret_url2, + on_refresh_success=mock_callback, + refresh_interval=999999, + ) + + # Verify initial state + assert client["secret"] == "Very secret value" + + # Mock the refresh method to track calls + with patch('time.time') as mock_time: + # Make time.time() return increasing values to simulate passage of time + mock_time.side_effect = [time.time(), time.time() + 100] + + # Access the key vault reference - this shouldn't trigger an auto-refresh since + # we didn't set a secret_refresh_interval + await client.refresh() + + # Access it again to verify no auto-refresh due to secrets timer + await client.refresh() + + # The mock_time should have been called twice (for our side_effect setup) + # but there should be no automatic refresh caused by the secret timer + assert mock_time.call_count == 2 + + @recorded_by_proxy + @app_config_aad_decorator_async + async def test_secret_refresh_timer_triggers_refresh(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): + """Test that the secret refresh timer triggers a refresh after the specified interval.""" + mock_callback = Mock() + + # Create client with key vault reference and separate refresh intervals + client = await self.create_aad_client( + appconfiguration_endpoint_string, + selects={SettingSelector(key_filter="*", label_filter="prod")}, + keyvault_secret_url=appconfiguration_keyvault_secret_url, + keyvault_secret_url2=appconfiguration_keyvault_secret_url2, + on_refresh_success=mock_callback, + refresh_interval=999999, + secret_refresh_interval=5 # Secret refresh interval is short + ) + + # Now patch the refresh method and _secret_refresh_timer to control behavior + with patch.object(client, 'refresh') as mock_refresh: + # Now patch the _secret_refresh_timer to control its behavior + with patch.object(client, '_secret_refresh_timer') as mock_timer: + # Make needs_refresh() return True to simulate timer expiration + mock_timer.needs_refresh.return_value = True + + # Access a key vault reference which should trigger refresh due to timer + await client.refresh() + + # Verify refresh was called + assert mock_refresh.call_count > 0 + + @recorded_by_proxy + @app_config_aad_decorator_async + async def test_secret_refresh_interval_parameter(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): + """Test that secret_refresh_interval parameter is correctly passed and used.""" + mock_callback = Mock() + + # Create client with specific secret_refresh_interval + client = await self.create_aad_client( + appconfiguration_endpoint_string, + selects={SettingSelector(key_filter="*", label_filter="prod")}, + keyvault_secret_url=appconfiguration_keyvault_secret_url, + keyvault_secret_url2=appconfiguration_keyvault_secret_url2, + on_refresh_success=mock_callback, + refresh_interval=999999, + secret_refresh_interval=42 # Use a specific value we can check for + ) + + # Verify the secret refresh timer exists + assert client._secret_refresh_timer is not None + + # We can only verify that it exists, but can't directly access the internal refresh_interval + # as it's a protected attribute + + # Check with no refresh interval to ensure it's properly handled + client2 = await self.create_aad_client( + appconfiguration_endpoint_string, + selects={SettingSelector(key_filter="*", label_filter="prod")}, + keyvault_secret_url=appconfiguration_keyvault_secret_url, + keyvault_secret_url2=appconfiguration_keyvault_secret_url2, + on_refresh_success=mock_callback + # No secret_refresh_interval specified + ) + + # Verify timer is created only when secret_refresh_interval is provided + assert client._secret_refresh_timer is not None + assert client2._secret_refresh_timer is None From 5f548376fa91051424302aa1f84de4d925d2c5a6 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 2 Jul 2025 10:20:19 -0700 Subject: [PATCH 05/43] Fixed Async Tests --- .../_azureappconfigurationproviderasync.py | 5 +++- .../tests/asynctestcase.py | 2 +- .../tests/test_async_provider.py | 3 ++- .../tests/test_async_provider_aad.py | 3 ++- .../test_async_provider_feature_management.py | 3 ++- .../tests/test_async_provider_refresh.py | 12 ++++++--- .../tests/test_async_secret_refresh.py | 26 ++++++++++++++----- 7 files changed, 40 insertions(+), 14 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py index 844b70c883a3..63ddc566de9d 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -6,6 +6,7 @@ import json import datetime import logging +import inspect from typing import ( Any, Awaitable, @@ -392,8 +393,10 @@ async def _common_refresh( return raise exception - if self._on_refresh_success: + if self._on_refresh_success and inspect.iscoroutinefunction(self._on_refresh_success): await self._on_refresh_success() + elif self._on_refresh_success: + self._on_refresh_success() async def _refresh_configuration_settings(self, force: bool = False, **kwargs: Any) -> None: diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py index aa0a600c22b2..05b3159a8489 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py @@ -150,7 +150,7 @@ def create_sdk_client(appconfiguration_connection_string): appconfiguration_connection_string, user_agent="SDK/Integration" ) - async def create_aad_sdk_client(self, appconfiguration_endpoint_string): + def create_aad_sdk_client(self, appconfiguration_endpoint_string): cred = self.get_credential(AzureAppConfigurationClient, is_async=True) return AzureAppConfigurationClient(appconfiguration_endpoint_string, cred, user_agent="SDK/Integration") diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider.py index 8b30ebca4479..d930e3f39d58 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider.py @@ -7,7 +7,8 @@ from azure.appconfiguration.provider.aio import AzureAppConfigurationProvider from devtools_testutils.aio import recorded_by_proxy_async from async_preparers import app_config_decorator_async -from asynctestcase import AppConfigTestCase, has_feature_flag +from testcase import has_feature_flag +from asynctestcase import AppConfigTestCase from test_constants import FEATURE_MANAGEMENT_KEY from unittest.mock import MagicMock, patch import asyncio diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_aad.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_aad.py index e49e880699d9..6b27a79cc498 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_aad.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_aad.py @@ -6,7 +6,8 @@ from azure.appconfiguration.provider import SettingSelector, AzureAppConfigurationKeyVaultOptions from devtools_testutils.aio import recorded_by_proxy_async from async_preparers import app_config_decorator_async -from asynctestcase import AppConfigTestCase, has_feature_flag +from testcase import has_feature_flag +from asynctestcase import AppConfigTestCase from test_constants import FEATURE_MANAGEMENT_KEY diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_feature_management.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_feature_management.py index 931b05593c43..87d53f10370a 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_feature_management.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_feature_management.py @@ -8,7 +8,8 @@ from azure.appconfiguration.aio import AzureAppConfigurationClient from devtools_testutils.aio import recorded_by_proxy_async from async_preparers import app_config_decorator_async -from asynctestcase import AppConfigTestCase, setup_configs, has_feature_flag +from testcase import has_feature_flag +from asynctestcase import AppConfigTestCase, setup_configs from test_constants import FEATURE_MANAGEMENT_KEY diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_refresh.py index 797373421839..54583c4ee6de 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_refresh.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_refresh.py @@ -11,8 +11,10 @@ from azure.appconfiguration.provider import WatchKey from devtools_testutils.aio import recorded_by_proxy_async from async_preparers import app_config_decorator_async -from asynctestcase import AppConfigTestCase, has_feature_flag +from testcase import has_feature_flag +from asynctestcase import AppConfigTestCase from test_constants import FEATURE_MANAGEMENT_KEY +from unittest.mock import Mock try: # Python 3.7 does not support AsyncMock @@ -25,7 +27,9 @@ class TestAppConfigurationProvider(AppConfigTestCase, unittest.TestCase): @pytest.mark.skipif(sys.version_info < (3, 8), reason="Python 3.7 does not support AsyncMock") @pytest.mark.asyncio async def test_refresh(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url): - mock_callback = AsyncMock() + async def async_callback(): + pass + mock_callback = Mock(side_effect=async_callback) async with await self.create_aad_client( appconfiguration_endpoint_string, keyvault_secret_url=appconfiguration_keyvault_secret_url, @@ -94,7 +98,9 @@ async def test_refresh(self, appconfiguration_endpoint_string, appconfiguration_ @pytest.mark.skipif(sys.version_info < (3, 8), reason="Python 3.7 does not support AsyncMock") @pytest.mark.asyncio async def test_empty_refresh(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url): - mock_callback = AsyncMock() + async def async_callback(): + pass + mock_callback = Mock(side_effect=async_callback) async with await self.create_aad_client( appconfiguration_endpoint_string, keyvault_secret_url=appconfiguration_keyvault_secret_url, diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py index 47fa14e4b04b..9b22dabce1cb 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py @@ -19,7 +19,10 @@ class TestAsyncSecretRefresh(AppConfigTestCase, unittest.TestCase): @app_config_aad_decorator_async async def test_secret_refresh_timer(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): """Test that secrets are refreshed based on the secret_refresh_interval.""" - mock_callback = Mock() + # Create an async mock callback + async def async_callback(): + pass + mock_callback = Mock(side_effect=async_callback) # Create client with key vault reference and secret refresh interval client = await self.create_aad_client( @@ -57,7 +60,10 @@ async def test_secret_refresh_timer(self, appconfiguration_endpoint_string, appc @app_config_aad_decorator_async async def test_secret_refresh_with_updated_values(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): """Test that secrets are refreshed with updated values.""" - mock_callback = Mock() + # Create an async mock callback + async def async_callback(): + pass + mock_callback = Mock(side_effect=async_callback) # Create client with the mock secret resolver client = await self.create_aad_client( @@ -92,7 +98,6 @@ async def test_secret_refresh_with_updated_values(self, appconfiguration_endpoin await client.refresh() # Verify the value was updated - breakpoint() assert client["secret"] == "Very secret value2" assert mock_callback.call_count >= 1 @@ -100,7 +105,10 @@ async def test_secret_refresh_with_updated_values(self, appconfiguration_endpoin @app_config_aad_decorator_async async def test_no_secret_refresh_without_timer(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): """Test that secrets are not refreshed if secret_refresh_interval is not set.""" - mock_callback = Mock() + # Create an async mock callback + async def async_callback(): + pass + mock_callback = Mock(side_effect=async_callback) # Create client without specifying secret_refresh_interval client = await self.create_aad_client( @@ -135,7 +143,10 @@ async def test_no_secret_refresh_without_timer(self, appconfiguration_endpoint_s @app_config_aad_decorator_async async def test_secret_refresh_timer_triggers_refresh(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): """Test that the secret refresh timer triggers a refresh after the specified interval.""" - mock_callback = Mock() + # Create an async mock callback + async def async_callback(): + pass + mock_callback = Mock(side_effect=async_callback) # Create client with key vault reference and separate refresh intervals client = await self.create_aad_client( @@ -165,7 +176,10 @@ async def test_secret_refresh_timer_triggers_refresh(self, appconfiguration_endp @app_config_aad_decorator_async async def test_secret_refresh_interval_parameter(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): """Test that secret_refresh_interval parameter is correctly passed and used.""" - mock_callback = Mock() + # Create an async mock callback + async def async_callback(): + pass + mock_callback = Mock(side_effect=async_callback) # Create client with specific secret_refresh_interval client = await self.create_aad_client( From d5e8b875ad4ed453320c58f22a42bb558084a365 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 2 Jul 2025 15:14:26 -0700 Subject: [PATCH 06/43] Updated tests and change log --- .../CHANGELOG.md | 5 + .../assets.json | 2 +- .../_azureappconfigurationprovider.py | 4 +- .../_azureappconfigurationproviderasync.py | 12 +- .../tests/test_async_provider_refresh.py | 5 +- .../tests/test_async_secret_refresh.py | 134 +++++++++++------- .../tests/test_secret_refresh.py | 119 ++++++++++------ .../tests/testcase.py | 10 +- 8 files changed, 179 insertions(+), 112 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md b/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md index 81e2651c396a..b756452c90b4 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md +++ b/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md @@ -4,10 +4,15 @@ ### Features Added +* Added support for forced refresh of configurations when using Key Vault references. Adds `secret_refresh_interval` to the `AzureAppConfigurationProvider.load` method. This allows the provider to refresh Key Vault secrets at a specified interval. Is set to 60 seconds by default, and can only be set if using Key Vault references. +* Added support for async `on_refresh_success`. + ### Breaking Changes ### Bugs Fixed +* Fixed a bug where feature flags were using the configuration refresh timer instead of the feature flag refresh timer. + ### Other Changes ## 2.1.0 (2025-04-28) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json index 3772f98ebe7e..788935cfef9c 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json +++ b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/appconfiguration/azure-appconfiguration-provider", - "Tag": "python/appconfiguration/azure-appconfiguration-provider_6f35ca6dc7" + "Tag": "python/appconfiguration/azure-appconfiguration-provider_a1d88c0647" } diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py index 7d425bf9dfad..321261df7e58 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -468,9 +468,9 @@ def refresh(self, **kwargs) -> None: # pylint: disable=too-many-statements logger.debug("Refresh called but refresh already in progress.") return try: - if (self._secret_refresh_timer and self._secret_refresh_timer.needs_refresh()): + if self._secret_refresh_timer and self._secret_refresh_timer.needs_refresh(): self._refresh_configuration_settings(force=True, **kwargs) - elif(self._refresh_timer and self._refresh_timer.needs_refresh()): + elif self._refresh_timer and self._refresh_timer.needs_refresh(): self._refresh_configuration_settings(**kwargs) if self._feature_flag_refresh_enabled and ( self._feature_flag_refresh_timer and self._feature_flag_refresh_timer.needs_refresh() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py index 63ddc566de9d..05f273e2b008 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -410,9 +410,11 @@ async def refresh_operation(client, headers, **inner_kwargs): ) else: # Force a refresh to make sure secrets are up to date - configuration_settings = (await client.load_configuration_settings( - self._selects, self._refresh_on, headers=headers, **inner_kwargs - ))[0] + configuration_settings = ( + await client.load_configuration_settings( + self._selects, self._refresh_on, headers=headers, **inner_kwargs + ) + )[0] need_refresh = True configuration_settings_processed: Dict[str, Any] = {} @@ -486,9 +488,9 @@ async def refresh(self, **kwargs) -> None: # pylint: disable=too-many-statement logger.debug("Refresh called but refresh already in progress.") return try: - if (self._secret_refresh_timer and self._secret_refresh_timer.needs_refresh()): + if self._secret_refresh_timer and self._secret_refresh_timer.needs_refresh(): await self._refresh_configuration_settings(force=True, **kwargs) - elif (self._refresh_timer and self._refresh_timer.needs_refresh()): + elif self._refresh_timer and self._refresh_timer.needs_refresh(): await self._refresh_configuration_settings(**kwargs) if self._feature_flag_refresh_enabled and ( self._feature_flag_refresh_timer and self._feature_flag_refresh_timer.needs_refresh() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_refresh.py index 54583c4ee6de..4a1ce9374630 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_refresh.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_refresh.py @@ -27,9 +27,7 @@ class TestAppConfigurationProvider(AppConfigTestCase, unittest.TestCase): @pytest.mark.skipif(sys.version_info < (3, 8), reason="Python 3.7 does not support AsyncMock") @pytest.mark.asyncio async def test_refresh(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url): - async def async_callback(): - pass - mock_callback = Mock(side_effect=async_callback) + mock_callback = Mock() async with await self.create_aad_client( appconfiguration_endpoint_string, keyvault_secret_url=appconfiguration_keyvault_secret_url, @@ -100,6 +98,7 @@ async def async_callback(): async def test_empty_refresh(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url): async def async_callback(): pass + mock_callback = Mock(side_effect=async_callback) async with await self.create_aad_client( appconfiguration_endpoint_string, diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py index 9b22dabce1cb..bc4206efaffc 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py @@ -17,13 +17,20 @@ class TestAsyncSecretRefresh(AppConfigTestCase, unittest.TestCase): @recorded_by_proxy @app_config_aad_decorator_async - async def test_secret_refresh_timer(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): + async def test_secret_refresh_timer( + self, + appconfiguration_endpoint_string, + appconfiguration_keyvault_secret_url, + appconfiguration_keyvault_secret_url2, + ): """Test that secrets are refreshed based on the secret_refresh_interval.""" + # Create an async mock callback async def async_callback(): pass + mock_callback = Mock(side_effect=async_callback) - + # Create client with key vault reference and secret refresh interval client = await self.create_aad_client( appconfiguration_endpoint_string, @@ -32,39 +39,41 @@ async def async_callback(): keyvault_secret_url2=appconfiguration_keyvault_secret_url2, on_refresh_success=mock_callback, refresh_interval=999999, - secret_refresh_interval=1 + secret_refresh_interval=1, ) - + # Verify initial state assert client["secret"] == "Very secret value" assert mock_callback.call_count == 0 - + # Mock the refresh method to track calls - with patch.object(client, 'refresh') as mock_refresh: + with patch.object(client, "refresh") as mock_refresh: # Wait for the secret refresh interval to pass await asyncio.sleep(2) - + await client.refresh() - + # Verify refresh was called assert mock_refresh.call_count >= 1 - + # Wait again to ensure multiple refreshes await asyncio.sleep(2) await client.refresh() - + # Should have been called at least twice now assert mock_refresh.call_count >= 2 - + @recorded_by_proxy @app_config_aad_decorator_async - async def test_secret_refresh_with_updated_values(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): + async def test_secret_refresh_with_updated_values( + self, + appconfiguration_endpoint_string, + appconfiguration_keyvault_secret_url, + appconfiguration_keyvault_secret_url2, + ): """Test that secrets are refreshed with updated values.""" - # Create an async mock callback - async def async_callback(): - pass - mock_callback = Mock(side_effect=async_callback) - + mock_callback = Mock() + # Create client with the mock secret resolver client = await self.create_aad_client( appconfiguration_endpoint_string, @@ -73,16 +82,16 @@ async def async_callback(): keyvault_secret_url2=appconfiguration_keyvault_secret_url2, on_refresh_success=mock_callback, refresh_interval=999999, - secret_refresh_interval=1 # Using a short interval for testing + secret_refresh_interval=1, # Using a short interval for testing ) - + # Add a key vault reference to the client (this will use mock resolver) - appconfig_client = await self.create_aad_sdk_client(appconfiguration_endpoint_string) - + appconfig_client = self.create_aad_sdk_client(appconfiguration_endpoint_string) + # Get and modify a key vault reference setting kv_setting = await appconfig_client.get_configuration_setting(key="secret", label="prod") assert kv_setting is not None - + # Verify initial value from mock resolver assert client["secret"] == "Very secret value" assert kv_setting is not None @@ -90,26 +99,33 @@ async def async_callback(): # Update the secret_id (which is the value for SecretReferenceConfigurationSetting) kv_setting.secret_id = appconfiguration_keyvault_secret_url2 await appconfig_client.set_configuration_setting(kv_setting) - + # Wait for the secret refresh interval to pass await asyncio.sleep(2) - + # Access the value again to trigger refresh await client.refresh() - + # Verify the value was updated assert client["secret"] == "Very secret value2" assert mock_callback.call_count >= 1 - + @recorded_by_proxy @app_config_aad_decorator_async - async def test_no_secret_refresh_without_timer(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): + async def test_no_secret_refresh_without_timer( + self, + appconfiguration_endpoint_string, + appconfiguration_keyvault_secret_url, + appconfiguration_keyvault_secret_url2, + ): """Test that secrets are not refreshed if secret_refresh_interval is not set.""" + # Create an async mock callback async def async_callback(): pass + mock_callback = Mock(side_effect=async_callback) - + # Create client without specifying secret_refresh_interval client = await self.create_aad_client( appconfiguration_endpoint_string, @@ -119,35 +135,42 @@ async def async_callback(): on_refresh_success=mock_callback, refresh_interval=999999, ) - + # Verify initial state assert client["secret"] == "Very secret value" - + # Mock the refresh method to track calls - with patch('time.time') as mock_time: + with patch("time.time") as mock_time: # Make time.time() return increasing values to simulate passage of time mock_time.side_effect = [time.time(), time.time() + 100] - + # Access the key vault reference - this shouldn't trigger an auto-refresh since # we didn't set a secret_refresh_interval await client.refresh() - + # Access it again to verify no auto-refresh due to secrets timer await client.refresh() - + # The mock_time should have been called twice (for our side_effect setup) # but there should be no automatic refresh caused by the secret timer assert mock_time.call_count == 2 - + @recorded_by_proxy @app_config_aad_decorator_async - async def test_secret_refresh_timer_triggers_refresh(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): + async def test_secret_refresh_timer_triggers_refresh( + self, + appconfiguration_endpoint_string, + appconfiguration_keyvault_secret_url, + appconfiguration_keyvault_secret_url2, + ): """Test that the secret refresh timer triggers a refresh after the specified interval.""" + # Create an async mock callback async def async_callback(): pass + mock_callback = Mock(side_effect=async_callback) - + # Create client with key vault reference and separate refresh intervals client = await self.create_aad_client( appconfiguration_endpoint_string, @@ -156,31 +179,38 @@ async def async_callback(): keyvault_secret_url2=appconfiguration_keyvault_secret_url2, on_refresh_success=mock_callback, refresh_interval=999999, - secret_refresh_interval=5 # Secret refresh interval is short + secret_refresh_interval=5, # Secret refresh interval is short ) - + # Now patch the refresh method and _secret_refresh_timer to control behavior - with patch.object(client, 'refresh') as mock_refresh: + with patch.object(client, "refresh") as mock_refresh: # Now patch the _secret_refresh_timer to control its behavior - with patch.object(client, '_secret_refresh_timer') as mock_timer: + with patch.object(client, "_secret_refresh_timer") as mock_timer: # Make needs_refresh() return True to simulate timer expiration mock_timer.needs_refresh.return_value = True - + # Access a key vault reference which should trigger refresh due to timer await client.refresh() - + # Verify refresh was called assert mock_refresh.call_count > 0 - + @recorded_by_proxy @app_config_aad_decorator_async - async def test_secret_refresh_interval_parameter(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): + async def test_secret_refresh_interval_parameter( + self, + appconfiguration_endpoint_string, + appconfiguration_keyvault_secret_url, + appconfiguration_keyvault_secret_url2, + ): """Test that secret_refresh_interval parameter is correctly passed and used.""" + # Create an async mock callback async def async_callback(): pass + mock_callback = Mock(side_effect=async_callback) - + # Create client with specific secret_refresh_interval client = await self.create_aad_client( appconfiguration_endpoint_string, @@ -189,25 +219,25 @@ async def async_callback(): keyvault_secret_url2=appconfiguration_keyvault_secret_url2, on_refresh_success=mock_callback, refresh_interval=999999, - secret_refresh_interval=42 # Use a specific value we can check for + secret_refresh_interval=42, # Use a specific value we can check for ) - + # Verify the secret refresh timer exists assert client._secret_refresh_timer is not None - + # We can only verify that it exists, but can't directly access the internal refresh_interval # as it's a protected attribute - + # Check with no refresh interval to ensure it's properly handled client2 = await self.create_aad_client( appconfiguration_endpoint_string, selects={SettingSelector(key_filter="*", label_filter="prod")}, keyvault_secret_url=appconfiguration_keyvault_secret_url, keyvault_secret_url2=appconfiguration_keyvault_secret_url2, - on_refresh_success=mock_callback + on_refresh_success=mock_callback, # No secret_refresh_interval specified ) - + # Verify timer is created only when secret_refresh_interval is provided assert client._secret_refresh_timer is not None assert client2._secret_refresh_timer is None diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py index 54edd46bdd2e..6793234b8ee8 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py @@ -16,10 +16,15 @@ class TestSecretRefresh(AppConfigTestCase, unittest.TestCase): @recorded_by_proxy @app_config_decorator_aad - def test_secret_refresh_timer(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): + def test_secret_refresh_timer( + self, + appconfiguration_endpoint_string, + appconfiguration_keyvault_secret_url, + appconfiguration_keyvault_secret_url2, + ): """Test that secrets are refreshed based on the secret_refresh_interval.""" mock_callback = Mock() - + # Create client with key vault reference and secret refresh interval client = self.create_aad_client( appconfiguration_endpoint_string, @@ -28,36 +33,41 @@ def test_secret_refresh_timer(self, appconfiguration_endpoint_string, appconfigu keyvault_secret_url2=appconfiguration_keyvault_secret_url2, on_refresh_success=mock_callback, refresh_interval=999999, - secret_refresh_interval=1 + secret_refresh_interval=1, ) - + # Verify initial state assert client["secret"] == "Very secret value" assert mock_callback.call_count == 0 - + # Mock the refresh method to track calls - with patch.object(client, 'refresh') as mock_refresh: + with patch.object(client, "refresh") as mock_refresh: # Wait for the secret refresh interval to pass time.sleep(2) - + client.refresh() - + # Verify refresh was called assert mock_refresh.call_count >= 1 - + # Wait again to ensure multiple refreshes time.sleep(2) client.refresh() - + # Should have been called at least twice now assert mock_refresh.call_count >= 2 - + @recorded_by_proxy @app_config_decorator_aad - def test_secret_refresh_with_updated_values(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): + def test_secret_refresh_with_updated_values( + self, + appconfiguration_endpoint_string, + appconfiguration_keyvault_secret_url, + appconfiguration_keyvault_secret_url2, + ): """Test that secrets are refreshed with updated values.""" mock_callback = Mock() - + # Create client with the mock secret resolver client = self.create_aad_client( appconfiguration_endpoint_string, @@ -66,16 +76,16 @@ def test_secret_refresh_with_updated_values(self, appconfiguration_endpoint_stri keyvault_secret_url2=appconfiguration_keyvault_secret_url2, on_refresh_success=mock_callback, refresh_interval=999999, - secret_refresh_interval=1 # Using a short interval for testing + secret_refresh_interval=1, # Using a short interval for testing ) - + # Add a key vault reference to the client (this will use mock resolver) appconfig_client = self.create_aad_sdk_client(appconfiguration_endpoint_string) - + # Get and modify a key vault reference setting kv_setting = appconfig_client.get_configuration_setting(key="secret", label="prod") assert kv_setting is not None - + # Verify initial value from mock resolver assert client["secret"] == "Very secret value" assert kv_setting is not None @@ -83,23 +93,28 @@ def test_secret_refresh_with_updated_values(self, appconfiguration_endpoint_stri # Update the secret_id (which is the value for SecretReferenceConfigurationSetting) kv_setting.secret_id = appconfiguration_keyvault_secret_url2 appconfig_client.set_configuration_setting(kv_setting) - + # Wait for the secret refresh interval to pass time.sleep(2) - + # Access the value again to trigger refresh client.refresh() - + # Verify the value was updated assert client["secret"] == "Very secret value2" assert mock_callback.call_count >= 1 - + @recorded_by_proxy @app_config_decorator_aad - def test_no_secret_refresh_without_timer(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): + def test_no_secret_refresh_without_timer( + self, + appconfiguration_endpoint_string, + appconfiguration_keyvault_secret_url, + appconfiguration_keyvault_secret_url2, + ): """Test that secrets are not refreshed if secret_refresh_interval is not set.""" mock_callback = Mock() - + # Create client without specifying secret_refresh_interval client = self.create_aad_client( appconfiguration_endpoint_string, @@ -109,32 +124,37 @@ def test_no_secret_refresh_without_timer(self, appconfiguration_endpoint_string, on_refresh_success=mock_callback, refresh_interval=999999, ) - + # Verify initial state assert client["secret"] == "Very secret value" - + # Mock the refresh method to track calls - with patch('time.time') as mock_time: + with patch("time.time") as mock_time: # Make time.time() return increasing values to simulate passage of time mock_time.side_effect = [time.time(), time.time() + 100] - + # Access the key vault reference - this shouldn't trigger an auto-refresh since # we didn't set a secret_refresh_interval client.refresh() - + # Access it again to verify no auto-refresh due to secrets timer client.refresh() - + # The mock_time should have been called twice (for our side_effect setup) # but there should be no automatic refresh caused by the secret timer assert mock_time.call_count == 2 - + @recorded_by_proxy @app_config_decorator_aad - def test_secret_refresh_timer_triggers_refresh(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): + def test_secret_refresh_timer_triggers_refresh( + self, + appconfiguration_endpoint_string, + appconfiguration_keyvault_secret_url, + appconfiguration_keyvault_secret_url2, + ): """Test that the secret refresh timer triggers a refresh after the specified interval.""" mock_callback = Mock() - + # Create client with key vault reference and separate refresh intervals client = self.create_aad_client( appconfiguration_endpoint_string, @@ -143,28 +163,33 @@ def test_secret_refresh_timer_triggers_refresh(self, appconfiguration_endpoint_s keyvault_secret_url2=appconfiguration_keyvault_secret_url2, on_refresh_success=mock_callback, refresh_interval=999999, - secret_refresh_interval=5 # Secret refresh interval is short + secret_refresh_interval=5, # Secret refresh interval is short ) - + # Now patch the refresh method and _secret_refresh_timer to control behavior - with patch.object(client, 'refresh') as mock_refresh: + with patch.object(client, "refresh") as mock_refresh: # Now patch the _secret_refresh_timer to control its behavior - with patch.object(client, '_secret_refresh_timer') as mock_timer: + with patch.object(client, "_secret_refresh_timer") as mock_timer: # Make needs_refresh() return True to simulate timer expiration mock_timer.needs_refresh.return_value = True - + # Access a key vault reference which should trigger refresh due to timer client.refresh() - + # Verify refresh was called assert mock_refresh.call_count > 0 - + @recorded_by_proxy @app_config_decorator_aad - def test_secret_refresh_interval_parameter(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, appconfiguration_keyvault_secret_url2): + def test_secret_refresh_interval_parameter( + self, + appconfiguration_endpoint_string, + appconfiguration_keyvault_secret_url, + appconfiguration_keyvault_secret_url2, + ): """Test that secret_refresh_interval parameter is correctly passed and used.""" mock_callback = Mock() - + # Create client with specific secret_refresh_interval client = self.create_aad_client( appconfiguration_endpoint_string, @@ -173,25 +198,25 @@ def test_secret_refresh_interval_parameter(self, appconfiguration_endpoint_strin keyvault_secret_url2=appconfiguration_keyvault_secret_url2, on_refresh_success=mock_callback, refresh_interval=999999, - secret_refresh_interval=42 # Use a specific value we can check for + secret_refresh_interval=42, # Use a specific value we can check for ) - + # Verify the secret refresh timer exists assert client._secret_refresh_timer is not None - + # We can only verify that it exists, but can't directly access the internal refresh_interval # as it's a protected attribute - + # Check with no refresh interval to ensure it's properly handled client2 = self.create_aad_client( appconfiguration_endpoint_string, selects={SettingSelector(key_filter="*", label_filter="prod")}, keyvault_secret_url=appconfiguration_keyvault_secret_url, keyvault_secret_url2=appconfiguration_keyvault_secret_url2, - on_refresh_success=mock_callback + on_refresh_success=mock_callback, # No secret_refresh_interval specified ) - + # Verify timer is created only when secret_refresh_interval is provided assert client._secret_refresh_timer is not None assert client2._secret_refresh_timer is None diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py index 14b7ef263c5a..352fa2a0cae7 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py @@ -5,7 +5,12 @@ # license information. # -------------------------------------------------------------------------- from devtools_testutils import AzureRecordedTestCase -from azure.appconfiguration import AzureAppConfigurationClient, ConfigurationSetting, FeatureFlagConfigurationSetting, SecretReferenceConfigurationSetting +from azure.appconfiguration import ( + AzureAppConfigurationClient, + ConfigurationSetting, + FeatureFlagConfigurationSetting, + SecretReferenceConfigurationSetting, +) from azure.appconfiguration.provider import SettingSelector, load, AzureAppConfigurationKeyVaultOptions from test_constants import FEATURE_MANAGEMENT_KEY, FEATURE_FLAG_KEY @@ -202,12 +207,13 @@ def create_config_setting(key, label, value, content_type="text/plain"): content_type=content_type, ) + def create_secret_config_setting(key, label, value): return SecretReferenceConfigurationSetting( key=key, label=label, secret_id=value, - ) + ) def create_feature_flag_config_setting(key, label, enabled): From 3f838d77b9498a415de562287fc198c048a11f1a Mon Sep 17 00:00:00 2001 From: Matthew Metcalf Date: Wed, 2 Jul 2025 15:28:27 -0700 Subject: [PATCH 07/43] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../tests/test_async_secret_refresh.py | 2 +- .../tests/test_secret_refresh.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py index bc4206efaffc..9ffe05b3f3be 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py @@ -107,7 +107,7 @@ async def test_secret_refresh_with_updated_values( await client.refresh() # Verify the value was updated - assert client["secret"] == "Very secret value2" + assert client["secret"] == "Very secret value 2" assert mock_callback.call_count >= 1 @recorded_by_proxy diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py index 6793234b8ee8..6b0a55f6ff90 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py @@ -101,7 +101,7 @@ def test_secret_refresh_with_updated_values( client.refresh() # Verify the value was updated - assert client["secret"] == "Very secret value2" + assert client["secret"] == "Very secret value 2" assert mock_callback.call_count >= 1 @recorded_by_proxy From 251b82516102f9814a09f31513abdd3b0f488171 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 2 Jul 2025 16:12:58 -0700 Subject: [PATCH 08/43] Fixing merge issue --- .../azure-appconfiguration-provider/assets.json | 2 +- .../azure-appconfiguration-provider/tests/asynctestcase.py | 4 ++-- .../azure-appconfiguration-provider/tests/testcase.py | 7 ++++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json index 788935cfef9c..3c3611d3a523 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json +++ b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/appconfiguration/azure-appconfiguration-provider", - "Tag": "python/appconfiguration/azure-appconfiguration-provider_a1d88c0647" + "Tag": "python/appconfiguration/azure-appconfiguration-provider_e16b70d17c" } diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py index 1b1d8b566459..e51ca500a856 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py @@ -46,7 +46,7 @@ async def create_aad_client( "keyvault_credential": keyvault_cred, "on_refresh_success": on_refresh_success, "feature_flag_enabled": feature_flag_enabled, - "feature_flag_selectors"=feature_flag_selectors, + "feature_flag_selectors": feature_flag_selectors, "feature_flag_refresh_enabled": feature_flag_refresh_enabled, } if secret_refresh_interval is not None: @@ -66,7 +66,7 @@ async def create_aad_client( "key_vault_options": key_vault_options, "on_refresh_success": on_refresh_success, "feature_flag_enabled": feature_flag_enabled, - "feature_flag_selectors"=feature_flag_selectors, + "feature_flag_selectors": feature_flag_selectors, "feature_flag_refresh_enabled": feature_flag_refresh_enabled, } if secret_refresh_interval is not None: diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py index a51b025f85ba..fdeacf64ab2c 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py @@ -51,7 +51,7 @@ def create_aad_client( "keyvault_credential": keyvault_cred, "on_refresh_success": on_refresh_success, "feature_flag_enabled": feature_flag_enabled, - "feature_flag_selectors"=feature_flag_selectors, + "feature_flag_selectors": feature_flag_selectors, "feature_flag_refresh_enabled": feature_flag_refresh_enabled, } if secret_refresh_interval is not None: @@ -71,7 +71,7 @@ def create_aad_client( "key_vault_options": key_vault_options, "on_refresh_success": on_refresh_success, "feature_flag_enabled": feature_flag_enabled, - "feature_flag_selectors"=feature_flag_selectors, + "feature_flag_selectors": feature_flag_selectors, "feature_flag_refresh_enabled": feature_flag_refresh_enabled, } if secret_refresh_interval is not None: @@ -240,6 +240,7 @@ def get_configs(keyvault_secret_url, keyvault_secret_url2): def create_config_setting(key, label, value, content_type="text/plain", tags=None): return ConfigurationSetting(key=key, label=label, value=value, content_type=content_type, tags=tags) + def create_secret_config_setting(key, label, value): return SecretReferenceConfigurationSetting( key=key, @@ -247,11 +248,11 @@ def create_secret_config_setting(key, label, value): secret_id=value, ) + def create_feature_flag_config_setting(key, label, enabled, tags=None): return FeatureFlagConfigurationSetting(feature_id=key, label=label, enabled=enabled, tags=tags) - def get_feature_flag(client, feature_id): for feature_flag in client[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY]: if feature_flag["id"] == feature_id: From 382dbbaa7090a6565dfd301ded909b0102ae9492 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 2 Jul 2025 16:18:06 -0700 Subject: [PATCH 09/43] Updating comments --- .../provider/_azureappconfigurationprovider.py | 7 ++----- .../provider/aio/_azureappconfigurationproviderasync.py | 8 ++------ 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py index 321261df7e58..edb61be152c8 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -33,6 +33,7 @@ ) from ._azureappconfigurationproviderbase import ( AzureAppConfigurationProviderBase, + _RefreshTimer, update_correlation_context_header, delay_failure, is_json_content_type, @@ -327,7 +328,7 @@ def _common_refresh( self, refresh_operation: Callable, error_log_message: str, - timer, # Type is _RefreshTimer but avoiding import + timer: _RefreshTimer, refresh_condition: bool, **kwargs, ) -> None: @@ -412,7 +413,6 @@ def refresh_operation(client, headers, **inner_kwargs): # If feature flags were already loaded, we need to keep them self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags - # Even if we don't need to refresh, we should reset the timer self._refresh_timer.reset() if self._secret_refresh_timer: self._secret_refresh_timer.reset() @@ -448,7 +448,6 @@ def refresh_operation(client, headers, **inner_kwargs): self._dict[FEATURE_MANAGEMENT_KEY] = {} self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags - # Even if we don't need to refresh, we should reset the timer self._feature_flag_refresh_timer.reset() return True @@ -551,7 +550,6 @@ def _load_all(self, **kwargs): raise exception def _process_configurations(self, configuration_settings: List[ConfigurationSetting]) -> Dict[str, Any]: - # Reset feature flag usage self._uses_ai_configuration = False self._uses_aicc_configuration = False @@ -571,7 +569,6 @@ def _process_key_value(self, config): if isinstance(config, SecretReferenceConfigurationSetting): return _resolve_keyvault_reference(config, self) if is_json_content_type(config.content_type) and not isinstance(config, FeatureFlagConfigurationSetting): - # Feature flags are of type json, but don't treat them as such try: if APP_CONFIG_AI_MIME_PROFILE in config.content_type: self._uses_ai_configuration = True diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py index 05f273e2b008..d527fd345d7b 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -38,6 +38,7 @@ ) from .._azureappconfigurationproviderbase import ( AzureAppConfigurationProviderBase, + _RefreshTimer, update_correlation_context_header, delay_failure, is_json_content_type, @@ -343,7 +344,7 @@ async def _common_refresh( self, refresh_operation: Callable, error_log_message: str, - timer, # Type is _RefreshTimer but avoiding import + timer: _RefreshTimer, refresh_condition: bool, **kwargs, ) -> None: @@ -432,7 +433,6 @@ async def refresh_operation(client, headers, **inner_kwargs): # If feature flags were already loaded, we need to keep them self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags - # Even if we don't need to refresh, we should reset the timer self._refresh_timer.reset() if self._secret_refresh_timer: self._secret_refresh_timer.reset() @@ -468,7 +468,6 @@ async def refresh_operation(client, headers, **inner_kwargs): self._dict[FEATURE_MANAGEMENT_KEY] = {} self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags - # Even if we don't need to refresh, we should reset the timer self._feature_flag_refresh_timer.reset() return True @@ -573,14 +572,12 @@ async def _load_all(self, **kwargs): raise exception async def _process_configurations(self, configuration_settings: List[ConfigurationSetting]) -> Dict[str, Any]: - # Reset feature flag usage self._uses_ai_configuration = False self._uses_aicc_configuration = False configuration_settings_processed = {} for config in configuration_settings: if isinstance(config, FeatureFlagConfigurationSetting): - # Feature flags are not processed like other settings continue key = self._process_key_name(config) value = await self._process_key_value(config) @@ -593,7 +590,6 @@ async def _process_key_value(self, config): if isinstance(config, SecretReferenceConfigurationSetting): return await _resolve_keyvault_reference(config, self) if is_json_content_type(config.content_type) and not isinstance(config, FeatureFlagConfigurationSetting): - # Feature flags are of type json, but don't treat them as such try: if APP_CONFIG_AI_MIME_PROFILE in config.content_type: self._uses_ai_configuration = True From 6d7a8f815ce1542c8208d46b943e7a1086fb329b Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 11 Aug 2025 10:45:27 -0700 Subject: [PATCH 10/43] Updating secret refresh --- .../_azureappconfigurationprovider.py | 98 ++++------ .../_azureappconfigurationproviderbase.py | 14 -- .../provider/_secret_provider.py | 169 +++++++++++++++++ .../provider/aio/_async_secret_provider.py | 178 ++++++++++++++++++ .../_azureappconfigurationproviderasync.py | 112 ++++------- .../tests/test_async_secret_refresh.py | 19 +- .../tests/test_secret_refresh.py | 17 +- 7 files changed, 435 insertions(+), 172 deletions(-) create mode 100644 sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py create mode 100644 sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py index ace3788a08e7..8926ecb3989f 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -23,8 +23,8 @@ SecretReferenceConfigurationSetting, ) from azure.core.exceptions import AzureError, HttpResponseError -from azure.keyvault.secrets import SecretClient, KeyVaultSecretIdentifier from ._models import AzureAppConfigurationKeyVaultOptions, SettingSelector +from ._secret_provider import SecretProvider from ._constants import ( FEATURE_MANAGEMENT_KEY, FEATURE_FLAG_KEY, @@ -246,46 +246,6 @@ def _buildprovider( return AzureAppConfigurationProvider(**kwargs) -def _resolve_keyvault_reference( - config: "SecretReferenceConfigurationSetting", provider: "AzureAppConfigurationProvider" -) -> str: - # pylint:disable=protected-access - if not (provider._keyvault_credential or provider._keyvault_client_configs or provider._secret_resolver): - raise ValueError( - """ - Either a credential to Key Vault, custom Key Vault client, or a secret resolver must be set to resolve Key - Vault references. - """ - ) - - if config.secret_id is None: - raise ValueError("Key Vault reference must have a uri value.") - - keyvault_identifier = KeyVaultSecretIdentifier(config.secret_id) - - vault_url = keyvault_identifier.vault_url + "/" - - # pylint:disable=protected-access - referenced_client = provider._secret_clients.get(vault_url, None) - - vault_config = provider._keyvault_client_configs.get(vault_url, {}) - credential = vault_config.pop("credential", provider._keyvault_credential) - - if referenced_client is None and credential is not None: - referenced_client = SecretClient(vault_url=vault_url, credential=credential, **vault_config) - provider._secret_clients[vault_url] = referenced_client - - if referenced_client: - secret_value = referenced_client.get_secret(keyvault_identifier.name, version=keyvault_identifier.version).value - if secret_value is not None: - return secret_value - - if provider._secret_resolver: - return provider._secret_resolver(config.secret_id) - - raise ValueError("No Secret Client found for Key Vault reference %s" % (vault_url)) - - class AzureAppConfigurationProvider(AzureAppConfigurationProviderBase): # pylint: disable=too-many-instance-attributes """ Provides a dictionary-like interface to Azure App Configuration settings. Enables loading of sets of configuration @@ -321,7 +281,7 @@ def __init__(self, **kwargs: Any) -> None: load_balancing_enabled=kwargs.pop("load_balancing_enabled", False), **kwargs, ) - self._secret_clients: Dict[str, SecretClient] = {} + self._secret_provider = SecretProvider(**kwargs) self._on_refresh_success: Optional[Callable] = kwargs.pop("on_refresh_success", None) self._on_refresh_error: Optional[Callable[[Exception], None]] = kwargs.pop("on_refresh_error", None) @@ -346,7 +306,6 @@ def _common_refresh( self._replica_client_manager.refresh_clients() self._replica_client_manager.find_active_clients() replica_count = self._replica_client_manager.get_client_count() - 1 - while client := self._replica_client_manager.get_next_active_client(): headers = update_correlation_context_header( kwargs.pop("headers", {}), @@ -354,7 +313,7 @@ def _common_refresh( replica_count, self._feature_flag_enabled, self._feature_filter_usage, - self._uses_key_vault, + self._secret_provider.uses_key_vault, self._uses_load_balancing, is_failover_request, self._uses_ai_configuration, @@ -382,24 +341,33 @@ def _common_refresh( if self._on_refresh_success: self._on_refresh_success() - def _refresh_configuration_settings(self, force: bool = False, **kwargs: Any) -> None: + def _refresh_configuration_settings(self, **kwargs: Any) -> None: def refresh_operation(client, headers, **inner_kwargs): configuration_settings: Optional[List[ConfigurationSetting]] = None need_refresh = False - force = inner_kwargs.pop("force", False) - if not force: + reset_secret_timer = False + + if ( + self._secret_provider.secret_refresh_timer + and self._secret_provider.secret_refresh_timer.needs_refresh() + ): + self._secret_provider.bust_cache() + reset_secret_timer = True + need_refresh = True + + if not need_refresh: need_refresh, self._refresh_on, configuration_settings = client.refresh_configuration_settings( self._selects, self._refresh_on, headers=headers, **inner_kwargs ) else: # Force a refresh to make sure secrets are up to date - configuration_settings = client.load_configuration_settings( + configuration_settings, self._refresh_on = client.load_configuration_settings( self._selects, self._refresh_on, headers=headers, **inner_kwargs - )[0] - need_refresh = True + ) configuration_settings_processed: Dict[str, Any] = {} + if configuration_settings is not None: configuration_settings_processed = self._process_configurations(configuration_settings) @@ -415,8 +383,8 @@ def refresh_operation(client, headers, **inner_kwargs): self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags self._refresh_timer.reset() - if self._secret_refresh_timer: - self._secret_refresh_timer.reset() + if reset_secret_timer and self._secret_provider.secret_refresh_timer: + self._secret_provider.secret_refresh_timer.reset() return True @@ -424,8 +392,7 @@ def refresh_operation(client, headers, **inner_kwargs): refresh_operation=refresh_operation, error_log_message="Failed to refresh configurations from endpoint %s", timer=self._refresh_timer, - refresh_condition=bool(self._refresh_on or force), - force=force, + refresh_condition=bool(self._refresh_on), **kwargs, ) @@ -461,16 +428,18 @@ def refresh_operation(client, headers, **inner_kwargs): ) def refresh(self, **kwargs) -> None: # pylint: disable=too-many-statements - if not self._refresh_on and not self._feature_flag_refresh_enabled and not self._secret_refresh_timer: + if ( + not self._refresh_on + and not self._feature_flag_refresh_enabled + and not self._secret_provider.secret_refresh_timer + ): logger.debug("Refresh called but no refresh enabled.") return if not self._refresh_lock.acquire(blocking=False): # pylint: disable= consider-using-with logger.debug("Refresh called but refresh already in progress.") return try: - if self._secret_refresh_timer and self._secret_refresh_timer.needs_refresh(): - self._refresh_configuration_settings(force=True, **kwargs) - elif self._refresh_timer and self._refresh_timer.needs_refresh(): + if self._refresh_timer and self._refresh_timer.needs_refresh(): self._refresh_configuration_settings(**kwargs) if self._feature_flag_refresh_enabled and ( self._feature_flag_refresh_timer and self._feature_flag_refresh_timer.needs_refresh() @@ -497,7 +466,7 @@ def _load_all(self, **kwargs): replica_count, self._feature_flag_enabled, self._feature_filter_usage, - self._uses_key_vault, + self._secret_provider.uses_key_vault, self._uses_load_balancing, is_failover_request, self._uses_ai_configuration, @@ -568,7 +537,7 @@ def _process_configurations(self, configuration_settings: List[ConfigurationSett def _process_key_value(self, config): if isinstance(config, SecretReferenceConfigurationSetting): - return _resolve_keyvault_reference(config, self) + return self._secret_provider.resolve_keyvault_reference(config) if is_json_content_type(config.content_type) and not isinstance(config, FeatureFlagConfigurationSetting): try: if APP_CONFIG_AI_MIME_PROFILE in config.content_type: @@ -598,17 +567,14 @@ def close(self) -> None: """ Closes the connection to Azure App Configuration. """ - for client in self._secret_clients.values(): - client.close() + self._secret_provider.close() self._replica_client_manager.close() def __enter__(self) -> "AzureAppConfigurationProvider": self._replica_client_manager.__enter__() - for client in self._secret_clients.values(): - client.__enter__() + self._secret_provider.__enter__() return self def __exit__(self, *args) -> None: self._replica_client_manager.__exit__() - for client in self._secret_clients.values(): - client.__exit__() + self._secret_provider.__exit__() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py index 0ff67554514f..2db52a8af12b 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -268,23 +268,9 @@ def __init__(self, **kwargs: Any) -> None: trim_prefixes: List[str] = kwargs.pop("trim_prefixes", []) self._trim_prefixes: List[str] = sorted(trim_prefixes, key=len, reverse=True) - refresh_on: List[Tuple[str, str]] = kwargs.pop("refresh_on", None) or [] self._refresh_on: Mapping[Tuple[str, str], Optional[str]] = {_build_sentinel(s): None for s in refresh_on} self._refresh_timer: _RefreshTimer = _RefreshTimer(**kwargs) - self._keyvault_credential = kwargs.pop("keyvault_credential", None) - self._secret_resolver = kwargs.pop("secret_resolver", None) - self._keyvault_client_configs = kwargs.pop("keyvault_client_configs", {}) - self._uses_key_vault = ( - self._keyvault_credential is not None - or (self._keyvault_client_configs is not None and len(self._keyvault_client_configs) > 0) - or self._secret_resolver is not None - ) - self._secret_refresh_timer: Optional[_RefreshTimer] = ( - _RefreshTimer(refresh_interval=kwargs.pop("secret_refresh_interval", 60)) - if self._uses_key_vault and "secret_refresh_interval" in kwargs - else None - ) self._feature_flag_enabled = kwargs.pop("feature_flag_enabled", False) self._feature_flag_selectors = kwargs.pop("feature_flag_selectors", [SettingSelector(key_filter="*")]) self._refresh_on_feature_flags: Mapping[Tuple[str, str], Optional[str]] = {} diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py new file mode 100644 index 000000000000..a10726034c7e --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py @@ -0,0 +1,169 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- +from typing import Mapping, Union, Any, Iterator, KeysView, ItemsView, ValuesView, TypeVar, overload, Optional, Dict +from azure.appconfiguration import SecretReferenceConfigurationSetting +from azure.keyvault.secrets import SecretClient, KeyVaultSecretIdentifier +from ._azureappconfigurationproviderbase import _RefreshTimer + +JSON = Mapping[str, Any] +_T = TypeVar("_T") + + +class SecretProvider(Mapping[str, Union[str, JSON]]): + + def __init__(self, **kwargs: Any) -> None: + self._secret_cache: Dict[str, Any] = {} + self._secret_clients: Dict[str, SecretClient] = {} + self._keyvault_credential = kwargs.pop("keyvault_credential", None) + self._secret_resolver = kwargs.pop("secret_resolver", None) + self._keyvault_client_configs = kwargs.pop("keyvault_client_configs", {}) + self.uses_key_vault = ( + self._keyvault_credential is not None + or (self._keyvault_client_configs is not None and len(self._keyvault_client_configs) > 0) + or self._secret_resolver is not None + ) + self.secret_refresh_timer: Optional[_RefreshTimer] = ( + _RefreshTimer(refresh_interval=kwargs.pop("secret_refresh_interval", 60)) + if self.uses_key_vault and "secret_refresh_interval" in kwargs + else None + ) + + def bust_cache(self) -> None: + """ + Clears the secret cache. + """ + self._secret_cache = {} + + def resolve_keyvault_reference(self, config: SecretReferenceConfigurationSetting) -> str: + # pylint:disable=protected-access + if config.key in self._secret_cache: + return self._secret_cache[config.key] + if not (self._keyvault_credential or self._keyvault_client_configs or self._secret_resolver): + raise ValueError( + """ + Either a credential to Key Vault, custom Key Vault client, or a secret resolver must be set to resolve + Key Vault references. + """ + ) + + if config.secret_id is None: + raise ValueError("Key Vault reference must have a uri value.") + + keyvault_identifier = KeyVaultSecretIdentifier(config.secret_id) + + vault_url = keyvault_identifier.vault_url + "/" + + # pylint:disable=protected-access + referenced_client = self._secret_clients.get(vault_url, None) + + vault_config = self._keyvault_client_configs.get(vault_url, {}) + credential = vault_config.pop("credential", self._keyvault_credential) + + if referenced_client is None and credential is not None: + referenced_client = SecretClient(vault_url=vault_url, credential=credential, **vault_config) + self._secret_clients[vault_url] = referenced_client + + if referenced_client: + secret_value = referenced_client.get_secret( + keyvault_identifier.name, version=keyvault_identifier.version + ).value + if secret_value is not None: + self._secret_cache[config.key] = secret_value + return secret_value + + if self._secret_resolver: + self._secret_cache[config.key] = self._secret_resolver(config.secret_id) + return self._secret_cache[config.key] + + raise ValueError("No Secret Client found for Key Vault reference %s" % (vault_url)) + + def __getitem__(self, key: str) -> Any: + # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype + """ + Returns the value of the specified key. + """ + return self._secret_cache[key] + + def __iter__(self) -> Iterator[str]: + return self._secret_cache.__iter__() + + def __len__(self) -> int: + return len(self._secret_cache) + + def __contains__(self, __x: object) -> bool: + # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype + """ + Returns True if the configuration settings contains the specified key. + """ + return self._secret_cache.__contains__(__x) + + def keys(self) -> KeysView[str]: + """ + Returns a list of keys loaded from Azure App Configuration. + + :return: A list of keys loaded from Azure App Configuration. + :rtype: KeysView[str] + """ + return self._secret_cache.keys() + + def items(self) -> ItemsView[str, Union[str, Mapping[str, Any]]]: + """ + Returns a set-like object of key-value pairs loaded from Azure App Configuration. Any values that are Key Vault + references will be resolved. + + :return: A set-like object of key-value pairs loaded from Azure App Configuration. + :rtype: ItemsView[str, Union[str, Mapping[str, Any]]] + """ + return self._secret_cache.items() + + def values(self) -> ValuesView[Union[str, Mapping[str, Any]]]: + """ + Returns a list of values loaded from Azure App Configuration. Any values that are Key Vault references will be + resolved. + + :return: A list of values loaded from Azure App Configuration. The values are either Strings or JSON objects, + based on there content type. + :rtype: ValuesView[Union[str, Mapping[str, Any]]] + """ + return (self._secret_cache).values() + + @overload + def get(self, key: str, default: None = None) -> Union[str, JSON, None]: ... + + @overload + def get(self, key: str, default: Union[str, JSON, _T]) -> Union[str, JSON, _T]: # pylint: disable=signature-differs + ... + + def get(self, key: str, default: Optional[Union[str, JSON, _T]] = None) -> Union[str, JSON, _T, None]: + """ + Returns the value of the specified key. If the key does not exist, returns the default value. + + :param str key: The key of the value to get. + :param default: The default value to return. + :type: str or None + :return: The value of the specified key. + :rtype: Union[str, JSON] + """ + return self._secret_cache.get(key, default) + + def __ne__(self, other: Any) -> bool: + return not self == other + + def close(self) -> None: + """ + Closes the connection to Azure App Configuration. + """ + for client in self._secret_clients.values(): + client.close() + + def __enter__(self) -> "SecretProvider": + for client in self._secret_clients.values(): + client.__enter__() + return self + + def __exit__(self, *args) -> None: + for client in self._secret_clients.values(): + client.__exit__() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py new file mode 100644 index 000000000000..38e190ecf8e5 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py @@ -0,0 +1,178 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- +from typing import Mapping, Union, Any, Iterator, KeysView, ItemsView, ValuesView, TypeVar, overload, Optional, Dict +from azure.appconfiguration import SecretReferenceConfigurationSetting +from azure.keyvault.secrets.aio import SecretClient +from azure.keyvault.secrets import KeyVaultSecretIdentifier +from .._azureappconfigurationproviderbase import _RefreshTimer + +JSON = Mapping[str, Any] +_T = TypeVar("_T") + + +class SecretProvider(Mapping[str, Union[str, JSON]]): + + def __init__(self, **kwargs: Any) -> None: + self._secret_cache: Dict[str, Any] = {} + self._secret_clients: Dict[str, SecretClient] = {} + self._keyvault_credential = kwargs.pop("keyvault_credential", None) + self._secret_resolver = kwargs.pop("secret_resolver", None) + self._keyvault_client_configs = kwargs.pop("keyvault_client_configs", {}) + self.uses_key_vault = ( + self._keyvault_credential is not None + or (self._keyvault_client_configs is not None and len(self._keyvault_client_configs) > 0) + or self._secret_resolver is not None + ) + self.secret_refresh_timer: Optional[_RefreshTimer] = ( + _RefreshTimer(refresh_interval=kwargs.pop("secret_refresh_interval", 60)) + if self.uses_key_vault and "secret_refresh_interval" in kwargs + else None + ) + + def bust_cache(self) -> None: + """ + Clears the secret cache. + """ + self._secret_cache = {} + + async def resolve_keyvault_reference(self, config: SecretReferenceConfigurationSetting) -> str: + # pylint:disable=protected-access + if config.key in self._secret_cache: + return self._secret_cache[config.key] + if not (self._keyvault_credential or self._keyvault_client_configs or self._secret_resolver): + raise ValueError( + """ + Either a credential to Key Vault, custom Key Vault client, or a secret resolver must be set to resolve + Key Vault references. + """ + ) + + if config.secret_id is None: + raise ValueError("Key Vault reference must have a uri value.") + + keyvault_identifier = KeyVaultSecretIdentifier(config.secret_id) + + vault_url = keyvault_identifier.vault_url + "/" + + # pylint:disable=protected-access + referenced_client = self._secret_clients.get(vault_url, None) + + vault_config = self._keyvault_client_configs.get(vault_url, {}) + credential = vault_config.pop("credential", self._keyvault_credential) + + if referenced_client is None and credential is not None: + referenced_client = SecretClient(vault_url=vault_url, credential=credential, **vault_config) + self._secret_clients[vault_url] = referenced_client + + if referenced_client: + secret_value = ( + await referenced_client.get_secret(keyvault_identifier.name, version=keyvault_identifier.version) + ).value + if secret_value is not None: + self._secret_cache[config.key] = secret_value + return secret_value + + if self._secret_resolver: + resolved = self._secret_resolver(config.secret_id) + try: + # Secret resolver was async + value = await resolved + self._secret_cache[config.key] = value + return value + except TypeError: + # Secret resolver was sync + self._secret_cache[config.key] = resolved + return resolved + + raise ValueError("No Secret Client found for Key Vault reference %s" % (vault_url)) + + def __getitem__(self, key: str) -> Any: + # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype + """ + Returns the value of the specified key. + """ + return self._secret_cache[key] + + def __iter__(self) -> Iterator[str]: + return self._secret_cache.__iter__() + + def __len__(self) -> int: + return len(self._secret_cache) + + def __contains__(self, __x: object) -> bool: + # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype + """ + Returns True if the configuration settings contains the specified key. + """ + return self._secret_cache.__contains__(__x) + + def keys(self) -> KeysView[str]: + """ + Returns a list of keys loaded from Azure App Configuration. + + :return: A list of keys loaded from Azure App Configuration. + :rtype: KeysView[str] + """ + return self._secret_cache.keys() + + def items(self) -> ItemsView[str, Union[str, Mapping[str, Any]]]: + """ + Returns a set-like object of key-value pairs loaded from Azure App Configuration. Any values that are Key Vault + references will be resolved. + + :return: A set-like object of key-value pairs loaded from Azure App Configuration. + :rtype: ItemsView[str, Union[str, Mapping[str, Any]]] + """ + return self._secret_cache.items() + + def values(self) -> ValuesView[Union[str, Mapping[str, Any]]]: + """ + Returns a list of values loaded from Azure App Configuration. Any values that are Key Vault references will be + resolved. + + :return: A list of values loaded from Azure App Configuration. The values are either Strings or JSON objects, + based on there content type. + :rtype: ValuesView[Union[str, Mapping[str, Any]]] + """ + return (self._secret_cache).values() + + @overload + def get(self, key: str, default: None = None) -> Union[str, JSON, None]: ... + + @overload + def get(self, key: str, default: Union[str, JSON, _T]) -> Union[str, JSON, _T]: # pylint: disable=signature-differs + ... + + def get(self, key: str, default: Optional[Union[str, JSON, _T]] = None) -> Union[str, JSON, _T, None]: + """ + Returns the value of the specified key. If the key does not exist, returns the default value. + + :param str key: The key of the value to get. + :param default: The default value to return. + :type: str or None + :return: The value of the specified key. + :rtype: Union[str, JSON] + """ + return self._secret_cache.get(key, default) + + def __ne__(self, other: Any) -> bool: + return not self == other + + async def close(self) -> None: + """ + Closes the connection to Azure App Configuration. + """ + for client in self._secret_clients.values(): + await client.close() + + async def __aenter__(self) -> "SecretProvider": + for client in self._secret_clients.values(): + await client.__aenter__() + return self + + async def __aexit__(self, *args) -> None: + for client in self._secret_clients.values(): + await client.__aexit__() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py index 2a37d3f713d5..f9d5331e6688 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -26,10 +26,8 @@ SecretReferenceConfigurationSetting, ) from azure.core.exceptions import AzureError, HttpResponseError -from azure.keyvault.secrets.aio import SecretClient -from azure.keyvault.secrets import KeyVaultSecretIdentifier - from .._models import AzureAppConfigurationKeyVaultOptions, SettingSelector +from ._async_secret_provider import SecretProvider from .._constants import ( FEATURE_MANAGEMENT_KEY, FEATURE_FLAG_KEY, @@ -252,54 +250,6 @@ async def _buildprovider( return AzureAppConfigurationProvider(**kwargs) -async def _resolve_keyvault_reference( - config: "SecretReferenceConfigurationSetting", provider: "AzureAppConfigurationProvider" -) -> str: - # pylint:disable=protected-access - if not (provider._keyvault_credential or provider._keyvault_client_configs or provider._secret_resolver): - raise ValueError( - """ - Either a credential to Key Vault, custom Key Vault client, or a secret resolver must be set to resolve Key - Vault references. - """ - ) - - if config.secret_id is None: - raise ValueError("Key Vault reference must have a uri value.") - - keyvault_identifier = KeyVaultSecretIdentifier(config.secret_id) - - vault_url = keyvault_identifier.vault_url + "/" - - # pylint:disable=protected-access - referenced_client = provider._secret_clients.get(vault_url, None) - - vault_config = provider._keyvault_client_configs.get(vault_url, {}) - credential = vault_config.pop("credential", provider._keyvault_credential) - - if referenced_client is None and credential is not None: - referenced_client = SecretClient(vault_url=vault_url, credential=credential, **vault_config) - provider._secret_clients[vault_url] = referenced_client - - if referenced_client: - secret_value = ( - await referenced_client.get_secret(keyvault_identifier.name, version=keyvault_identifier.version) - ).value - if secret_value is not None: - return secret_value - - if provider._secret_resolver: - resolved = provider._secret_resolver(config.secret_id) - try: - # Secret resolver was async - return await resolved - except TypeError: - # Secret resolver was sync - return resolved - - raise ValueError("No Secret Client found for Key Vault reference %s" % (vault_url)) - - class AzureAppConfigurationProvider(AzureAppConfigurationProviderBase): # pylint: disable=too-many-instance-attributes """ Provides a dictionary-like interface to Azure App Configuration settings. Enables loading of sets of configuration @@ -335,7 +285,7 @@ def __init__(self, **kwargs: Any) -> None: load_balancing_enabled=kwargs.pop("load_balancing_enabled", False), **kwargs, ) - self._secret_clients: Dict[str, SecretClient] = {} + self._secret_provider = SecretProvider(**kwargs) self._on_refresh_success: Optional[Callable] = kwargs.pop("on_refresh_success", None) self._on_refresh_error: Optional[Union[Callable[[Exception], Awaitable[None]], None]] = kwargs.pop( "on_refresh_error", None @@ -362,7 +312,6 @@ async def _common_refresh( await self._replica_client_manager.refresh_clients() self._replica_client_manager.find_active_clients() replica_count = self._replica_client_manager.get_client_count() - 1 - while client := self._replica_client_manager.get_next_active_client(): headers = update_correlation_context_header( kwargs.pop("headers", {}), @@ -370,7 +319,7 @@ async def _common_refresh( replica_count, self._feature_flag_enabled, self._feature_filter_usage, - self._uses_key_vault, + self._secret_provider.uses_key_vault, self._uses_load_balancing, is_failover_request, self._uses_ai_configuration, @@ -400,26 +349,35 @@ async def _common_refresh( elif self._on_refresh_success: self._on_refresh_success() - async def _refresh_configuration_settings(self, force: bool = False, **kwargs: Any) -> None: + async def _refresh_configuration_settings(self, **kwargs: Any) -> None: async def refresh_operation(client, headers, **inner_kwargs): configuration_settings: Optional[List[ConfigurationSetting]] = None need_refresh = False - force = inner_kwargs.pop("force", False) - if not force: + reset_secret_timer = False + + if ( + self._secret_provider.secret_refresh_timer + and self._secret_provider.secret_refresh_timer.needs_refresh() + ): + self._secret_provider.bust_cache() + reset_secret_timer = True + need_refresh = True + + if not need_refresh: need_refresh, self._refresh_on, configuration_settings = await client.refresh_configuration_settings( self._selects, self._refresh_on, headers=headers, **inner_kwargs ) else: # Force a refresh to make sure secrets are up to date - configuration_settings = ( + configuration_settings, self._refresh_on = ( await client.load_configuration_settings( self._selects, self._refresh_on, headers=headers, **inner_kwargs ) - )[0] - need_refresh = True + ) configuration_settings_processed: Dict[str, Any] = {} + if configuration_settings is not None: configuration_settings_processed = await self._process_configurations(configuration_settings) @@ -435,8 +393,8 @@ async def refresh_operation(client, headers, **inner_kwargs): self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags self._refresh_timer.reset() - if self._secret_refresh_timer: - self._secret_refresh_timer.reset() + if reset_secret_timer and self._secret_provider.secret_refresh_timer: + self._secret_provider.secret_refresh_timer.reset() return True @@ -444,8 +402,7 @@ async def refresh_operation(client, headers, **inner_kwargs): refresh_operation=refresh_operation, error_log_message="Failed to refresh configurations from endpoint %s", timer=self._refresh_timer, - refresh_condition=bool(self._refresh_on or force), - force=force, + refresh_condition=bool(self._refresh_on), **kwargs, ) @@ -481,15 +438,22 @@ async def refresh_operation(client, headers, **inner_kwargs): ) async def refresh(self, **kwargs) -> None: # pylint: disable=too-many-statements - if not self._refresh_on and not self._feature_flag_refresh_enabled and not self._secret_refresh_timer: + if ( + not self._refresh_on + and not self._feature_flag_refresh_enabled + and not self._secret_provider.secret_refresh_timer + ): logger.debug("Refresh called but no refresh enabled.") return if not self._refresh_lock.acquire(blocking=False): # pylint: disable= consider-using-with logger.debug("Refresh called but refresh already in progress.") return try: - if self._secret_refresh_timer and self._secret_refresh_timer.needs_refresh(): - await self._refresh_configuration_settings(force=True, **kwargs) + if ( + self._secret_provider.secret_refresh_timer + and self._secret_provider.secret_refresh_timer.needs_refresh() + ): + await self._refresh_configuration_settings(**kwargs) elif self._refresh_timer and self._refresh_timer.needs_refresh(): await self._refresh_configuration_settings(**kwargs) if self._feature_flag_refresh_enabled and ( @@ -517,7 +481,7 @@ async def _load_all(self, **kwargs): replica_count, self._feature_flag_enabled, self._feature_filter_usage, - self._uses_key_vault, + self._secret_provider.uses_key_vault, self._uses_load_balancing, is_failover_request, self._uses_ai_configuration, @@ -579,6 +543,7 @@ async def _process_configurations(self, configuration_settings: List[Configurati configuration_settings_processed = {} for config in configuration_settings: if isinstance(config, FeatureFlagConfigurationSetting): + # Feature flags are not processed like other settings continue key = self._process_key_name(config) value = await self._process_key_value(config) @@ -589,7 +554,7 @@ async def _process_configurations(self, configuration_settings: List[Configurati async def _process_key_value(self, config): if isinstance(config, SecretReferenceConfigurationSetting): - return await _resolve_keyvault_reference(config, self) + return await self._secret_provider.resolve_keyvault_reference(config) if is_json_content_type(config.content_type) and not isinstance(config, FeatureFlagConfigurationSetting): try: if APP_CONFIG_AI_MIME_PROFILE in config.content_type: @@ -619,17 +584,14 @@ async def close(self) -> None: """ Closes the connection to Azure App Configuration. """ - for client in self._secret_clients.values(): - await client.close() + await self._secret_provider.close() await self._replica_client_manager.close() async def __aenter__(self) -> "AzureAppConfigurationProvider": await self._replica_client_manager.__aenter__() - for client in self._secret_clients.values(): - await client.__aenter__() + await self._secret_provider.__aenter__() return self async def __aexit__(self, *args) -> None: await self._replica_client_manager.__aexit__(*args) - for client in self._secret_clients.values(): - await client.__aexit__() + await self._secret_provider.__aexit__(*args) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py index 9ffe05b3f3be..6e2e6fec045e 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py @@ -8,7 +8,7 @@ import unittest from unittest.mock import Mock, patch from azure.appconfiguration import SecretReferenceConfigurationSetting -from azure.appconfiguration.provider import SettingSelector +from azure.appconfiguration.provider import SettingSelector, WatchKey from devtools_testutils import recorded_by_proxy from async_preparers import app_config_aad_decorator_async from asynctestcase import AppConfigTestCase @@ -17,7 +17,7 @@ class TestAsyncSecretRefresh(AppConfigTestCase, unittest.TestCase): @recorded_by_proxy @app_config_aad_decorator_async - async def test_secret_refresh_timer( + async def testsecret_refresh_timer( self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url, @@ -81,7 +81,8 @@ async def test_secret_refresh_with_updated_values( keyvault_secret_url=appconfiguration_keyvault_secret_url, keyvault_secret_url2=appconfiguration_keyvault_secret_url2, on_refresh_success=mock_callback, - refresh_interval=999999, + refresh_on=[WatchKey("secret")], + refresh_interval=1, secret_refresh_interval=1, # Using a short interval for testing ) @@ -182,10 +183,10 @@ async def async_callback(): secret_refresh_interval=5, # Secret refresh interval is short ) - # Now patch the refresh method and _secret_refresh_timer to control behavior + # Now patch the refresh method and secret_refresh_timer to control behavior with patch.object(client, "refresh") as mock_refresh: - # Now patch the _secret_refresh_timer to control its behavior - with patch.object(client, "_secret_refresh_timer") as mock_timer: + # Now patch the secret_refresh_timer to control its behavior + with patch.object(client._secret_provider, "secret_refresh_timer") as mock_timer: # Make needs_refresh() return True to simulate timer expiration mock_timer.needs_refresh.return_value = True @@ -223,7 +224,7 @@ async def async_callback(): ) # Verify the secret refresh timer exists - assert client._secret_refresh_timer is not None + assert client._secret_provider.secret_refresh_timer is not None # We can only verify that it exists, but can't directly access the internal refresh_interval # as it's a protected attribute @@ -239,5 +240,5 @@ async def async_callback(): ) # Verify timer is created only when secret_refresh_interval is provided - assert client._secret_refresh_timer is not None - assert client2._secret_refresh_timer is None + assert client._secret_provider.secret_refresh_timer is not None + assert client2._secret_provider.secret_refresh_timer is None diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py index 6b0a55f6ff90..9343a3132b63 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py @@ -7,7 +7,7 @@ import unittest from unittest.mock import Mock, patch from azure.appconfiguration import SecretReferenceConfigurationSetting -from azure.appconfiguration.provider import SettingSelector +from azure.appconfiguration.provider import SettingSelector, WatchKey from devtools_testutils import recorded_by_proxy from preparers import app_config_decorator_aad from testcase import AppConfigTestCase @@ -75,7 +75,8 @@ def test_secret_refresh_with_updated_values( keyvault_secret_url=appconfiguration_keyvault_secret_url, keyvault_secret_url2=appconfiguration_keyvault_secret_url2, on_refresh_success=mock_callback, - refresh_interval=999999, + refresh_on=[WatchKey("secret")], + refresh_interval=1, secret_refresh_interval=1, # Using a short interval for testing ) @@ -166,10 +167,10 @@ def test_secret_refresh_timer_triggers_refresh( secret_refresh_interval=5, # Secret refresh interval is short ) - # Now patch the refresh method and _secret_refresh_timer to control behavior + # Now patch the refresh method and secret_refresh_timer to control behavior with patch.object(client, "refresh") as mock_refresh: - # Now patch the _secret_refresh_timer to control its behavior - with patch.object(client, "_secret_refresh_timer") as mock_timer: + # Now patch the secret_refresh_timer to control its behavior + with patch.object(client._secret_provider, "secret_refresh_timer") as mock_timer: # Make needs_refresh() return True to simulate timer expiration mock_timer.needs_refresh.return_value = True @@ -202,7 +203,7 @@ def test_secret_refresh_interval_parameter( ) # Verify the secret refresh timer exists - assert client._secret_refresh_timer is not None + assert client._secret_provider.secret_refresh_timer is not None # We can only verify that it exists, but can't directly access the internal refresh_interval # as it's a protected attribute @@ -218,5 +219,5 @@ def test_secret_refresh_interval_parameter( ) # Verify timer is created only when secret_refresh_interval is provided - assert client._secret_refresh_timer is not None - assert client2._secret_refresh_timer is None + assert client._secret_provider.secret_refresh_timer is not None + assert client2._secret_provider.secret_refresh_timer is None From 7ccb2a03691955e6e34fee456a708bd42e426fca Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 11 Aug 2025 11:16:08 -0700 Subject: [PATCH 11/43] Update _azureappconfigurationproviderasync.py --- .../provider/aio/_azureappconfigurationproviderasync.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py index f9d5331e6688..63cc9fa4ecd2 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -370,10 +370,8 @@ async def refresh_operation(client, headers, **inner_kwargs): ) else: # Force a refresh to make sure secrets are up to date - configuration_settings, self._refresh_on = ( - await client.load_configuration_settings( - self._selects, self._refresh_on, headers=headers, **inner_kwargs - ) + configuration_settings, self._refresh_on = await client.load_configuration_settings( + self._selects, self._refresh_on, headers=headers, **inner_kwargs ) configuration_settings_processed: Dict[str, Any] = {} From 2663cb3ed81b0de86b520ce9dcee8f982f42acf4 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 11 Aug 2025 14:01:49 -0700 Subject: [PATCH 12/43] Fixing Optional Endpoint --- .../azure-appconfiguration-provider/assets.json | 2 +- .../provider/_azureappconfigurationprovider.py | 4 ++-- .../provider/aio/_azureappconfigurationproviderasync.py | 2 +- .../tests/test_async_provider.py | 7 ++++--- .../azure-appconfiguration-provider/tests/test_provider.py | 3 ++- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json index 3c3611d3a523..572b07c75ab9 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json +++ b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/appconfiguration/azure-appconfiguration-provider", - "Tag": "python/appconfiguration/azure-appconfiguration-provider_e16b70d17c" + "Tag": "python/appconfiguration/azure-appconfiguration-provider_9e62d4d01d" } diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py index 8926ecb3989f..0826fd1dfcf5 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -269,8 +269,8 @@ def __init__(self, **kwargs: Any) -> None: max_backoff: int = min(kwargs.pop("max_backoff", 600), interval) self._replica_client_manager = ConfigurationClientManager( - connection_string=kwargs.pop("connection_string", None), - endpoint=kwargs.pop("endpoint", None), + connection_string=kwargs.pop("connection_string"), + endpoint=kwargs.pop("endpoint"), credential=kwargs.pop("credential", None), user_agent=user_agent, retry_total=kwargs.pop("retry_total", 2), diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py index 63cc9fa4ecd2..521759aee89b 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -274,7 +274,7 @@ def __init__(self, **kwargs: Any) -> None: self._replica_client_manager = AsyncConfigurationClientManager( connection_string=kwargs.pop("connection_string", None), - endpoint=kwargs.pop("endpoint", None), + endpoint=kwargs.pop("endpoint"), credential=kwargs.pop("credential", None), user_agent=user_agent, retry_total=kwargs.pop("retry_total", 2), diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider.py index b9e4956029f9..5cbb810d48ae 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider.py @@ -4,7 +4,6 @@ # license information. # -------------------------------------------------------------------------- from azure.appconfiguration.provider import SettingSelector, AzureAppConfigurationKeyVaultOptions -from azure.appconfiguration.provider.aio import AzureAppConfigurationProvider from devtools_testutils.aio import recorded_by_proxy_async from async_preparers import app_config_decorator_async from testcase import has_feature_flag @@ -15,7 +14,9 @@ from azure.appconfiguration.provider._azureappconfigurationproviderbase import ( update_correlation_context_header, ) - +from azure.appconfiguration.provider.aio._azureappconfigurationproviderasync import ( + _buildprovider, +) class TestAppConfigurationProvider(AppConfigTestCase): # method: provider_creation @@ -131,7 +132,7 @@ async def test_process_key_value_content_type(self, appconfiguration_connection_ ] # Create the provider with the mocked client manager - provider = AzureAppConfigurationProvider(connection_string="mock_connection_string") + provider = await _buildprovider("=mock_connection_string;;", None, None) provider._replica_client_manager = mock_client_manager # Call the method to process key-value pairs diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider.py index 8b6acc27454a..5386fa6b6661 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider.py @@ -19,6 +19,7 @@ delay_failure, update_correlation_context_header, ) +from azure.appconfiguration.provider._azureappconfigurationprovider import _buildprovider def sleep(seconds): @@ -147,7 +148,7 @@ def test_process_key_value_content_type(self): ] # Create the provider with the mocked client manager - provider = AzureAppConfigurationProvider(connection_string="mock_connection_string") + provider = _buildprovider("=mock_connection_string;;", None, None) provider._replica_client_manager = mock_client_manager # Call the method to process key-value pairs From 12fbdda27f49e8e50c9ab39131306f12a3446d56 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 11 Aug 2025 15:53:14 -0700 Subject: [PATCH 13/43] fix mypy issue --- .../azure/appconfiguration/provider/_secret_provider.py | 2 +- .../appconfiguration/provider/aio/_async_secret_provider.py | 2 +- .../tests/test_async_provider.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py index a10726034c7e..5439b6a73bbe 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py @@ -4,7 +4,7 @@ # license information. # ------------------------------------------------------------------------- from typing import Mapping, Union, Any, Iterator, KeysView, ItemsView, ValuesView, TypeVar, overload, Optional, Dict -from azure.appconfiguration import SecretReferenceConfigurationSetting +from azure.appconfiguration import SecretReferenceConfigurationSetting # type:ignore # pylint:disable=no-name-in-module from azure.keyvault.secrets import SecretClient, KeyVaultSecretIdentifier from ._azureappconfigurationproviderbase import _RefreshTimer diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py index 38e190ecf8e5..8f68c0bf0876 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py @@ -4,7 +4,7 @@ # license information. # ------------------------------------------------------------------------- from typing import Mapping, Union, Any, Iterator, KeysView, ItemsView, ValuesView, TypeVar, overload, Optional, Dict -from azure.appconfiguration import SecretReferenceConfigurationSetting +from azure.appconfiguration import SecretReferenceConfigurationSetting # type:ignore # pylint:disable=no-name-in-module from azure.keyvault.secrets.aio import SecretClient from azure.keyvault.secrets import KeyVaultSecretIdentifier from .._azureappconfigurationproviderbase import _RefreshTimer diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider.py index 5cbb810d48ae..6222b51f559b 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider.py @@ -10,7 +10,6 @@ from asynctestcase import AppConfigTestCase from test_constants import FEATURE_MANAGEMENT_KEY from unittest.mock import MagicMock, patch -import asyncio from azure.appconfiguration.provider._azureappconfigurationproviderbase import ( update_correlation_context_header, ) @@ -18,6 +17,7 @@ _buildprovider, ) + class TestAppConfigurationProvider(AppConfigTestCase): # method: provider_creation @app_config_decorator_async From 2758caf04264d0b6edf8d0781971cc3c35c03243 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 12 Aug 2025 16:09:42 -0700 Subject: [PATCH 14/43] fixing async test --- .../azure-appconfiguration-provider/assets.json | 2 +- .../tests/test_async_secret_refresh.py | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json index 572b07c75ab9..ab53cdb0d738 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json +++ b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/appconfiguration/azure-appconfiguration-provider", - "Tag": "python/appconfiguration/azure-appconfiguration-provider_9e62d4d01d" + "Tag": "python/appconfiguration/azure-appconfiguration-provider_dc8f23ef71" } diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py index 6e2e6fec045e..16cf010e1c17 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py @@ -7,6 +7,7 @@ import asyncio import unittest from unittest.mock import Mock, patch +from devtools_testutils.aio import recorded_by_proxy_async from azure.appconfiguration import SecretReferenceConfigurationSetting from azure.appconfiguration.provider import SettingSelector, WatchKey from devtools_testutils import recorded_by_proxy @@ -15,8 +16,9 @@ class TestAsyncSecretRefresh(AppConfigTestCase, unittest.TestCase): - @recorded_by_proxy + @app_config_aad_decorator_async + @recorded_by_proxy_async async def testsecret_refresh_timer( self, appconfiguration_endpoint_string, @@ -63,8 +65,8 @@ async def async_callback(): # Should have been called at least twice now assert mock_refresh.call_count >= 2 - @recorded_by_proxy @app_config_aad_decorator_async + @recorded_by_proxy_async async def test_secret_refresh_with_updated_values( self, appconfiguration_endpoint_string, @@ -111,8 +113,8 @@ async def test_secret_refresh_with_updated_values( assert client["secret"] == "Very secret value 2" assert mock_callback.call_count >= 1 - @recorded_by_proxy @app_config_aad_decorator_async + @recorded_by_proxy_async async def test_no_secret_refresh_without_timer( self, appconfiguration_endpoint_string, @@ -156,8 +158,8 @@ async def async_callback(): # but there should be no automatic refresh caused by the secret timer assert mock_time.call_count == 2 - @recorded_by_proxy @app_config_aad_decorator_async + @recorded_by_proxy_async async def test_secret_refresh_timer_triggers_refresh( self, appconfiguration_endpoint_string, @@ -196,8 +198,8 @@ async def async_callback(): # Verify refresh was called assert mock_refresh.call_count > 0 - @recorded_by_proxy @app_config_aad_decorator_async + @recorded_by_proxy_async async def test_secret_refresh_interval_parameter( self, appconfiguration_endpoint_string, From f2f279f33320a7cd126df2046a84cf8c35619dab Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 22 Aug 2025 09:22:37 -0700 Subject: [PATCH 15/43] mixing merge --- .../azure-appconfiguration-provider/tests/asynctestcase.py | 2 +- .../azure-appconfiguration-provider/tests/testcase.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py index ce69d9be5a45..bdab2e0670f2 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/asynctestcase.py @@ -21,7 +21,7 @@ async def create_client(self, **kwargs): else: client = AzureAppConfigurationClient(kwargs["endpoint"], credential) - await setup_configs(client, kwargs.get("keyvault_secret_url")) + await setup_configs(client, kwargs.get("keyvault_secret_url"), kwargs.get("keyvault_secret_url2")) kwargs["user_agent"] = "SDK/Integration" if "endpoint" in kwargs: diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py index b5bb8e7cc8f9..4e3d725fd912 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py @@ -22,7 +22,7 @@ def create_client(self, **kwargs): else: client = AzureAppConfigurationClient(kwargs["endpoint"], credential) - setup_configs(client, kwargs.get("keyvault_secret_url")) + setup_configs(client, kwargs.get("keyvault_secret_url"), kwargs.get("keyvault_secret_url2")) kwargs["user_agent"] = "SDK/Integration" if "endpoint" in kwargs: From 4b7f2c59ee6aaa96605de0ed1007cb1406500349 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 22 Aug 2025 09:35:11 -0700 Subject: [PATCH 16/43] fixing test after merge --- .../tests/test_async_secret_refresh.py | 24 +++++++++---------- .../tests/test_secret_refresh.py | 24 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py index 16cf010e1c17..169f8fab3f07 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py @@ -34,8 +34,8 @@ async def async_callback(): mock_callback = Mock(side_effect=async_callback) # Create client with key vault reference and secret refresh interval - client = await self.create_aad_client( - appconfiguration_endpoint_string, + client = await self.create_client( + endpoint=appconfiguration_endpoint_string, selects={SettingSelector(key_filter="*", label_filter="prod")}, keyvault_secret_url=appconfiguration_keyvault_secret_url, keyvault_secret_url2=appconfiguration_keyvault_secret_url2, @@ -77,8 +77,8 @@ async def test_secret_refresh_with_updated_values( mock_callback = Mock() # Create client with the mock secret resolver - client = await self.create_aad_client( - appconfiguration_endpoint_string, + client = await self.create_client( + endpoint=appconfiguration_endpoint_string, selects={SettingSelector(key_filter="*", label_filter="prod")}, keyvault_secret_url=appconfiguration_keyvault_secret_url, keyvault_secret_url2=appconfiguration_keyvault_secret_url2, @@ -130,8 +130,8 @@ async def async_callback(): mock_callback = Mock(side_effect=async_callback) # Create client without specifying secret_refresh_interval - client = await self.create_aad_client( - appconfiguration_endpoint_string, + client = await self.create_client( + endpoint=appconfiguration_endpoint_string, selects={SettingSelector(key_filter="*", label_filter="prod")}, keyvault_secret_url=appconfiguration_keyvault_secret_url, keyvault_secret_url2=appconfiguration_keyvault_secret_url2, @@ -175,8 +175,8 @@ async def async_callback(): mock_callback = Mock(side_effect=async_callback) # Create client with key vault reference and separate refresh intervals - client = await self.create_aad_client( - appconfiguration_endpoint_string, + client = await self.create_client( + endpoint=appconfiguration_endpoint_string, selects={SettingSelector(key_filter="*", label_filter="prod")}, keyvault_secret_url=appconfiguration_keyvault_secret_url, keyvault_secret_url2=appconfiguration_keyvault_secret_url2, @@ -215,8 +215,8 @@ async def async_callback(): mock_callback = Mock(side_effect=async_callback) # Create client with specific secret_refresh_interval - client = await self.create_aad_client( - appconfiguration_endpoint_string, + client = await self.create_client( + endpoint=appconfiguration_endpoint_string, selects={SettingSelector(key_filter="*", label_filter="prod")}, keyvault_secret_url=appconfiguration_keyvault_secret_url, keyvault_secret_url2=appconfiguration_keyvault_secret_url2, @@ -232,8 +232,8 @@ async def async_callback(): # as it's a protected attribute # Check with no refresh interval to ensure it's properly handled - client2 = await self.create_aad_client( - appconfiguration_endpoint_string, + client2 = await self.create_client( + endpoint=appconfiguration_endpoint_string, selects={SettingSelector(key_filter="*", label_filter="prod")}, keyvault_secret_url=appconfiguration_keyvault_secret_url, keyvault_secret_url2=appconfiguration_keyvault_secret_url2, diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py index 9343a3132b63..5dd98cc7affc 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py @@ -26,8 +26,8 @@ def test_secret_refresh_timer( mock_callback = Mock() # Create client with key vault reference and secret refresh interval - client = self.create_aad_client( - appconfiguration_endpoint_string, + client = self.create_client( + endpoint=appconfiguration_endpoint_string, selects={SettingSelector(key_filter="*", label_filter="prod")}, keyvault_secret_url=appconfiguration_keyvault_secret_url, keyvault_secret_url2=appconfiguration_keyvault_secret_url2, @@ -69,8 +69,8 @@ def test_secret_refresh_with_updated_values( mock_callback = Mock() # Create client with the mock secret resolver - client = self.create_aad_client( - appconfiguration_endpoint_string, + client = self.create_client( + endpoint=appconfiguration_endpoint_string, selects={SettingSelector(key_filter="*", label_filter="prod")}, keyvault_secret_url=appconfiguration_keyvault_secret_url, keyvault_secret_url2=appconfiguration_keyvault_secret_url2, @@ -117,8 +117,8 @@ def test_no_secret_refresh_without_timer( mock_callback = Mock() # Create client without specifying secret_refresh_interval - client = self.create_aad_client( - appconfiguration_endpoint_string, + client = self.create_client( + endpoint=appconfiguration_endpoint_string, selects={SettingSelector(key_filter="*", label_filter="prod")}, keyvault_secret_url=appconfiguration_keyvault_secret_url, keyvault_secret_url2=appconfiguration_keyvault_secret_url2, @@ -157,8 +157,8 @@ def test_secret_refresh_timer_triggers_refresh( mock_callback = Mock() # Create client with key vault reference and separate refresh intervals - client = self.create_aad_client( - appconfiguration_endpoint_string, + client = self.create_client( + endpoint=appconfiguration_endpoint_string, selects={SettingSelector(key_filter="*", label_filter="prod")}, keyvault_secret_url=appconfiguration_keyvault_secret_url, keyvault_secret_url2=appconfiguration_keyvault_secret_url2, @@ -192,8 +192,8 @@ def test_secret_refresh_interval_parameter( mock_callback = Mock() # Create client with specific secret_refresh_interval - client = self.create_aad_client( - appconfiguration_endpoint_string, + client = self.create_client( + endpoint=appconfiguration_endpoint_string, selects={SettingSelector(key_filter="*", label_filter="prod")}, keyvault_secret_url=appconfiguration_keyvault_secret_url, keyvault_secret_url2=appconfiguration_keyvault_secret_url2, @@ -209,8 +209,8 @@ def test_secret_refresh_interval_parameter( # as it's a protected attribute # Check with no refresh interval to ensure it's properly handled - client2 = self.create_aad_client( - appconfiguration_endpoint_string, + client2 = self.create_client( + endpoint=appconfiguration_endpoint_string, selects={SettingSelector(key_filter="*", label_filter="prod")}, keyvault_secret_url=appconfiguration_keyvault_secret_url, keyvault_secret_url2=appconfiguration_keyvault_secret_url2, From 7d95da6728224924f4666fe06423d3fc0c692b34 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 22 Aug 2025 10:18:37 -0700 Subject: [PATCH 17/43] Update testcase.py --- .../azure-appconfiguration-provider/tests/testcase.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py index 4e3d725fd912..67451060f4f5 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/testcase.py @@ -5,7 +5,12 @@ # license information. # -------------------------------------------------------------------------- from devtools_testutils import AzureRecordedTestCase -from azure.appconfiguration import AzureAppConfigurationClient, ConfigurationSetting, FeatureFlagConfigurationSetting, SecretReferenceConfigurationSetting +from azure.appconfiguration import ( + AzureAppConfigurationClient, + ConfigurationSetting, + FeatureFlagConfigurationSetting, + SecretReferenceConfigurationSetting, +) from azure.appconfiguration.provider import load, AzureAppConfigurationKeyVaultOptions from azure.appconfiguration.provider._constants import NULL_CHAR from test_constants import FEATURE_MANAGEMENT_KEY, FEATURE_FLAG_KEY From bfaaa280b0f5e3b10dc7f664912173c9bd9da0e0 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 27 Aug 2025 13:29:55 -0700 Subject: [PATCH 18/43] Secret Provider Base --- .../provider/_secret_provider.py | 96 +--------------- .../provider/_secret_provider_base.py | 104 ++++++++++++++++++ .../provider/aio/_async_secret_provider.py | 95 +--------------- 3 files changed, 112 insertions(+), 183 deletions(-) create mode 100644 sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider_base.py diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py index 5439b6a73bbe..5f5b4f2bff4c 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py @@ -6,42 +6,26 @@ from typing import Mapping, Union, Any, Iterator, KeysView, ItemsView, ValuesView, TypeVar, overload, Optional, Dict from azure.appconfiguration import SecretReferenceConfigurationSetting # type:ignore # pylint:disable=no-name-in-module from azure.keyvault.secrets import SecretClient, KeyVaultSecretIdentifier -from ._azureappconfigurationproviderbase import _RefreshTimer +from ._secret_provider_base import _SecretProviderBase JSON = Mapping[str, Any] _T = TypeVar("_T") -class SecretProvider(Mapping[str, Union[str, JSON]]): +class SecretProvider(_SecretProviderBase): def __init__(self, **kwargs: Any) -> None: - self._secret_cache: Dict[str, Any] = {} + super().__init__(**kwargs) self._secret_clients: Dict[str, SecretClient] = {} self._keyvault_credential = kwargs.pop("keyvault_credential", None) self._secret_resolver = kwargs.pop("secret_resolver", None) self._keyvault_client_configs = kwargs.pop("keyvault_client_configs", {}) - self.uses_key_vault = ( - self._keyvault_credential is not None - or (self._keyvault_client_configs is not None and len(self._keyvault_client_configs) > 0) - or self._secret_resolver is not None - ) - self.secret_refresh_timer: Optional[_RefreshTimer] = ( - _RefreshTimer(refresh_interval=kwargs.pop("secret_refresh_interval", 60)) - if self.uses_key_vault and "secret_refresh_interval" in kwargs - else None - ) - - def bust_cache(self) -> None: - """ - Clears the secret cache. - """ - self._secret_cache = {} def resolve_keyvault_reference(self, config: SecretReferenceConfigurationSetting) -> str: # pylint:disable=protected-access if config.key in self._secret_cache: return self._secret_cache[config.key] - if not (self._keyvault_credential or self._keyvault_client_configs or self._secret_resolver): + if not self.uses_key_vault: raise ValueError( """ Either a credential to Key Vault, custom Key Vault client, or a secret resolver must be set to resolve @@ -80,78 +64,6 @@ def resolve_keyvault_reference(self, config: SecretReferenceConfigurationSetting raise ValueError("No Secret Client found for Key Vault reference %s" % (vault_url)) - def __getitem__(self, key: str) -> Any: - # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype - """ - Returns the value of the specified key. - """ - return self._secret_cache[key] - - def __iter__(self) -> Iterator[str]: - return self._secret_cache.__iter__() - - def __len__(self) -> int: - return len(self._secret_cache) - - def __contains__(self, __x: object) -> bool: - # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype - """ - Returns True if the configuration settings contains the specified key. - """ - return self._secret_cache.__contains__(__x) - - def keys(self) -> KeysView[str]: - """ - Returns a list of keys loaded from Azure App Configuration. - - :return: A list of keys loaded from Azure App Configuration. - :rtype: KeysView[str] - """ - return self._secret_cache.keys() - - def items(self) -> ItemsView[str, Union[str, Mapping[str, Any]]]: - """ - Returns a set-like object of key-value pairs loaded from Azure App Configuration. Any values that are Key Vault - references will be resolved. - - :return: A set-like object of key-value pairs loaded from Azure App Configuration. - :rtype: ItemsView[str, Union[str, Mapping[str, Any]]] - """ - return self._secret_cache.items() - - def values(self) -> ValuesView[Union[str, Mapping[str, Any]]]: - """ - Returns a list of values loaded from Azure App Configuration. Any values that are Key Vault references will be - resolved. - - :return: A list of values loaded from Azure App Configuration. The values are either Strings or JSON objects, - based on there content type. - :rtype: ValuesView[Union[str, Mapping[str, Any]]] - """ - return (self._secret_cache).values() - - @overload - def get(self, key: str, default: None = None) -> Union[str, JSON, None]: ... - - @overload - def get(self, key: str, default: Union[str, JSON, _T]) -> Union[str, JSON, _T]: # pylint: disable=signature-differs - ... - - def get(self, key: str, default: Optional[Union[str, JSON, _T]] = None) -> Union[str, JSON, _T, None]: - """ - Returns the value of the specified key. If the key does not exist, returns the default value. - - :param str key: The key of the value to get. - :param default: The default value to return. - :type: str or None - :return: The value of the specified key. - :rtype: Union[str, JSON] - """ - return self._secret_cache.get(key, default) - - def __ne__(self, other: Any) -> bool: - return not self == other - def close(self) -> None: """ Closes the connection to Azure App Configuration. diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider_base.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider_base.py new file mode 100644 index 000000000000..f9ea6a1fbae5 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider_base.py @@ -0,0 +1,104 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- +from typing import Mapping, Union, Any, Iterator, KeysView, ItemsView, ValuesView, TypeVar, overload, Optional, Dict +from ._azureappconfigurationproviderbase import _RefreshTimer + +JSON = Mapping[str, Any] +_T = TypeVar("_T") + + +class _SecretProviderBase(Mapping[str, Union[str, JSON]]): + + def __init__(self, **kwargs: Any) -> None: + self._secret_cache: Dict[str, Any] = {} + self.uses_key_vault = ( + "keyvault_credential" in kwargs + or ("keyvault_client_configs" in kwargs and len(kwargs.get("keyvault_client_configs", {})) > 0) + or "secret_resolver" in kwargs + ) + self.secret_refresh_timer: Optional[_RefreshTimer] = ( + _RefreshTimer(refresh_interval=kwargs.pop("secret_refresh_interval", 60)) + if self.uses_key_vault and "secret_refresh_interval" in kwargs + else None + ) + + def bust_cache(self) -> None: + """ + Clears the secret cache. + """ + self._secret_cache = {} + + def __getitem__(self, key: str) -> Any: + # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype + """ + Returns the value of the specified key. + """ + return self._secret_cache[key] + + def __iter__(self) -> Iterator[str]: + return self._secret_cache.__iter__() + + def __len__(self) -> int: + return len(self._secret_cache) + + def __contains__(self, __x: object) -> bool: + # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype + """ + Returns True if the configuration settings contains the specified key. + """ + return self._secret_cache.__contains__(__x) + + def keys(self) -> KeysView[str]: + """ + Returns a list of keys loaded from Azure App Configuration. + + :return: A list of keys loaded from Azure App Configuration. + :rtype: KeysView[str] + """ + return self._secret_cache.keys() + + def items(self) -> ItemsView[str, Union[str, Mapping[str, Any]]]: + """ + Returns a set-like object of key-value pairs loaded from Azure App Configuration. Any values that are Key Vault + references will be resolved. + + :return: A set-like object of key-value pairs loaded from Azure App Configuration. + :rtype: ItemsView[str, Union[str, Mapping[str, Any]]] + """ + return self._secret_cache.items() + + def values(self) -> ValuesView[Union[str, Mapping[str, Any]]]: + """ + Returns a list of values loaded from Azure App Configuration. Any values that are Key Vault references will be + resolved. + + :return: A list of values loaded from Azure App Configuration. The values are either Strings or JSON objects, + based on there content type. + :rtype: ValuesView[Union[str, Mapping[str, Any]]] + """ + return (self._secret_cache).values() + + @overload + def get(self, key: str, default: None = None) -> Union[str, JSON, None]: ... + + @overload + def get(self, key: str, default: Union[str, JSON, _T]) -> Union[str, JSON, _T]: # pylint: disable=signature-differs + ... + + def get(self, key: str, default: Optional[Union[str, JSON, _T]] = None) -> Union[str, JSON, _T, None]: + """ + Returns the value of the specified key. If the key does not exist, returns the default value. + + :param str key: The key of the value to get. + :param default: The default value to return. + :type: str or None + :return: The value of the specified key. + :rtype: Union[str, JSON] + """ + return self._secret_cache.get(key, default) + + def __ne__(self, other: Any) -> bool: + return not self == other diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py index 8f68c0bf0876..9ae43eba8bca 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py @@ -8,41 +8,26 @@ from azure.keyvault.secrets.aio import SecretClient from azure.keyvault.secrets import KeyVaultSecretIdentifier from .._azureappconfigurationproviderbase import _RefreshTimer +from .._secret_provider_base import _SecretProviderBase JSON = Mapping[str, Any] _T = TypeVar("_T") -class SecretProvider(Mapping[str, Union[str, JSON]]): +class SecretProvider(_SecretProviderBase): def __init__(self, **kwargs: Any) -> None: - self._secret_cache: Dict[str, Any] = {} + super().__init__(**kwargs) self._secret_clients: Dict[str, SecretClient] = {} self._keyvault_credential = kwargs.pop("keyvault_credential", None) self._secret_resolver = kwargs.pop("secret_resolver", None) self._keyvault_client_configs = kwargs.pop("keyvault_client_configs", {}) - self.uses_key_vault = ( - self._keyvault_credential is not None - or (self._keyvault_client_configs is not None and len(self._keyvault_client_configs) > 0) - or self._secret_resolver is not None - ) - self.secret_refresh_timer: Optional[_RefreshTimer] = ( - _RefreshTimer(refresh_interval=kwargs.pop("secret_refresh_interval", 60)) - if self.uses_key_vault and "secret_refresh_interval" in kwargs - else None - ) - - def bust_cache(self) -> None: - """ - Clears the secret cache. - """ - self._secret_cache = {} async def resolve_keyvault_reference(self, config: SecretReferenceConfigurationSetting) -> str: # pylint:disable=protected-access if config.key in self._secret_cache: return self._secret_cache[config.key] - if not (self._keyvault_credential or self._keyvault_client_configs or self._secret_resolver): + if not self.uses_key_vault: raise ValueError( """ Either a credential to Key Vault, custom Key Vault client, or a secret resolver must be set to resolve @@ -89,78 +74,6 @@ async def resolve_keyvault_reference(self, config: SecretReferenceConfigurationS raise ValueError("No Secret Client found for Key Vault reference %s" % (vault_url)) - def __getitem__(self, key: str) -> Any: - # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype - """ - Returns the value of the specified key. - """ - return self._secret_cache[key] - - def __iter__(self) -> Iterator[str]: - return self._secret_cache.__iter__() - - def __len__(self) -> int: - return len(self._secret_cache) - - def __contains__(self, __x: object) -> bool: - # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype - """ - Returns True if the configuration settings contains the specified key. - """ - return self._secret_cache.__contains__(__x) - - def keys(self) -> KeysView[str]: - """ - Returns a list of keys loaded from Azure App Configuration. - - :return: A list of keys loaded from Azure App Configuration. - :rtype: KeysView[str] - """ - return self._secret_cache.keys() - - def items(self) -> ItemsView[str, Union[str, Mapping[str, Any]]]: - """ - Returns a set-like object of key-value pairs loaded from Azure App Configuration. Any values that are Key Vault - references will be resolved. - - :return: A set-like object of key-value pairs loaded from Azure App Configuration. - :rtype: ItemsView[str, Union[str, Mapping[str, Any]]] - """ - return self._secret_cache.items() - - def values(self) -> ValuesView[Union[str, Mapping[str, Any]]]: - """ - Returns a list of values loaded from Azure App Configuration. Any values that are Key Vault references will be - resolved. - - :return: A list of values loaded from Azure App Configuration. The values are either Strings or JSON objects, - based on there content type. - :rtype: ValuesView[Union[str, Mapping[str, Any]]] - """ - return (self._secret_cache).values() - - @overload - def get(self, key: str, default: None = None) -> Union[str, JSON, None]: ... - - @overload - def get(self, key: str, default: Union[str, JSON, _T]) -> Union[str, JSON, _T]: # pylint: disable=signature-differs - ... - - def get(self, key: str, default: Optional[Union[str, JSON, _T]] = None) -> Union[str, JSON, _T, None]: - """ - Returns the value of the specified key. If the key does not exist, returns the default value. - - :param str key: The key of the value to get. - :param default: The default value to return. - :type: str or None - :return: The value of the specified key. - :rtype: Union[str, JSON] - """ - return self._secret_cache.get(key, default) - - def __ne__(self, other: Any) -> bool: - return not self == other - async def close(self) -> None: """ Closes the connection to Azure App Configuration. From 0ab5e6616b3a395e659f4c7cb39dd047cdc829a5 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 27 Aug 2025 13:31:56 -0700 Subject: [PATCH 19/43] removing unused imports --- .../azure/appconfiguration/provider/_secret_provider.py | 2 +- .../appconfiguration/provider/aio/_async_secret_provider.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py index 5f5b4f2bff4c..41142b225fb0 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py @@ -3,7 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # ------------------------------------------------------------------------- -from typing import Mapping, Union, Any, Iterator, KeysView, ItemsView, ValuesView, TypeVar, overload, Optional, Dict +from typing import Mapping, Any, TypeVar, Dict from azure.appconfiguration import SecretReferenceConfigurationSetting # type:ignore # pylint:disable=no-name-in-module from azure.keyvault.secrets import SecretClient, KeyVaultSecretIdentifier from ._secret_provider_base import _SecretProviderBase diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py index 9ae43eba8bca..8eb6fc04cb58 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py @@ -3,11 +3,10 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # ------------------------------------------------------------------------- -from typing import Mapping, Union, Any, Iterator, KeysView, ItemsView, ValuesView, TypeVar, overload, Optional, Dict +from typing import Mapping, Any, TypeVar, Dict from azure.appconfiguration import SecretReferenceConfigurationSetting # type:ignore # pylint:disable=no-name-in-module from azure.keyvault.secrets.aio import SecretClient from azure.keyvault.secrets import KeyVaultSecretIdentifier -from .._azureappconfigurationproviderbase import _RefreshTimer from .._secret_provider_base import _SecretProviderBase JSON = Mapping[str, Any] From 6d68ba3078391f411d9a642bda3e8fa85b6839ab Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 27 Aug 2025 13:55:16 -0700 Subject: [PATCH 20/43] updating exception --- .../provider/_azureappconfigurationprovider.py | 4 +++- .../provider/aio/_azureappconfigurationproviderasync.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py index 0826fd1dfcf5..9e79d612bb40 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -301,7 +301,7 @@ def _common_refresh( error_message = """ Failed to refresh configuration settings from Azure App Configuration. """ - exception: Exception = RuntimeError(error_message) + exception: Optional[Exception] = None is_failover_request = False self._replica_client_manager.refresh_clients() self._replica_client_manager.find_active_clients() @@ -332,6 +332,8 @@ def _common_refresh( is_failover_request = True if not success: + if exception is None: + exception = RuntimeError(error_message) timer.backoff() if self._on_refresh_error: self._on_refresh_error(exception) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py index 521759aee89b..ccd3452e578d 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -307,7 +307,7 @@ async def _common_refresh( error_message = """ Failed to refresh configuration settings from Azure App Configuration. """ - exception: Exception = RuntimeError(error_message) + exception: Optional[Exception] = None is_failover_request = False await self._replica_client_manager.refresh_clients() self._replica_client_manager.find_active_clients() @@ -338,6 +338,8 @@ async def _common_refresh( is_failover_request = True if not success: + if exception is None: + exception = RuntimeError(error_message) timer.backoff() if self._on_refresh_error: self._on_refresh_error(exception) From 15af7f34b6daa75a2be27e865ddbff6bc4ce96ce Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 27 Aug 2025 15:19:38 -0700 Subject: [PATCH 21/43] updating resolve key vault references --- .../provider/_secret_provider.py | 32 +++++--------- .../provider/_secret_provider_base.py | 37 +++++++++++++++- .../provider/aio/_async_secret_provider.py | 42 +++++++------------ 3 files changed, 60 insertions(+), 51 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py index 41142b225fb0..2537320a5bbc 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py @@ -5,7 +5,7 @@ # ------------------------------------------------------------------------- from typing import Mapping, Any, TypeVar, Dict from azure.appconfiguration import SecretReferenceConfigurationSetting # type:ignore # pylint:disable=no-name-in-module -from azure.keyvault.secrets import SecretClient, KeyVaultSecretIdentifier +from azure.keyvault.secrets import SecretClient from ._secret_provider_base import _SecretProviderBase JSON = Mapping[str, Any] @@ -22,23 +22,9 @@ def __init__(self, **kwargs: Any) -> None: self._keyvault_client_configs = kwargs.pop("keyvault_client_configs", {}) def resolve_keyvault_reference(self, config: SecretReferenceConfigurationSetting) -> str: - # pylint:disable=protected-access if config.key in self._secret_cache: return self._secret_cache[config.key] - if not self.uses_key_vault: - raise ValueError( - """ - Either a credential to Key Vault, custom Key Vault client, or a secret resolver must be set to resolve - Key Vault references. - """ - ) - - if config.secret_id is None: - raise ValueError("Key Vault reference must have a uri value.") - - keyvault_identifier = KeyVaultSecretIdentifier(config.secret_id) - - vault_url = keyvault_identifier.vault_url + "/" + keyvault_identifier, vault_url = self.resolve_keyvault_reference_base(config) # pylint:disable=protected-access referenced_client = self._secret_clients.get(vault_url, None) @@ -50,17 +36,19 @@ def resolve_keyvault_reference(self, config: SecretReferenceConfigurationSetting referenced_client = SecretClient(vault_url=vault_url, credential=credential, **vault_config) self._secret_clients[vault_url] = referenced_client + secret_value = None + if referenced_client: secret_value = referenced_client.get_secret( keyvault_identifier.name, version=keyvault_identifier.version ).value - if secret_value is not None: - self._secret_cache[config.key] = secret_value - return secret_value - if self._secret_resolver: - self._secret_cache[config.key] = self._secret_resolver(config.secret_id) - return self._secret_cache[config.key] + if self._secret_resolver and secret_value is None: + secret_value = self._secret_resolver(config.secret_id) + + if secret_value: + self._secret_cache[config.key] = secret_value + return secret_value raise ValueError("No Secret Client found for Key Vault reference %s" % (vault_url)) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider_base.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider_base.py index f9ea6a1fbae5..02f3603977df 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider_base.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider_base.py @@ -3,7 +3,22 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # ------------------------------------------------------------------------- -from typing import Mapping, Union, Any, Iterator, KeysView, ItemsView, ValuesView, TypeVar, overload, Optional, Dict +from typing import ( + Mapping, + Union, + Any, + Iterator, + KeysView, + ItemsView, + ValuesView, + TypeVar, + overload, + Optional, + Dict, + Tuple, +) +from azure.appconfiguration import SecretReferenceConfigurationSetting # type:ignore # pylint:disable=no-name-in-module +from azure.keyvault.secrets import KeyVaultSecretIdentifier from ._azureappconfigurationproviderbase import _RefreshTimer JSON = Mapping[str, Any] @@ -31,6 +46,26 @@ def bust_cache(self) -> None: """ self._secret_cache = {} + def resolve_keyvault_reference_base( + self, config: SecretReferenceConfigurationSetting + ) -> Tuple[KeyVaultSecretIdentifier, str]: + if not self.uses_key_vault: + raise ValueError( + """ + Either a credential to Key Vault, custom Key Vault client, or a secret resolver must be set to resolve + Key Vault references. + """ + ) + + if config.secret_id is None: + raise ValueError("Key Vault reference must have a uri value.") + + keyvault_identifier = KeyVaultSecretIdentifier(config.secret_id) + + vault_url = keyvault_identifier.vault_url + "/" + + return keyvault_identifier, vault_url + def __getitem__(self, key: str) -> Any: # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype """ diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py index 8eb6fc04cb58..70c90c1449bf 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py @@ -3,10 +3,10 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # ------------------------------------------------------------------------- +import inspect from typing import Mapping, Any, TypeVar, Dict from azure.appconfiguration import SecretReferenceConfigurationSetting # type:ignore # pylint:disable=no-name-in-module from azure.keyvault.secrets.aio import SecretClient -from azure.keyvault.secrets import KeyVaultSecretIdentifier from .._secret_provider_base import _SecretProviderBase JSON = Mapping[str, Any] @@ -23,23 +23,9 @@ def __init__(self, **kwargs: Any) -> None: self._keyvault_client_configs = kwargs.pop("keyvault_client_configs", {}) async def resolve_keyvault_reference(self, config: SecretReferenceConfigurationSetting) -> str: - # pylint:disable=protected-access if config.key in self._secret_cache: return self._secret_cache[config.key] - if not self.uses_key_vault: - raise ValueError( - """ - Either a credential to Key Vault, custom Key Vault client, or a secret resolver must be set to resolve - Key Vault references. - """ - ) - - if config.secret_id is None: - raise ValueError("Key Vault reference must have a uri value.") - - keyvault_identifier = KeyVaultSecretIdentifier(config.secret_id) - - vault_url = keyvault_identifier.vault_url + "/" + keyvault_identifier, vault_url = self.resolve_keyvault_reference_base(config) # pylint:disable=protected-access referenced_client = self._secret_clients.get(vault_url, None) @@ -51,25 +37,25 @@ async def resolve_keyvault_reference(self, config: SecretReferenceConfigurationS referenced_client = SecretClient(vault_url=vault_url, credential=credential, **vault_config) self._secret_clients[vault_url] = referenced_client + secret_value = None + if referenced_client: secret_value = ( await referenced_client.get_secret(keyvault_identifier.name, version=keyvault_identifier.version) ).value - if secret_value is not None: - self._secret_cache[config.key] = secret_value - return secret_value - if self._secret_resolver: - resolved = self._secret_resolver(config.secret_id) - try: + if self._secret_resolver and secret_value is None: + result = self._secret_resolver(config.secret_id) + if inspect.isawaitable(result): # Secret resolver was async - value = await resolved - self._secret_cache[config.key] = value - return value - except TypeError: + secret_value = await result + else: # Secret resolver was sync - self._secret_cache[config.key] = resolved - return resolved + secret_value = result + + if secret_value: + self._secret_cache[config.key] = secret_value + return secret_value raise ValueError("No Secret Client found for Key Vault reference %s" % (vault_url)) From b16096302aa7301e619776d3557a748dc64535ea Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 28 Aug 2025 13:42:30 -0700 Subject: [PATCH 22/43] Review comments --- .../_azureappconfigurationprovider.py | 119 ++-- .../provider/_secret_provider.py | 22 +- .../provider/_secret_provider_base.py | 1 + .../provider/aio/_async_secret_provider.py | 30 +- .../tests/test_secret_provider.py | 508 ++++++++++++++++++ 5 files changed, 601 insertions(+), 79 deletions(-) create mode 100644 sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider.py diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py index 9e79d612bb40..b9685ce49fbe 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -343,55 +343,74 @@ def _common_refresh( if self._on_refresh_success: self._on_refresh_success() - def _refresh_configuration_settings(self, **kwargs: Any) -> None: + def _refresh_operation_configuration(self, client, headers, **inner_kwargs): + configuration_settings: Optional[List[ConfigurationSetting]] = None + need_refresh = False + reset_secret_timer = False - def refresh_operation(client, headers, **inner_kwargs): - configuration_settings: Optional[List[ConfigurationSetting]] = None - need_refresh = False - reset_secret_timer = False + if ( + self._secret_provider.secret_refresh_timer + and self._secret_provider.secret_refresh_timer.needs_refresh() + ): + self._secret_provider.bust_cache() + reset_secret_timer = True + need_refresh = True - if ( - self._secret_provider.secret_refresh_timer - and self._secret_provider.secret_refresh_timer.needs_refresh() - ): - self._secret_provider.bust_cache() - reset_secret_timer = True - need_refresh = True + if not need_refresh: + need_refresh, self._refresh_on, configuration_settings = client.refresh_configuration_settings( + self._selects, self._refresh_on, headers=headers, **inner_kwargs + ) + else: + # Force a refresh to make sure secrets are up to date + configuration_settings, self._refresh_on = client.load_configuration_settings( + self._selects, self._refresh_on, headers=headers, **inner_kwargs + ) - if not need_refresh: - need_refresh, self._refresh_on, configuration_settings = client.refresh_configuration_settings( - self._selects, self._refresh_on, headers=headers, **inner_kwargs - ) - else: - # Force a refresh to make sure secrets are up to date - configuration_settings, self._refresh_on = client.load_configuration_settings( - self._selects, self._refresh_on, headers=headers, **inner_kwargs - ) + configuration_settings_processed: Dict[str, Any] = {} - configuration_settings_processed: Dict[str, Any] = {} + if configuration_settings is not None: + configuration_settings_processed = self._process_configurations(configuration_settings) - if configuration_settings is not None: - configuration_settings_processed = self._process_configurations(configuration_settings) + if need_refresh: + feature_flags = [] + uses_feature_flags = False + if self._dict.get(FEATURE_MANAGEMENT_KEY, {}).get(FEATURE_FLAG_KEY): + uses_feature_flags = True + feature_flags = self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] + self._dict = configuration_settings_processed + if uses_feature_flags: + # If feature flags were already loaded, we need to keep them + self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags - if need_refresh: - feature_flags = [] - uses_feature_flags = False - if self._dict.get(FEATURE_MANAGEMENT_KEY, {}).get(FEATURE_FLAG_KEY): - uses_feature_flags = True - feature_flags = self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] - self._dict = configuration_settings_processed - if uses_feature_flags: - # If feature flags were already loaded, we need to keep them - self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags + self._refresh_timer.reset() + if reset_secret_timer and self._secret_provider.secret_refresh_timer: + self._secret_provider.secret_refresh_timer.reset() - self._refresh_timer.reset() - if reset_secret_timer and self._secret_provider.secret_refresh_timer: - self._secret_provider.secret_refresh_timer.reset() + return True - return True + def _refresh_operation_feature_flags(self, client, headers, **inner_kwargs): + need_ff_refresh, refresh_on_feature_flags, feature_flags, filters_used = client.refresh_feature_flags( + self._refresh_on_feature_flags, + self._feature_flag_selectors, + headers, + self._origin_endpoint or "", + **inner_kwargs, + ) + + if refresh_on_feature_flags: + self._refresh_on_feature_flags = refresh_on_feature_flags + self._feature_filter_usage = filters_used + + if need_ff_refresh: + self._dict[FEATURE_MANAGEMENT_KEY] = {} + self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags + self._feature_flag_refresh_timer.reset() + return True + + def _refresh_configuration_settings(self, **kwargs: Any) -> None: self._common_refresh( - refresh_operation=refresh_operation, + refresh_operation=self._refresh_operation_configuration, error_log_message="Failed to refresh configurations from endpoint %s", timer=self._refresh_timer, refresh_condition=bool(self._refresh_on), @@ -401,28 +420,8 @@ def refresh_operation(client, headers, **inner_kwargs): def _refresh_feature_flags(self, **kwargs) -> None: # pylint: disable=too-many-statements """Refresh feature flags from Azure App Configuration.""" - def refresh_operation(client, headers, **inner_kwargs): - need_ff_refresh, refresh_on_feature_flags, feature_flags, filters_used = client.refresh_feature_flags( - self._refresh_on_feature_flags, - self._feature_flag_selectors, - headers, - self._origin_endpoint or "", - **inner_kwargs, - ) - - if refresh_on_feature_flags: - self._refresh_on_feature_flags = refresh_on_feature_flags - self._feature_filter_usage = filters_used - - if need_ff_refresh: - self._dict[FEATURE_MANAGEMENT_KEY] = {} - self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags - - self._feature_flag_refresh_timer.reset() - return True - self._common_refresh( - refresh_operation=refresh_operation, + refresh_operation=self._refresh_operation_feature_flags, error_log_message="Failed to refresh feature flags from endpoint %s", timer=self._feature_flag_refresh_timer, refresh_condition=self._feature_flag_refresh_enabled, diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py index 2537320a5bbc..066b5e908d6d 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py @@ -6,6 +6,7 @@ from typing import Mapping, Any, TypeVar, Dict from azure.appconfiguration import SecretReferenceConfigurationSetting # type:ignore # pylint:disable=no-name-in-module from azure.keyvault.secrets import SecretClient +from azure.core.exceptions import ServiceRequestError from ._secret_provider_base import _SecretProviderBase JSON = Mapping[str, Any] @@ -22,9 +23,11 @@ def __init__(self, **kwargs: Any) -> None: self._keyvault_client_configs = kwargs.pop("keyvault_client_configs", {}) def resolve_keyvault_reference(self, config: SecretReferenceConfigurationSetting) -> str: - if config.key in self._secret_cache: - return self._secret_cache[config.key] keyvault_identifier, vault_url = self.resolve_keyvault_reference_base(config) + if keyvault_identifier.source_id in self._secret_cache: + return self._secret_cache[keyvault_identifier.source_id] + elif keyvault_identifier.source_id in self._secret_version_cache: + return self._secret_version_cache[keyvault_identifier.source_id] # pylint:disable=protected-access referenced_client = self._secret_clients.get(vault_url, None) @@ -39,15 +42,20 @@ def resolve_keyvault_reference(self, config: SecretReferenceConfigurationSetting secret_value = None if referenced_client: - secret_value = referenced_client.get_secret( - keyvault_identifier.name, version=keyvault_identifier.version - ).value + try: + secret_value = referenced_client.get_secret( + keyvault_identifier.name, version=keyvault_identifier.version + ).value + except ServiceRequestError as e: + raise ValueError("Failed to retrieve secret from Key Vault") from e if self._secret_resolver and secret_value is None: secret_value = self._secret_resolver(config.secret_id) - if secret_value: - self._secret_cache[config.key] = secret_value + if keyvault_identifier.version: + self._secret_version_cache[keyvault_identifier.source_id] = keyvault_identifier.version + else: + self._secret_cache[keyvault_identifier.source_id] = secret_value return secret_value raise ValueError("No Secret Client found for Key Vault reference %s" % (vault_url)) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider_base.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider_base.py index 02f3603977df..8b7c06a82cf0 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider_base.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider_base.py @@ -29,6 +29,7 @@ class _SecretProviderBase(Mapping[str, Union[str, JSON]]): def __init__(self, **kwargs: Any) -> None: self._secret_cache: Dict[str, Any] = {} + self._secret_version_cache: Dict[str, str] = {} self.uses_key_vault = ( "keyvault_credential" in kwargs or ("keyvault_client_configs" in kwargs and len(kwargs.get("keyvault_client_configs", {})) > 0) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py index 70c90c1449bf..871bf6d38717 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py @@ -7,6 +7,7 @@ from typing import Mapping, Any, TypeVar, Dict from azure.appconfiguration import SecretReferenceConfigurationSetting # type:ignore # pylint:disable=no-name-in-module from azure.keyvault.secrets.aio import SecretClient +from azure.core.exceptions import ServiceRequestError from .._secret_provider_base import _SecretProviderBase JSON = Mapping[str, Any] @@ -23,9 +24,11 @@ def __init__(self, **kwargs: Any) -> None: self._keyvault_client_configs = kwargs.pop("keyvault_client_configs", {}) async def resolve_keyvault_reference(self, config: SecretReferenceConfigurationSetting) -> str: - if config.key in self._secret_cache: - return self._secret_cache[config.key] keyvault_identifier, vault_url = self.resolve_keyvault_reference_base(config) + if keyvault_identifier.source_id in self._secret_cache: + return self._secret_cache[keyvault_identifier.source_id] + elif keyvault_identifier.source_id in self._secret_version_cache: + return self._secret_version_cache[keyvault_identifier.source_id] # pylint:disable=protected-access referenced_client = self._secret_clients.get(vault_url, None) @@ -40,21 +43,24 @@ async def resolve_keyvault_reference(self, config: SecretReferenceConfigurationS secret_value = None if referenced_client: - secret_value = ( - await referenced_client.get_secret(keyvault_identifier.name, version=keyvault_identifier.version) - ).value + try: + secret_value = ( + await referenced_client.get_secret(keyvault_identifier.name, version=keyvault_identifier.version) + ).value + except ServiceRequestError as e: + raise ValueError("Failed to retrieve secret from Key Vault") from e if self._secret_resolver and secret_value is None: - result = self._secret_resolver(config.secret_id) - if inspect.isawaitable(result): + secret_value = self._secret_resolver(config.secret_id) + if inspect.isawaitable(secret_value): # Secret resolver was async - secret_value = await result - else: - # Secret resolver was sync - secret_value = result + secret_value = await secret_value if secret_value: - self._secret_cache[config.key] = secret_value + if keyvault_identifier.version: + self._secret_version_cache[keyvault_identifier.source_id] = keyvault_identifier.version + else: + self._secret_cache[keyvault_identifier.source_id] = secret_value return secret_value raise ValueError("No Secret Client found for Key Vault reference %s" % (vault_url)) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider.py new file mode 100644 index 000000000000..2a8905ca3ae3 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider.py @@ -0,0 +1,508 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import unittest +from unittest.mock import Mock, patch, MagicMock +from azure.appconfiguration import SecretReferenceConfigurationSetting +from azure.appconfiguration.provider._secret_provider import SecretProvider +from azure.keyvault.secrets import SecretClient, KeyVaultSecret +from devtools_testutils import recorded_by_proxy +from preparers import app_config_decorator_aad +from testcase import AppConfigTestCase + + +class TestSecretProvider(AppConfigTestCase, unittest.TestCase): + + def test_init_with_defaults(self): + """Test initialization of SecretProvider with default parameters.""" + secret_provider = SecretProvider() + + # Verify initialization with defaults + self.assertEqual(len(secret_provider._secret_clients), 0) + self.assertIsNone(secret_provider._keyvault_credential) + self.assertIsNone(secret_provider._secret_resolver) + self.assertEqual(secret_provider._keyvault_client_configs, {}) + self.assertFalse(secret_provider.uses_key_vault) + self.assertIsNone(secret_provider.secret_refresh_timer) + self.assertEqual(len(secret_provider._secret_cache), 0) + + def test_init_with_keyvault_credential(self): + """Test initialization with a Key Vault credential.""" + mock_credential = Mock() + secret_provider = SecretProvider(keyvault_credential=mock_credential) + + # Verify initialization with a Key Vault credential + self.assertEqual(len(secret_provider._secret_clients), 0) + self.assertEqual(secret_provider._keyvault_credential, mock_credential) + self.assertIsNone(secret_provider._secret_resolver) + self.assertEqual(secret_provider._keyvault_client_configs, {}) + self.assertTrue(secret_provider.uses_key_vault) + + def test_init_with_secret_resolver(self): + """Test initialization with a secret resolver.""" + mock_resolver = Mock() + secret_provider = SecretProvider(secret_resolver=mock_resolver) + + # Verify initialization with a secret resolver + self.assertEqual(len(secret_provider._secret_clients), 0) + self.assertIsNone(secret_provider._keyvault_credential) + self.assertEqual(secret_provider._secret_resolver, mock_resolver) + self.assertEqual(secret_provider._keyvault_client_configs, {}) + self.assertTrue(secret_provider.uses_key_vault) + + def test_init_with_keyvault_client_configs(self): + """Test initialization with Key Vault client configurations.""" + client_configs = {"https://myvault.vault.azure.net/": {"retry_total": 3}} + secret_provider = SecretProvider(keyvault_client_configs=client_configs) + + # Verify initialization with Key Vault client configurations + self.assertEqual(len(secret_provider._secret_clients), 0) + self.assertIsNone(secret_provider._keyvault_credential) + self.assertIsNone(secret_provider._secret_resolver) + self.assertEqual(secret_provider._keyvault_client_configs, client_configs) + self.assertTrue(secret_provider.uses_key_vault) + + def test_init_with_secret_refresh_interval(self): + """Test initialization with a secret refresh interval.""" + mock_credential = Mock() + refresh_interval = 30 + secret_provider = SecretProvider( + keyvault_credential=mock_credential, + secret_refresh_interval=refresh_interval + ) + + # Verify initialization with a secret refresh interval + self.assertIsNotNone(secret_provider.secret_refresh_timer) + self.assertTrue(secret_provider.uses_key_vault) + + def test_resolve_keyvault_reference_with_cached_secret(self): + """Test resolving a Key Vault reference when the secret is in the cache.""" + # Create a mock Key Vault reference + secret_id = "https://myvault.vault.azure.net/secrets/mysecret/" + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + + # Create a SecretProvider with a mock credential + secret_provider = SecretProvider(keyvault_credential=Mock()) + + # Add to cache + secret_provider._secret_cache[secret_id] = "cached-secret-value" + + # This should return the cached value without calling SecretClient + result = secret_provider.resolve_keyvault_reference(config) + + # Verify the result + self.assertEqual(result, "cached-secret-value") + + def test_resolve_keyvault_reference_with_cached_secret_version(self): + """Test resolving a Key Vault reference when the secret is in the cache.""" + # Create a mock Key Vault reference + secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + + # Create a SecretProvider with a mock credential + secret_provider = SecretProvider(keyvault_credential=Mock()) + + # Add to cache + secret_provider._secret_version_cache[secret_id] = "cached-secret-value" + + # This should return the cached value without calling SecretClient + result = secret_provider.resolve_keyvault_reference(config) + + # Verify the result + self.assertEqual(result, "cached-secret-value") + + def test_resolve_keyvault_reference_with_existing_client(self): + """Test resolving a Key Vault reference with an existing client.""" + # Create a mock Key Vault reference + secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + + # Create a SecretProvider with a mock credential + mock_credential = Mock() + secret_provider = SecretProvider(keyvault_credential=mock_credential) + + # Create a mock SecretClient + mock_client = Mock() + mock_secret = Mock() + mock_secret.value = "secret-value" + mock_client.get_secret.return_value = mock_secret + + # Add the mock client to the secret_clients dictionary + vault_url = "https://myvault.vault.azure.net/" + secret_provider._secret_clients[vault_url] = mock_client + + # Setup key vault identifier mock + with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + mock_id_instance = Mock() + mock_id_instance._resource_id = secret_id + mock_id_instance.source_id = secret_id + mock_id_instance.name = "mysecret" + mock_id_instance.version = "12345" + mock_id_instance.vault_url = "https://myvault.vault.azure.net" + mock_kv_id.return_value = mock_id_instance + + # Call resolve_keyvault_reference + with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + mock_base.return_value = (mock_id_instance, vault_url) + + result = secret_provider.resolve_keyvault_reference(config) + + # Verify the result + self.assertEqual(result, "secret-value") + mock_client.get_secret.assert_called_once_with( + mock_id_instance.name, + version=mock_id_instance.version + ) + # Verify the secret was cached + self.assertEqual(secret_provider._secret_cache[secret_id], "secret-value") + + def test_resolve_keyvault_reference_with_new_client(self): + """Test resolving a Key Vault reference by creating a new client.""" + # Create a mock Key Vault reference + secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + + # Create a SecretProvider with a mock credential + mock_credential = Mock() + secret_provider = SecretProvider(keyvault_credential=mock_credential) + + # Setup key vault identifier mock + with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + mock_id_instance = Mock() + mock_id_instance._resource_id = secret_id + mock_id_instance.source_id = secret_id + mock_id_instance.name = "mysecret" + mock_id_instance.version = "12345" + mock_id_instance.vault_url = "https://myvault.vault.azure.net" + mock_kv_id.return_value = mock_id_instance + + # Mock SecretClient creation and get_secret method + with patch('azure.appconfiguration.provider._secret_provider.SecretClient') as mock_client_class: + mock_client = Mock() + mock_secret = Mock() + mock_secret.value = "new-secret-value" + mock_client.get_secret.return_value = mock_secret + mock_client_class.return_value = mock_client + + # Call resolve_keyvault_reference + with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + vault_url = "https://myvault.vault.azure.net/" + mock_base.return_value = (mock_id_instance, vault_url) + + result = secret_provider.resolve_keyvault_reference(config) + + # Verify the result + self.assertEqual(result, "new-secret-value") + mock_client_class.assert_called_once_with( + vault_url=vault_url, + credential=mock_credential + ) + mock_client.get_secret.assert_called_once_with( + mock_id_instance.name, + version=mock_id_instance.version + ) + # Verify the client was cached + self.assertEqual(secret_provider._secret_clients[vault_url], mock_client) + # Verify the secret was cached + self.assertEqual(secret_provider._secret_cache[secret_id], "new-secret-value") + + def test_resolve_keyvault_reference_with_secret_resolver(self): + """Test resolving a Key Vault reference using a secret resolver.""" + # Create a mock Key Vault reference + secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + + # Create a mock secret resolver + mock_resolver = Mock(return_value="resolved-secret-value") + + # Create a SecretProvider with the mock resolver + secret_provider = SecretProvider(secret_resolver=mock_resolver) + + # Setup key vault identifier mock + with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + mock_id_instance = Mock() + mock_id_instance._resource_id = secret_id + mock_id_instance.source_id = secret_id + mock_id_instance.name = "mysecret" + mock_id_instance.version = "12345" + mock_id_instance.vault_url = "https://myvault.vault.azure.net" + mock_kv_id.return_value = mock_id_instance + + # Call resolve_keyvault_reference + with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + vault_url = "https://myvault.vault.azure.net/" + mock_base.return_value = (mock_id_instance, vault_url) + + result = secret_provider.resolve_keyvault_reference(config) + + # Verify the result + self.assertEqual(result, "resolved-secret-value") + mock_resolver.assert_called_once_with(secret_id) + # Verify the secret was cached + self.assertEqual(secret_provider._secret_cache[secret_id], "resolved-secret-value") + + def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self): + """Test falling back to a secret resolver if the client fails to get the secret.""" + # Create a mock Key Vault reference + secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + + # Create a mock credential and secret resolver + mock_credential = Mock() + mock_resolver = Mock(return_value="fallback-secret-value") + + # Create a SecretProvider with both credential and resolver + secret_provider = SecretProvider( + keyvault_credential=mock_credential, + secret_resolver=mock_resolver + ) + + # Create a mock SecretClient that returns None for get_secret + mock_client = Mock() + mock_client.get_secret.return_value.value = None + + # Add the mock client to the secret_clients dictionary + vault_url = "https://myvault.vault.azure.net/" + secret_provider._secret_clients[vault_url] = mock_client + + # Setup key vault identifier mock + with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + mock_id_instance = Mock() + mock_id_instance._resource_id = secret_id + mock_id_instance.source_id = secret_id + mock_id_instance.name = "mysecret" + mock_id_instance.version = "12345" + mock_id_instance.vault_url = "https://myvault.vault.azure.net" + mock_kv_id.return_value = mock_id_instance + + # Call resolve_keyvault_reference + with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + mock_base.return_value = (mock_id_instance, vault_url) + + result = secret_provider.resolve_keyvault_reference(config) + + # Verify the result + self.assertEqual(result, "fallback-secret-value") + mock_client.get_secret.assert_called_once_with( + mock_id_instance.name, + version=mock_id_instance.version + ) + mock_resolver.assert_called_once_with(secret_id) + # Verify the secret was cached + self.assertEqual(secret_provider._secret_cache[secret_id], "fallback-secret-value") + + def test_resolve_keyvault_reference_no_client_no_resolver(self): + """Test that an error is raised when no client or resolver can resolve the reference.""" + # Create a mock Key Vault reference + secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + + # Create a SecretProvider with a credential but no clients or resolvers + mock_credential = Mock() + secret_provider = SecretProvider(keyvault_credential=mock_credential) + + # Setup key vault identifier mock + with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + mock_id_instance = Mock() + mock_id_instance._resource_id = secret_id + mock_id_instance.source_id = secret_id + mock_id_instance.name = "mysecret" + mock_id_instance.version = "12345" + mock_id_instance.vault_url = "https://myvault.vault.azure.net" + mock_kv_id.return_value = mock_id_instance + + # Call resolve_keyvault_reference + with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + mock_base.return_value = (mock_id_instance, "https://othervault.vault.azure.net/") + + # This should raise an error since we have no client for this vault URL + with self.assertRaises(ValueError): + secret_provider.resolve_keyvault_reference(config) + + def test_close(self): + """Test closing the SecretProvider.""" + # Create a SecretProvider with mock clients + secret_provider = SecretProvider() + + # Create mock clients + mock_client1 = Mock() + mock_client2 = Mock() + + # Add the mock clients to the secret_clients dictionary + secret_provider._secret_clients = { + "https://vault1.vault.azure.net/": mock_client1, + "https://vault2.vault.azure.net/": mock_client2 + } + + # Call close + secret_provider.close() + + # Verify both clients were closed + mock_client1.close.assert_called_once() + mock_client2.close.assert_called_once() + + def test_client_config_specific_credential(self): + """Test that client configuration can specify a specific credential.""" + # Create a mock Key Vault reference + secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + + # Create mock credentials + mock_default_credential = Mock(name="default_credential") + mock_specific_credential = Mock(name="specific_credential") + + # Create client configs with a specific credential + client_configs = { + "https://myvault.vault.azure.net/": { + "credential": mock_specific_credential, + "retry_total": 3 + } + } + + # Create a SecretProvider with default credential and client configs + secret_provider = SecretProvider( + keyvault_credential=mock_default_credential, + keyvault_client_configs=client_configs + ) + + # Setup key vault identifier mock + with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + mock_id_instance = Mock() + mock_id_instance._resource_id = secret_id + mock_id_instance.source_id = secret_id + mock_id_instance.name = "mysecret" + mock_id_instance.version = "12345" + mock_id_instance.vault_url = "https://myvault.vault.azure.net" + mock_kv_id.return_value = mock_id_instance + + # Mock SecretClient creation and get_secret method + with patch('azure.appconfiguration.provider._secret_provider.SecretClient') as mock_client_class: + mock_client = Mock() + mock_secret = Mock() + mock_secret.value = "secret-value" + mock_client.get_secret.return_value = mock_secret + mock_client_class.return_value = mock_client + + # Call resolve_keyvault_reference + with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + vault_url = "https://myvault.vault.azure.net/" + mock_base.return_value = (mock_id_instance, vault_url) + + result = secret_provider.resolve_keyvault_reference(config) + + # Verify the specific credential was used instead of the default + mock_client_class.assert_called_once_with( + vault_url=vault_url, + credential=mock_specific_credential, + retry_total=3 + ) + # Verify the result + self.assertEqual(result, "secret-value") + + def test_mapping_interface(self): + """Test that the SecretProvider implements the Mapping interface.""" + # Create a SecretProvider + secret_provider = SecretProvider() + + # Add some items to the cache + secret_provider._secret_cache = { + "key1": "value1", + "key2": "value2", + "key3": "value3" + } + + # Test __getitem__ + self.assertEqual(secret_provider["key1"], "value1") + self.assertEqual(secret_provider["key2"], "value2") + self.assertEqual(secret_provider["key3"], "value3") + + # Test __len__ + self.assertEqual(len(secret_provider), 3) + + # Test __iter__ + keys = list(secret_provider) + self.assertEqual(sorted(keys), ["key1", "key2", "key3"]) + + # Test __contains__ + self.assertIn("key1", secret_provider) + self.assertIn("key2", secret_provider) + self.assertIn("key3", secret_provider) + self.assertNotIn("key4", secret_provider) + + # Test keys + self.assertEqual(set(secret_provider.keys()), {"key1", "key2", "key3"}) + + # Test items + items = dict(secret_provider.items()) + self.assertEqual(items, {"key1": "value1", "key2": "value2", "key3": "value3"}) + + # Test values + values = list(secret_provider.values()) + # Sort the expected values instead since values may not be comparable + self.assertEqual(set(values), {"value1", "value2", "value3"}) + + # Test get + self.assertEqual(secret_provider.get("key1"), "value1") + self.assertEqual(secret_provider.get("key4"), None) + self.assertEqual(secret_provider.get("key4", "default"), "default") + + def test_bust_cache(self): + """Test that bust_cache clears the secret cache.""" + # Create a SecretProvider + secret_provider = SecretProvider() + + # Add some items to the cache + secret_provider._secret_cache = { + "key1": "value1", + "key2": "value2", + "key3": "value3" + } + + # Verify the cache has items + self.assertEqual(len(secret_provider._secret_cache), 3) + + # Call bust_cache + secret_provider.bust_cache() + + # Verify the cache is empty + self.assertEqual(len(secret_provider._secret_cache), 0) + self.assertEqual(secret_provider._secret_cache, {}) + + @recorded_by_proxy + @app_config_decorator_aad + def test_integration_with_keyvault( + self, + appconfiguration_endpoint_string, + appconfiguration_keyvault_secret_url + ): + """Test integration with Key Vault.""" + if not appconfiguration_keyvault_secret_url: + self.skipTest("No Key Vault secret URL provided") + + # Get a credential + credential = self.get_credential(SecretClient) + + # Create a SecretProvider with the credential + secret_provider = SecretProvider(keyvault_credential=credential) + + # Create a Key Vault reference + config = SecretReferenceConfigurationSetting( + key="test-key", + secret_id=appconfiguration_keyvault_secret_url + ) + + # Resolve the reference + secret_value = secret_provider.resolve_keyvault_reference(config) + + # Verify a value was returned (we can't know the exact value) + self.assertIsNotNone(secret_value) + self.assertTrue(isinstance(secret_value, str)) + + # Verify the secret was cached + self.assertIn(appconfiguration_keyvault_secret_url, secret_provider._secret_cache) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 2cdff6dfeebb2cf8e76e9e3c4f500f54ca245f4e Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 29 Aug 2025 09:34:37 -0700 Subject: [PATCH 23/43] fixing tests --- .../provider/_secret_provider.py | 2 +- .../provider/aio/_async_secret_provider.py | 2 +- .../tests/test_secret_provider.py | 8 +- .../tests/test_secret_provider_async.py | 552 ++++++++++++++++++ 4 files changed, 558 insertions(+), 6 deletions(-) create mode 100644 sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider_async.py diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py index 066b5e908d6d..2a394f01aac7 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py @@ -53,7 +53,7 @@ def resolve_keyvault_reference(self, config: SecretReferenceConfigurationSetting secret_value = self._secret_resolver(config.secret_id) if secret_value: if keyvault_identifier.version: - self._secret_version_cache[keyvault_identifier.source_id] = keyvault_identifier.version + self._secret_version_cache[keyvault_identifier.source_id] = secret_value else: self._secret_cache[keyvault_identifier.source_id] = secret_value return secret_value diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py index 871bf6d38717..86fa7df8f044 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py @@ -58,7 +58,7 @@ async def resolve_keyvault_reference(self, config: SecretReferenceConfigurationS if secret_value: if keyvault_identifier.version: - self._secret_version_cache[keyvault_identifier.source_id] = keyvault_identifier.version + self._secret_version_cache[keyvault_identifier.source_id] = secret_value else: self._secret_cache[keyvault_identifier.source_id] = secret_value return secret_value diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider.py index 2a8905ca3ae3..97c3939852bc 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider.py @@ -156,7 +156,7 @@ def test_resolve_keyvault_reference_with_existing_client(self): version=mock_id_instance.version ) # Verify the secret was cached - self.assertEqual(secret_provider._secret_cache[secret_id], "secret-value") + self.assertEqual(secret_provider._secret_version_cache[secret_id], "secret-value") def test_resolve_keyvault_reference_with_new_client(self): """Test resolving a Key Vault reference by creating a new client.""" @@ -206,7 +206,7 @@ def test_resolve_keyvault_reference_with_new_client(self): # Verify the client was cached self.assertEqual(secret_provider._secret_clients[vault_url], mock_client) # Verify the secret was cached - self.assertEqual(secret_provider._secret_cache[secret_id], "new-secret-value") + self.assertEqual(secret_provider._secret_version_cache[secret_id], "new-secret-value") def test_resolve_keyvault_reference_with_secret_resolver(self): """Test resolving a Key Vault reference using a secret resolver.""" @@ -241,7 +241,7 @@ def test_resolve_keyvault_reference_with_secret_resolver(self): self.assertEqual(result, "resolved-secret-value") mock_resolver.assert_called_once_with(secret_id) # Verify the secret was cached - self.assertEqual(secret_provider._secret_cache[secret_id], "resolved-secret-value") + self.assertEqual(secret_provider._secret_version_cache[secret_id], "resolved-secret-value") def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self): """Test falling back to a secret resolver if the client fails to get the secret.""" @@ -291,7 +291,7 @@ def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self): ) mock_resolver.assert_called_once_with(secret_id) # Verify the secret was cached - self.assertEqual(secret_provider._secret_cache[secret_id], "fallback-secret-value") + self.assertEqual(secret_provider._secret_version_cache[secret_id], "fallback-secret-value") def test_resolve_keyvault_reference_no_client_no_resolver(self): """Test that an error is raised when no client or resolver can resolve the reference.""" diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider_async.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider_async.py new file mode 100644 index 000000000000..d21e9bdb3e78 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider_async.py @@ -0,0 +1,552 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import unittest +from unittest.mock import Mock, patch, MagicMock, AsyncMock +import asyncio +import pytest +from azure.appconfiguration import SecretReferenceConfigurationSetting +from azure.appconfiguration.provider.aio._async_secret_provider import SecretProvider +from azure.keyvault.secrets.aio import SecretClient +from azure.core.exceptions import ServiceRequestError +from devtools_testutils import recorded_by_proxy +from preparers import app_config_decorator_aad +from testcase import AppConfigTestCase + + +class TestSecretProviderAsync(AppConfigTestCase, unittest.IsolatedAsyncioTestCase): + + async def test_init_with_defaults(self): + """Test initialization of SecretProvider with default parameters.""" + secret_provider = SecretProvider() + + # Verify initialization with defaults + self.assertEqual(len(secret_provider._secret_clients), 0) + self.assertIsNone(secret_provider._keyvault_credential) + self.assertIsNone(secret_provider._secret_resolver) + self.assertEqual(secret_provider._keyvault_client_configs, {}) + self.assertFalse(secret_provider.uses_key_vault) + self.assertIsNone(secret_provider.secret_refresh_timer) + self.assertEqual(len(secret_provider._secret_cache), 0) + + async def test_init_with_keyvault_credential(self): + """Test initialization with a Key Vault credential.""" + mock_credential = Mock() + secret_provider = SecretProvider(keyvault_credential=mock_credential) + + # Verify initialization with a Key Vault credential + self.assertEqual(len(secret_provider._secret_clients), 0) + self.assertEqual(secret_provider._keyvault_credential, mock_credential) + self.assertIsNone(secret_provider._secret_resolver) + self.assertEqual(secret_provider._keyvault_client_configs, {}) + self.assertTrue(secret_provider.uses_key_vault) + + async def test_init_with_secret_resolver(self): + """Test initialization with a secret resolver.""" + mock_resolver = Mock() + secret_provider = SecretProvider(secret_resolver=mock_resolver) + + # Verify initialization with a secret resolver + self.assertEqual(len(secret_provider._secret_clients), 0) + self.assertIsNone(secret_provider._keyvault_credential) + self.assertEqual(secret_provider._secret_resolver, mock_resolver) + self.assertEqual(secret_provider._keyvault_client_configs, {}) + self.assertTrue(secret_provider.uses_key_vault) + + async def test_init_with_keyvault_client_configs(self): + """Test initialization with Key Vault client configurations.""" + client_configs = {"https://myvault.vault.azure.net/": {"retry_total": 3}} + secret_provider = SecretProvider(keyvault_client_configs=client_configs) + + # Verify initialization with Key Vault client configurations + self.assertEqual(len(secret_provider._secret_clients), 0) + self.assertIsNone(secret_provider._keyvault_credential) + self.assertIsNone(secret_provider._secret_resolver) + self.assertEqual(secret_provider._keyvault_client_configs, client_configs) + self.assertTrue(secret_provider.uses_key_vault) + + async def test_init_with_secret_refresh_interval(self): + """Test initialization with a secret refresh interval.""" + mock_credential = Mock() + refresh_interval = 30 + secret_provider = SecretProvider( + keyvault_credential=mock_credential, + secret_refresh_interval=refresh_interval + ) + + # Verify initialization with a secret refresh interval + self.assertIsNotNone(secret_provider.secret_refresh_timer) + self.assertTrue(secret_provider.uses_key_vault) + + async def test_resolve_keyvault_reference_with_cached_secret(self): + """Test resolving a Key Vault reference when the secret is in the cache.""" + # Create a mock Key Vault reference + secret_id = "https://myvault.vault.azure.net/secrets/mysecret/" + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + + # Create a SecretProvider with a mock credential + secret_provider = SecretProvider(keyvault_credential=Mock()) + + # Add to cache + secret_provider._secret_cache[secret_id] = "cached-secret-value" + + # This should return the cached value without calling SecretClient + result = await secret_provider.resolve_keyvault_reference(config) + + # Verify the result + self.assertEqual(result, "cached-secret-value") + + async def test_resolve_keyvault_reference_with_cached_secret_version(self): + """Test resolving a Key Vault reference when the secret is in the cache.""" + # Create a mock Key Vault reference + secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + + # Create a SecretProvider with a mock credential + secret_provider = SecretProvider(keyvault_credential=Mock()) + + # Add to cache + secret_provider._secret_version_cache[secret_id] = "cached-secret-value" + + # This should return the cached value without calling SecretClient + result = await secret_provider.resolve_keyvault_reference(config) + + # Verify the result + self.assertEqual(result, "cached-secret-value") + + async def test_resolve_keyvault_reference_with_existing_client(self): + """Test resolving a Key Vault reference with an existing client.""" + # Create a mock Key Vault reference + secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + + # Create a SecretProvider with a mock credential + mock_credential = Mock() + secret_provider = SecretProvider(keyvault_credential=mock_credential) + + # Create a mock SecretClient + mock_client = Mock() + mock_secret = Mock() + mock_secret.value = "secret-value" + # Set up the get_secret method to return an awaitable that resolves to mock_secret + mock_client.get_secret = AsyncMock(return_value=mock_secret) + + # Add the mock client to the secret_clients dictionary + vault_url = "https://myvault.vault.azure.net/" + secret_provider._secret_clients[vault_url] = mock_client + + # Setup key vault identifier mock + with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + mock_id_instance = Mock() + mock_id_instance._resource_id = secret_id + mock_id_instance.source_id = secret_id + mock_id_instance.name = "mysecret" + mock_id_instance.version = "12345" + mock_id_instance.vault_url = "https://myvault.vault.azure.net" + mock_kv_id.return_value = mock_id_instance + + # Call resolve_keyvault_reference + with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + mock_base.return_value = (mock_id_instance, vault_url) + + result = await secret_provider.resolve_keyvault_reference(config) + + # Verify the result + self.assertEqual(result, "secret-value") + mock_client.get_secret.assert_called_once_with( + mock_id_instance.name, + version=mock_id_instance.version + ) + # Verify the secret was cached + self.assertEqual(secret_provider._secret_cache[secret_id], "secret-value") + + async def test_resolve_keyvault_reference_with_new_client(self): + """Test resolving a Key Vault reference by creating a new client.""" + # Create a mock Key Vault reference + secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + + # Create a SecretProvider with a mock credential + mock_credential = Mock() + secret_provider = SecretProvider(keyvault_credential=mock_credential) + + # Setup key vault identifier mock + with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + mock_id_instance = Mock() + mock_id_instance._resource_id = secret_id + mock_id_instance.source_id = secret_id + mock_id_instance.name = "mysecret" + mock_id_instance.version = "12345" + mock_id_instance.vault_url = "https://myvault.vault.azure.net" + mock_kv_id.return_value = mock_id_instance + + # Mock SecretClient creation and get_secret method + with patch('azure.appconfiguration.provider.aio._async_secret_provider.SecretClient') as mock_client_class: + mock_client = Mock() + mock_secret = Mock() + mock_secret.value = "new-secret-value" + mock_client.get_secret = AsyncMock(return_value=mock_secret) + mock_client_class.return_value = mock_client + + # Call resolve_keyvault_reference + with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + vault_url = "https://myvault.vault.azure.net/" + mock_base.return_value = (mock_id_instance, vault_url) + + result = await secret_provider.resolve_keyvault_reference(config) + + # Verify the result + self.assertEqual(result, "new-secret-value") + mock_client_class.assert_called_once_with( + vault_url=vault_url, + credential=mock_credential + ) + mock_client.get_secret.assert_called_once_with( + mock_id_instance.name, + version=mock_id_instance.version + ) + # Verify the client was cached + self.assertEqual(secret_provider._secret_clients[vault_url], mock_client) + # Verify the secret was cached + self.assertEqual(secret_provider._secret_cache[secret_id], "new-secret-value") + + async def test_resolve_keyvault_reference_with_secret_resolver(self): + """Test resolving a Key Vault reference using a secret resolver.""" + # Create a mock Key Vault reference + secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + + # Create a mock secret resolver + mock_resolver = Mock(return_value="resolved-secret-value") + + # Create a SecretProvider with the mock resolver + secret_provider = SecretProvider(secret_resolver=mock_resolver) + + # Setup key vault identifier mock + with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + mock_id_instance = Mock() + mock_id_instance._resource_id = secret_id + mock_id_instance.source_id = secret_id + mock_id_instance.name = "mysecret" + mock_id_instance.version = "12345" + mock_id_instance.vault_url = "https://myvault.vault.azure.net" + mock_kv_id.return_value = mock_id_instance + + # Call resolve_keyvault_reference + with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + vault_url = "https://myvault.vault.azure.net/" + mock_base.return_value = (mock_id_instance, vault_url) + + result = await secret_provider.resolve_keyvault_reference(config) + + # Verify the result + self.assertEqual(result, "resolved-secret-value") + mock_resolver.assert_called_once_with(secret_id) + # Verify the secret was cached + self.assertEqual(secret_provider._secret_cache[secret_id], "resolved-secret-value") + + async def test_resolve_keyvault_reference_with_async_secret_resolver(self): + """Test resolving a Key Vault reference using an async secret resolver.""" + # Create a mock Key Vault reference + secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + + # Create a mock async secret resolver + async def async_resolver(secret_id): + return "async-resolved-secret-value" + + # Create a SecretProvider with the mock resolver + secret_provider = SecretProvider(secret_resolver=async_resolver) + + # Setup key vault identifier mock + with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + mock_id_instance = Mock() + mock_id_instance._resource_id = secret_id + mock_id_instance.source_id = secret_id + mock_id_instance.name = "mysecret" + mock_id_instance.version = "12345" + mock_id_instance.vault_url = "https://myvault.vault.azure.net" + mock_kv_id.return_value = mock_id_instance + + # Call resolve_keyvault_reference + with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + vault_url = "https://myvault.vault.azure.net/" + mock_base.return_value = (mock_id_instance, vault_url) + + result = await secret_provider.resolve_keyvault_reference(config) + + # Verify the result + self.assertEqual(result, "async-resolved-secret-value") + # Verify the secret was cached + breakpoint() + self.assertEqual(secret_provider._secret_version_cache[secret_id], "async-resolved-secret-value") + + async def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self): + """Test falling back to a secret resolver if the client fails to get the secret.""" + # Create a mock Key Vault reference + secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + + # Create a mock credential and secret resolver + mock_credential = Mock() + mock_resolver = Mock(return_value="fallback-secret-value") + + # Create a SecretProvider with both credential and resolver + secret_provider = SecretProvider( + keyvault_credential=mock_credential, + secret_resolver=mock_resolver + ) + + # Create a mock SecretClient that returns None for get_secret + mock_client = Mock() + mock_secret = Mock() + mock_secret.value = None + mock_client.get_secret = AsyncMock(return_value=mock_secret) + + # Add the mock client to the secret_clients dictionary + vault_url = "https://myvault.vault.azure.net/" + secret_provider._secret_clients[vault_url] = mock_client + + # Setup key vault identifier mock + with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + mock_id_instance = Mock() + mock_id_instance._resource_id = secret_id + mock_id_instance.source_id = secret_id + mock_id_instance.name = "mysecret" + mock_id_instance.version = "12345" + mock_id_instance.vault_url = "https://myvault.vault.azure.net" + mock_kv_id.return_value = mock_id_instance + + # Call resolve_keyvault_reference + with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + mock_base.return_value = (mock_id_instance, vault_url) + + result = await secret_provider.resolve_keyvault_reference(config) + + # Verify the result + self.assertEqual(result, "fallback-secret-value") + mock_client.get_secret.assert_called_once_with( + mock_id_instance.name, + version=mock_id_instance.version + ) + mock_resolver.assert_called_once_with(secret_id) + # Verify the secret was cached + self.assertEqual(secret_provider._secret_cache[secret_id], "fallback-secret-value") + + async def test_resolve_keyvault_reference_no_client_no_resolver(self): + """Test that an error is raised when no client or resolver can resolve the reference.""" + # Create a mock Key Vault reference + secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + + # Create a SecretProvider with a credential but no clients or resolvers + mock_credential = Mock() + secret_provider = SecretProvider(keyvault_credential=mock_credential) + + # Setup key vault identifier mock + with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + mock_id_instance = Mock() + mock_id_instance._resource_id = secret_id + mock_id_instance.source_id = secret_id + mock_id_instance.name = "mysecret" + mock_id_instance.version = "12345" + mock_id_instance.vault_url = "https://myvault.vault.azure.net" + mock_kv_id.return_value = mock_id_instance + + # Call resolve_keyvault_reference + with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + mock_base.return_value = (mock_id_instance, "https://othervault.vault.azure.net/") + + # This should raise an error since we have no client for this vault URL + with self.assertRaises(ValueError): + await secret_provider.resolve_keyvault_reference(config) + + async def test_close(self): + """Test closing the SecretProvider.""" + # Create a SecretProvider with mock clients + secret_provider = SecretProvider() + + # Create mock clients + mock_client1 = Mock() + mock_client1.close = AsyncMock() + mock_client2 = Mock() + mock_client2.close = AsyncMock() + + # Add the mock clients to the secret_clients dictionary + secret_provider._secret_clients = { + "https://vault1.vault.azure.net/": mock_client1, + "https://vault2.vault.azure.net/": mock_client2 + } + + # Call close + await secret_provider.close() + + # Verify both clients were closed + mock_client1.close.assert_called_once() + mock_client2.close.assert_called_once() + + async def test_client_config_specific_credential(self): + """Test that client configuration can specify a specific credential.""" + # Create a mock Key Vault reference + secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + + # Create mock credentials + mock_default_credential = Mock(name="default_credential") + mock_specific_credential = Mock(name="specific_credential") + + # Create client configs with a specific credential + client_configs = { + "https://myvault.vault.azure.net/": { + "credential": mock_specific_credential, + "retry_total": 3 + } + } + + # Create a SecretProvider with default credential and client configs + secret_provider = SecretProvider( + keyvault_credential=mock_default_credential, + keyvault_client_configs=client_configs + ) + + # Setup key vault identifier mock + with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + mock_id_instance = Mock() + mock_id_instance._resource_id = secret_id + mock_id_instance.source_id = secret_id + mock_id_instance.name = "mysecret" + mock_id_instance.version = "12345" + mock_id_instance.vault_url = "https://myvault.vault.azure.net" + mock_kv_id.return_value = mock_id_instance + + # Mock SecretClient creation and get_secret method + with patch('azure.appconfiguration.provider.aio._async_secret_provider.SecretClient') as mock_client_class: + mock_client = Mock() + mock_secret = Mock() + mock_secret.value = "secret-value" + mock_client.get_secret = AsyncMock(return_value=mock_secret) + mock_client_class.return_value = mock_client + + # Call resolve_keyvault_reference + with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + vault_url = "https://myvault.vault.azure.net/" + mock_base.return_value = (mock_id_instance, vault_url) + + result = await secret_provider.resolve_keyvault_reference(config) + + # Verify the specific credential was used instead of the default + mock_client_class.assert_called_once_with( + vault_url=vault_url, + credential=mock_specific_credential, + retry_total=3 + ) + # Verify the result + self.assertEqual(result, "secret-value") + + async def test_mapping_interface(self): + """Test that the SecretProvider implements the Mapping interface.""" + # Create a SecretProvider + secret_provider = SecretProvider() + + # Add some items to the cache + secret_provider._secret_cache = { + "key1": "value1", + "key2": "value2", + "key3": "value3" + } + + # Test __getitem__ + self.assertEqual(secret_provider["key1"], "value1") + self.assertEqual(secret_provider["key2"], "value2") + self.assertEqual(secret_provider["key3"], "value3") + + # Test __len__ + self.assertEqual(len(secret_provider), 3) + + # Test __iter__ + keys = list(secret_provider) + self.assertEqual(sorted(keys), ["key1", "key2", "key3"]) + + # Test __contains__ + self.assertIn("key1", secret_provider) + self.assertIn("key2", secret_provider) + self.assertIn("key3", secret_provider) + self.assertNotIn("key4", secret_provider) + + # Test keys + self.assertEqual(set(secret_provider.keys()), {"key1", "key2", "key3"}) + + # Test items + items = dict(secret_provider.items()) + self.assertEqual(items, {"key1": "value1", "key2": "value2", "key3": "value3"}) + + # Test values + values = list(secret_provider.values()) + # Sort the expected values instead since values may not be comparable + self.assertEqual(set(values), {"value1", "value2", "value3"}) + + # Test get + self.assertEqual(secret_provider.get("key1"), "value1") + self.assertEqual(secret_provider.get("key4"), None) + self.assertEqual(secret_provider.get("key4", "default"), "default") + + async def test_bust_cache(self): + """Test that bust_cache clears the secret cache.""" + # Create a SecretProvider + secret_provider = SecretProvider() + + # Add some items to the cache + secret_provider._secret_cache = { + "key1": "value1", + "key2": "value2", + "key3": "value3" + } + + # Verify the cache has items + self.assertEqual(len(secret_provider._secret_cache), 3) + + # Call bust_cache + secret_provider.bust_cache() + + # Verify the cache is empty + self.assertEqual(len(secret_provider._secret_cache), 0) + self.assertEqual(secret_provider._secret_cache, {}) + + @recorded_by_proxy + @app_config_decorator_aad + async def test_integration_with_keyvault( + self, + appconfiguration_endpoint_string, + appconfiguration_keyvault_secret_url + ): + """Test integration with Key Vault.""" + if not appconfiguration_keyvault_secret_url: + self.skipTest("No Key Vault secret URL provided") + + # Get a credential + credential = self.get_credential(SecretClient) + + # Create a SecretProvider with the credential + secret_provider = SecretProvider(keyvault_credential=credential) + + # Create a Key Vault reference + config = SecretReferenceConfigurationSetting( + key="test-key", + secret_id=appconfiguration_keyvault_secret_url + ) + + # Resolve the reference + secret_value = await secret_provider.resolve_keyvault_reference(config) + + # Verify a value was returned (we can't know the exact value) + self.assertIsNotNone(secret_value) + self.assertTrue(isinstance(secret_value, str)) + + # Verify the secret was cached + self.assertIn(appconfiguration_keyvault_secret_url, secret_provider._secret_cache) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From f49340f1140a1da0d2c29efdc0ee5778151c85bd Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 29 Aug 2025 09:44:49 -0700 Subject: [PATCH 24/43] tox updates --- .../_azureappconfigurationprovider.py | 5 +- .../provider/_secret_provider.py | 2 +- .../provider/aio/_async_secret_provider.py | 2 +- .../tests/test_secret_provider.py | 257 +++++++---------- .../tests/test_secret_provider_async.py | 273 ++++++++---------- 5 files changed, 232 insertions(+), 307 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py index b9685ce49fbe..e5b6d8f13407 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -348,10 +348,7 @@ def _refresh_operation_configuration(self, client, headers, **inner_kwargs): need_refresh = False reset_secret_timer = False - if ( - self._secret_provider.secret_refresh_timer - and self._secret_provider.secret_refresh_timer.needs_refresh() - ): + if self._secret_provider.secret_refresh_timer and self._secret_provider.secret_refresh_timer.needs_refresh(): self._secret_provider.bust_cache() reset_secret_timer = True need_refresh = True diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py index 2a394f01aac7..9e078c1430ba 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py @@ -26,7 +26,7 @@ def resolve_keyvault_reference(self, config: SecretReferenceConfigurationSetting keyvault_identifier, vault_url = self.resolve_keyvault_reference_base(config) if keyvault_identifier.source_id in self._secret_cache: return self._secret_cache[keyvault_identifier.source_id] - elif keyvault_identifier.source_id in self._secret_version_cache: + if keyvault_identifier.source_id in self._secret_version_cache: return self._secret_version_cache[keyvault_identifier.source_id] # pylint:disable=protected-access diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py index 86fa7df8f044..7f35c85b9cab 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py @@ -27,7 +27,7 @@ async def resolve_keyvault_reference(self, config: SecretReferenceConfigurationS keyvault_identifier, vault_url = self.resolve_keyvault_reference_base(config) if keyvault_identifier.source_id in self._secret_cache: return self._secret_cache[keyvault_identifier.source_id] - elif keyvault_identifier.source_id in self._secret_version_cache: + if keyvault_identifier.source_id in self._secret_version_cache: return self._secret_version_cache[keyvault_identifier.source_id] # pylint:disable=protected-access diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider.py index 97c3939852bc..34fdb562e42d 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider.py @@ -18,7 +18,7 @@ class TestSecretProvider(AppConfigTestCase, unittest.TestCase): def test_init_with_defaults(self): """Test initialization of SecretProvider with default parameters.""" secret_provider = SecretProvider() - + # Verify initialization with defaults self.assertEqual(len(secret_provider._secret_clients), 0) self.assertIsNone(secret_provider._keyvault_credential) @@ -27,52 +27,49 @@ def test_init_with_defaults(self): self.assertFalse(secret_provider.uses_key_vault) self.assertIsNone(secret_provider.secret_refresh_timer) self.assertEqual(len(secret_provider._secret_cache), 0) - + def test_init_with_keyvault_credential(self): """Test initialization with a Key Vault credential.""" mock_credential = Mock() secret_provider = SecretProvider(keyvault_credential=mock_credential) - + # Verify initialization with a Key Vault credential self.assertEqual(len(secret_provider._secret_clients), 0) self.assertEqual(secret_provider._keyvault_credential, mock_credential) self.assertIsNone(secret_provider._secret_resolver) self.assertEqual(secret_provider._keyvault_client_configs, {}) self.assertTrue(secret_provider.uses_key_vault) - + def test_init_with_secret_resolver(self): """Test initialization with a secret resolver.""" mock_resolver = Mock() secret_provider = SecretProvider(secret_resolver=mock_resolver) - + # Verify initialization with a secret resolver self.assertEqual(len(secret_provider._secret_clients), 0) self.assertIsNone(secret_provider._keyvault_credential) self.assertEqual(secret_provider._secret_resolver, mock_resolver) self.assertEqual(secret_provider._keyvault_client_configs, {}) self.assertTrue(secret_provider.uses_key_vault) - + def test_init_with_keyvault_client_configs(self): """Test initialization with Key Vault client configurations.""" client_configs = {"https://myvault.vault.azure.net/": {"retry_total": 3}} secret_provider = SecretProvider(keyvault_client_configs=client_configs) - + # Verify initialization with Key Vault client configurations self.assertEqual(len(secret_provider._secret_clients), 0) self.assertIsNone(secret_provider._keyvault_credential) self.assertIsNone(secret_provider._secret_resolver) self.assertEqual(secret_provider._keyvault_client_configs, client_configs) self.assertTrue(secret_provider.uses_key_vault) - + def test_init_with_secret_refresh_interval(self): """Test initialization with a secret refresh interval.""" mock_credential = Mock() refresh_interval = 30 - secret_provider = SecretProvider( - keyvault_credential=mock_credential, - secret_refresh_interval=refresh_interval - ) - + secret_provider = SecretProvider(keyvault_credential=mock_credential, secret_refresh_interval=refresh_interval) + # Verify initialization with a secret refresh interval self.assertIsNotNone(secret_provider.secret_refresh_timer) self.assertTrue(secret_provider.uses_key_vault) @@ -82,7 +79,7 @@ def test_resolve_keyvault_reference_with_cached_secret(self): # Create a mock Key Vault reference secret_id = "https://myvault.vault.azure.net/secrets/mysecret/" config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) - + # Create a SecretProvider with a mock credential secret_provider = SecretProvider(keyvault_credential=Mock()) @@ -91,7 +88,7 @@ def test_resolve_keyvault_reference_with_cached_secret(self): # This should return the cached value without calling SecretClient result = secret_provider.resolve_keyvault_reference(config) - + # Verify the result self.assertEqual(result, "cached-secret-value") @@ -100,7 +97,7 @@ def test_resolve_keyvault_reference_with_cached_secret_version(self): # Create a mock Key Vault reference secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) - + # Create a SecretProvider with a mock credential secret_provider = SecretProvider(keyvault_credential=Mock()) @@ -109,32 +106,32 @@ def test_resolve_keyvault_reference_with_cached_secret_version(self): # This should return the cached value without calling SecretClient result = secret_provider.resolve_keyvault_reference(config) - + # Verify the result self.assertEqual(result, "cached-secret-value") - + def test_resolve_keyvault_reference_with_existing_client(self): """Test resolving a Key Vault reference with an existing client.""" # Create a mock Key Vault reference secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) - + # Create a SecretProvider with a mock credential mock_credential = Mock() secret_provider = SecretProvider(keyvault_credential=mock_credential) - + # Create a mock SecretClient mock_client = Mock() mock_secret = Mock() mock_secret.value = "secret-value" mock_client.get_secret.return_value = mock_secret - + # Add the mock client to the secret_clients dictionary vault_url = "https://myvault.vault.azure.net/" secret_provider._secret_clients[vault_url] = mock_client - + # Setup key vault identifier mock - with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() mock_id_instance._resource_id = secret_id mock_id_instance.source_id = secret_id @@ -142,34 +139,31 @@ def test_resolve_keyvault_reference_with_existing_client(self): mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance - + # Call resolve_keyvault_reference - with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + with patch.object(secret_provider, "resolve_keyvault_reference_base") as mock_base: mock_base.return_value = (mock_id_instance, vault_url) - + result = secret_provider.resolve_keyvault_reference(config) - + # Verify the result self.assertEqual(result, "secret-value") - mock_client.get_secret.assert_called_once_with( - mock_id_instance.name, - version=mock_id_instance.version - ) + mock_client.get_secret.assert_called_once_with(mock_id_instance.name, version=mock_id_instance.version) # Verify the secret was cached self.assertEqual(secret_provider._secret_version_cache[secret_id], "secret-value") - + def test_resolve_keyvault_reference_with_new_client(self): """Test resolving a Key Vault reference by creating a new client.""" # Create a mock Key Vault reference secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) - + # Create a SecretProvider with a mock credential mock_credential = Mock() secret_provider = SecretProvider(keyvault_credential=mock_credential) - + # Setup key vault identifier mock - with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() mock_id_instance._resource_id = secret_id mock_id_instance.source_id = secret_id @@ -177,51 +171,47 @@ def test_resolve_keyvault_reference_with_new_client(self): mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance - + # Mock SecretClient creation and get_secret method - with patch('azure.appconfiguration.provider._secret_provider.SecretClient') as mock_client_class: + with patch("azure.appconfiguration.provider._secret_provider.SecretClient") as mock_client_class: mock_client = Mock() mock_secret = Mock() mock_secret.value = "new-secret-value" mock_client.get_secret.return_value = mock_secret mock_client_class.return_value = mock_client - + # Call resolve_keyvault_reference - with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + with patch.object(secret_provider, "resolve_keyvault_reference_base") as mock_base: vault_url = "https://myvault.vault.azure.net/" mock_base.return_value = (mock_id_instance, vault_url) - + result = secret_provider.resolve_keyvault_reference(config) - + # Verify the result self.assertEqual(result, "new-secret-value") - mock_client_class.assert_called_once_with( - vault_url=vault_url, - credential=mock_credential - ) + mock_client_class.assert_called_once_with(vault_url=vault_url, credential=mock_credential) mock_client.get_secret.assert_called_once_with( - mock_id_instance.name, - version=mock_id_instance.version + mock_id_instance.name, version=mock_id_instance.version ) # Verify the client was cached self.assertEqual(secret_provider._secret_clients[vault_url], mock_client) # Verify the secret was cached self.assertEqual(secret_provider._secret_version_cache[secret_id], "new-secret-value") - + def test_resolve_keyvault_reference_with_secret_resolver(self): """Test resolving a Key Vault reference using a secret resolver.""" # Create a mock Key Vault reference secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) - + # Create a mock secret resolver mock_resolver = Mock(return_value="resolved-secret-value") - + # Create a SecretProvider with the mock resolver secret_provider = SecretProvider(secret_resolver=mock_resolver) - + # Setup key vault identifier mock - with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() mock_id_instance._resource_id = secret_id mock_id_instance.source_id = secret_id @@ -229,46 +219,43 @@ def test_resolve_keyvault_reference_with_secret_resolver(self): mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance - + # Call resolve_keyvault_reference - with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + with patch.object(secret_provider, "resolve_keyvault_reference_base") as mock_base: vault_url = "https://myvault.vault.azure.net/" mock_base.return_value = (mock_id_instance, vault_url) - + result = secret_provider.resolve_keyvault_reference(config) - + # Verify the result self.assertEqual(result, "resolved-secret-value") mock_resolver.assert_called_once_with(secret_id) # Verify the secret was cached self.assertEqual(secret_provider._secret_version_cache[secret_id], "resolved-secret-value") - + def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self): """Test falling back to a secret resolver if the client fails to get the secret.""" # Create a mock Key Vault reference secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) - + # Create a mock credential and secret resolver mock_credential = Mock() mock_resolver = Mock(return_value="fallback-secret-value") - + # Create a SecretProvider with both credential and resolver - secret_provider = SecretProvider( - keyvault_credential=mock_credential, - secret_resolver=mock_resolver - ) - + secret_provider = SecretProvider(keyvault_credential=mock_credential, secret_resolver=mock_resolver) + # Create a mock SecretClient that returns None for get_secret mock_client = Mock() mock_client.get_secret.return_value.value = None - + # Add the mock client to the secret_clients dictionary vault_url = "https://myvault.vault.azure.net/" secret_provider._secret_clients[vault_url] = mock_client - + # Setup key vault identifier mock - with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() mock_id_instance._resource_id = secret_id mock_id_instance.source_id = secret_id @@ -276,35 +263,32 @@ def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self): mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance - + # Call resolve_keyvault_reference - with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + with patch.object(secret_provider, "resolve_keyvault_reference_base") as mock_base: mock_base.return_value = (mock_id_instance, vault_url) - + result = secret_provider.resolve_keyvault_reference(config) - + # Verify the result self.assertEqual(result, "fallback-secret-value") - mock_client.get_secret.assert_called_once_with( - mock_id_instance.name, - version=mock_id_instance.version - ) + mock_client.get_secret.assert_called_once_with(mock_id_instance.name, version=mock_id_instance.version) mock_resolver.assert_called_once_with(secret_id) # Verify the secret was cached self.assertEqual(secret_provider._secret_version_cache[secret_id], "fallback-secret-value") - + def test_resolve_keyvault_reference_no_client_no_resolver(self): """Test that an error is raised when no client or resolver can resolve the reference.""" # Create a mock Key Vault reference secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) - + # Create a SecretProvider with a credential but no clients or resolvers mock_credential = Mock() secret_provider = SecretProvider(keyvault_credential=mock_credential) - + # Setup key vault identifier mock - with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() mock_id_instance._resource_id = secret_id mock_id_instance.source_id = secret_id @@ -312,33 +296,33 @@ def test_resolve_keyvault_reference_no_client_no_resolver(self): mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance - + # Call resolve_keyvault_reference - with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + with patch.object(secret_provider, "resolve_keyvault_reference_base") as mock_base: mock_base.return_value = (mock_id_instance, "https://othervault.vault.azure.net/") - + # This should raise an error since we have no client for this vault URL with self.assertRaises(ValueError): secret_provider.resolve_keyvault_reference(config) - + def test_close(self): """Test closing the SecretProvider.""" # Create a SecretProvider with mock clients secret_provider = SecretProvider() - + # Create mock clients mock_client1 = Mock() mock_client2 = Mock() - + # Add the mock clients to the secret_clients dictionary secret_provider._secret_clients = { "https://vault1.vault.azure.net/": mock_client1, - "https://vault2.vault.azure.net/": mock_client2 + "https://vault2.vault.azure.net/": mock_client2, } - + # Call close secret_provider.close() - + # Verify both clients were closed mock_client1.close.assert_called_once() mock_client2.close.assert_called_once() @@ -348,27 +332,23 @@ def test_client_config_specific_credential(self): # Create a mock Key Vault reference secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) - + # Create mock credentials mock_default_credential = Mock(name="default_credential") mock_specific_credential = Mock(name="specific_credential") - + # Create client configs with a specific credential client_configs = { - "https://myvault.vault.azure.net/": { - "credential": mock_specific_credential, - "retry_total": 3 - } + "https://myvault.vault.azure.net/": {"credential": mock_specific_credential, "retry_total": 3} } - + # Create a SecretProvider with default credential and client configs secret_provider = SecretProvider( - keyvault_credential=mock_default_credential, - keyvault_client_configs=client_configs + keyvault_credential=mock_default_credential, keyvault_client_configs=client_configs ) - + # Setup key vault identifier mock - with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() mock_id_instance._resource_id = secret_id mock_id_instance.source_id = secret_id @@ -376,27 +356,25 @@ def test_client_config_specific_credential(self): mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance - + # Mock SecretClient creation and get_secret method - with patch('azure.appconfiguration.provider._secret_provider.SecretClient') as mock_client_class: + with patch("azure.appconfiguration.provider._secret_provider.SecretClient") as mock_client_class: mock_client = Mock() mock_secret = Mock() mock_secret.value = "secret-value" mock_client.get_secret.return_value = mock_secret mock_client_class.return_value = mock_client - + # Call resolve_keyvault_reference - with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + with patch.object(secret_provider, "resolve_keyvault_reference_base") as mock_base: vault_url = "https://myvault.vault.azure.net/" mock_base.return_value = (mock_id_instance, vault_url) - + result = secret_provider.resolve_keyvault_reference(config) - + # Verify the specific credential was used instead of the default mock_client_class.assert_called_once_with( - vault_url=vault_url, - credential=mock_specific_credential, - retry_total=3 + vault_url=vault_url, credential=mock_specific_credential, retry_total=3 ) # Verify the result self.assertEqual(result, "secret-value") @@ -405,44 +383,40 @@ def test_mapping_interface(self): """Test that the SecretProvider implements the Mapping interface.""" # Create a SecretProvider secret_provider = SecretProvider() - + # Add some items to the cache - secret_provider._secret_cache = { - "key1": "value1", - "key2": "value2", - "key3": "value3" - } - + secret_provider._secret_cache = {"key1": "value1", "key2": "value2", "key3": "value3"} + # Test __getitem__ self.assertEqual(secret_provider["key1"], "value1") self.assertEqual(secret_provider["key2"], "value2") self.assertEqual(secret_provider["key3"], "value3") - + # Test __len__ self.assertEqual(len(secret_provider), 3) - + # Test __iter__ keys = list(secret_provider) self.assertEqual(sorted(keys), ["key1", "key2", "key3"]) - + # Test __contains__ self.assertIn("key1", secret_provider) self.assertIn("key2", secret_provider) self.assertIn("key3", secret_provider) self.assertNotIn("key4", secret_provider) - + # Test keys self.assertEqual(set(secret_provider.keys()), {"key1", "key2", "key3"}) - + # Test items items = dict(secret_provider.items()) self.assertEqual(items, {"key1": "value1", "key2": "value2", "key3": "value3"}) - + # Test values values = list(secret_provider.values()) # Sort the expected values instead since values may not be comparable self.assertEqual(set(values), {"value1", "value2", "value3"}) - + # Test get self.assertEqual(secret_provider.get("key1"), "value1") self.assertEqual(secret_provider.get("key4"), None) @@ -452,57 +426,46 @@ def test_bust_cache(self): """Test that bust_cache clears the secret cache.""" # Create a SecretProvider secret_provider = SecretProvider() - + # Add some items to the cache - secret_provider._secret_cache = { - "key1": "value1", - "key2": "value2", - "key3": "value3" - } - + secret_provider._secret_cache = {"key1": "value1", "key2": "value2", "key3": "value3"} + # Verify the cache has items self.assertEqual(len(secret_provider._secret_cache), 3) - + # Call bust_cache secret_provider.bust_cache() - + # Verify the cache is empty self.assertEqual(len(secret_provider._secret_cache), 0) self.assertEqual(secret_provider._secret_cache, {}) @recorded_by_proxy @app_config_decorator_aad - def test_integration_with_keyvault( - self, - appconfiguration_endpoint_string, - appconfiguration_keyvault_secret_url - ): + def test_integration_with_keyvault(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url): """Test integration with Key Vault.""" if not appconfiguration_keyvault_secret_url: self.skipTest("No Key Vault secret URL provided") - + # Get a credential credential = self.get_credential(SecretClient) - + # Create a SecretProvider with the credential secret_provider = SecretProvider(keyvault_credential=credential) - + # Create a Key Vault reference - config = SecretReferenceConfigurationSetting( - key="test-key", - secret_id=appconfiguration_keyvault_secret_url - ) - + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=appconfiguration_keyvault_secret_url) + # Resolve the reference secret_value = secret_provider.resolve_keyvault_reference(config) - + # Verify a value was returned (we can't know the exact value) self.assertIsNotNone(secret_value) self.assertTrue(isinstance(secret_value, str)) - + # Verify the secret was cached self.assertIn(appconfiguration_keyvault_secret_url, secret_provider._secret_cache) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider_async.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider_async.py index d21e9bdb3e78..32fbecbfcbee 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider_async.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider_async.py @@ -21,7 +21,7 @@ class TestSecretProviderAsync(AppConfigTestCase, unittest.IsolatedAsyncioTestCas async def test_init_with_defaults(self): """Test initialization of SecretProvider with default parameters.""" secret_provider = SecretProvider() - + # Verify initialization with defaults self.assertEqual(len(secret_provider._secret_clients), 0) self.assertIsNone(secret_provider._keyvault_credential) @@ -30,52 +30,49 @@ async def test_init_with_defaults(self): self.assertFalse(secret_provider.uses_key_vault) self.assertIsNone(secret_provider.secret_refresh_timer) self.assertEqual(len(secret_provider._secret_cache), 0) - + async def test_init_with_keyvault_credential(self): """Test initialization with a Key Vault credential.""" mock_credential = Mock() secret_provider = SecretProvider(keyvault_credential=mock_credential) - + # Verify initialization with a Key Vault credential self.assertEqual(len(secret_provider._secret_clients), 0) self.assertEqual(secret_provider._keyvault_credential, mock_credential) self.assertIsNone(secret_provider._secret_resolver) self.assertEqual(secret_provider._keyvault_client_configs, {}) self.assertTrue(secret_provider.uses_key_vault) - + async def test_init_with_secret_resolver(self): """Test initialization with a secret resolver.""" mock_resolver = Mock() secret_provider = SecretProvider(secret_resolver=mock_resolver) - + # Verify initialization with a secret resolver self.assertEqual(len(secret_provider._secret_clients), 0) self.assertIsNone(secret_provider._keyvault_credential) self.assertEqual(secret_provider._secret_resolver, mock_resolver) self.assertEqual(secret_provider._keyvault_client_configs, {}) self.assertTrue(secret_provider.uses_key_vault) - + async def test_init_with_keyvault_client_configs(self): """Test initialization with Key Vault client configurations.""" client_configs = {"https://myvault.vault.azure.net/": {"retry_total": 3}} secret_provider = SecretProvider(keyvault_client_configs=client_configs) - + # Verify initialization with Key Vault client configurations self.assertEqual(len(secret_provider._secret_clients), 0) self.assertIsNone(secret_provider._keyvault_credential) self.assertIsNone(secret_provider._secret_resolver) self.assertEqual(secret_provider._keyvault_client_configs, client_configs) self.assertTrue(secret_provider.uses_key_vault) - + async def test_init_with_secret_refresh_interval(self): """Test initialization with a secret refresh interval.""" mock_credential = Mock() refresh_interval = 30 - secret_provider = SecretProvider( - keyvault_credential=mock_credential, - secret_refresh_interval=refresh_interval - ) - + secret_provider = SecretProvider(keyvault_credential=mock_credential, secret_refresh_interval=refresh_interval) + # Verify initialization with a secret refresh interval self.assertIsNotNone(secret_provider.secret_refresh_timer) self.assertTrue(secret_provider.uses_key_vault) @@ -85,7 +82,7 @@ async def test_resolve_keyvault_reference_with_cached_secret(self): # Create a mock Key Vault reference secret_id = "https://myvault.vault.azure.net/secrets/mysecret/" config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) - + # Create a SecretProvider with a mock credential secret_provider = SecretProvider(keyvault_credential=Mock()) @@ -94,7 +91,7 @@ async def test_resolve_keyvault_reference_with_cached_secret(self): # This should return the cached value without calling SecretClient result = await secret_provider.resolve_keyvault_reference(config) - + # Verify the result self.assertEqual(result, "cached-secret-value") @@ -103,7 +100,7 @@ async def test_resolve_keyvault_reference_with_cached_secret_version(self): # Create a mock Key Vault reference secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) - + # Create a SecretProvider with a mock credential secret_provider = SecretProvider(keyvault_credential=Mock()) @@ -112,33 +109,33 @@ async def test_resolve_keyvault_reference_with_cached_secret_version(self): # This should return the cached value without calling SecretClient result = await secret_provider.resolve_keyvault_reference(config) - + # Verify the result self.assertEqual(result, "cached-secret-value") - + async def test_resolve_keyvault_reference_with_existing_client(self): """Test resolving a Key Vault reference with an existing client.""" # Create a mock Key Vault reference secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) - + # Create a SecretProvider with a mock credential mock_credential = Mock() secret_provider = SecretProvider(keyvault_credential=mock_credential) - + # Create a mock SecretClient mock_client = Mock() mock_secret = Mock() mock_secret.value = "secret-value" # Set up the get_secret method to return an awaitable that resolves to mock_secret mock_client.get_secret = AsyncMock(return_value=mock_secret) - + # Add the mock client to the secret_clients dictionary vault_url = "https://myvault.vault.azure.net/" secret_provider._secret_clients[vault_url] = mock_client - + # Setup key vault identifier mock - with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() mock_id_instance._resource_id = secret_id mock_id_instance.source_id = secret_id @@ -146,34 +143,31 @@ async def test_resolve_keyvault_reference_with_existing_client(self): mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance - + # Call resolve_keyvault_reference - with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + with patch.object(secret_provider, "resolve_keyvault_reference_base") as mock_base: mock_base.return_value = (mock_id_instance, vault_url) - + result = await secret_provider.resolve_keyvault_reference(config) - + # Verify the result self.assertEqual(result, "secret-value") - mock_client.get_secret.assert_called_once_with( - mock_id_instance.name, - version=mock_id_instance.version - ) + mock_client.get_secret.assert_called_once_with(mock_id_instance.name, version=mock_id_instance.version) # Verify the secret was cached self.assertEqual(secret_provider._secret_cache[secret_id], "secret-value") - + async def test_resolve_keyvault_reference_with_new_client(self): """Test resolving a Key Vault reference by creating a new client.""" # Create a mock Key Vault reference secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) - + # Create a SecretProvider with a mock credential mock_credential = Mock() secret_provider = SecretProvider(keyvault_credential=mock_credential) - + # Setup key vault identifier mock - with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() mock_id_instance._resource_id = secret_id mock_id_instance.source_id = secret_id @@ -181,51 +175,47 @@ async def test_resolve_keyvault_reference_with_new_client(self): mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance - + # Mock SecretClient creation and get_secret method - with patch('azure.appconfiguration.provider.aio._async_secret_provider.SecretClient') as mock_client_class: + with patch("azure.appconfiguration.provider.aio._async_secret_provider.SecretClient") as mock_client_class: mock_client = Mock() mock_secret = Mock() mock_secret.value = "new-secret-value" mock_client.get_secret = AsyncMock(return_value=mock_secret) mock_client_class.return_value = mock_client - + # Call resolve_keyvault_reference - with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + with patch.object(secret_provider, "resolve_keyvault_reference_base") as mock_base: vault_url = "https://myvault.vault.azure.net/" mock_base.return_value = (mock_id_instance, vault_url) - + result = await secret_provider.resolve_keyvault_reference(config) - + # Verify the result self.assertEqual(result, "new-secret-value") - mock_client_class.assert_called_once_with( - vault_url=vault_url, - credential=mock_credential - ) + mock_client_class.assert_called_once_with(vault_url=vault_url, credential=mock_credential) mock_client.get_secret.assert_called_once_with( - mock_id_instance.name, - version=mock_id_instance.version + mock_id_instance.name, version=mock_id_instance.version ) # Verify the client was cached self.assertEqual(secret_provider._secret_clients[vault_url], mock_client) # Verify the secret was cached self.assertEqual(secret_provider._secret_cache[secret_id], "new-secret-value") - + async def test_resolve_keyvault_reference_with_secret_resolver(self): """Test resolving a Key Vault reference using a secret resolver.""" # Create a mock Key Vault reference secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) - + # Create a mock secret resolver mock_resolver = Mock(return_value="resolved-secret-value") - + # Create a SecretProvider with the mock resolver secret_provider = SecretProvider(secret_resolver=mock_resolver) - + # Setup key vault identifier mock - with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() mock_id_instance._resource_id = secret_id mock_id_instance.source_id = secret_id @@ -233,35 +223,35 @@ async def test_resolve_keyvault_reference_with_secret_resolver(self): mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance - + # Call resolve_keyvault_reference - with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + with patch.object(secret_provider, "resolve_keyvault_reference_base") as mock_base: vault_url = "https://myvault.vault.azure.net/" mock_base.return_value = (mock_id_instance, vault_url) - + result = await secret_provider.resolve_keyvault_reference(config) - + # Verify the result self.assertEqual(result, "resolved-secret-value") mock_resolver.assert_called_once_with(secret_id) # Verify the secret was cached self.assertEqual(secret_provider._secret_cache[secret_id], "resolved-secret-value") - + async def test_resolve_keyvault_reference_with_async_secret_resolver(self): """Test resolving a Key Vault reference using an async secret resolver.""" # Create a mock Key Vault reference secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) - + # Create a mock async secret resolver async def async_resolver(secret_id): return "async-resolved-secret-value" - + # Create a SecretProvider with the mock resolver secret_provider = SecretProvider(secret_resolver=async_resolver) - + # Setup key vault identifier mock - with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() mock_id_instance._resource_id = secret_id mock_id_instance.source_id = secret_id @@ -269,48 +259,45 @@ async def async_resolver(secret_id): mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance - + # Call resolve_keyvault_reference - with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + with patch.object(secret_provider, "resolve_keyvault_reference_base") as mock_base: vault_url = "https://myvault.vault.azure.net/" mock_base.return_value = (mock_id_instance, vault_url) - + result = await secret_provider.resolve_keyvault_reference(config) - + # Verify the result self.assertEqual(result, "async-resolved-secret-value") # Verify the secret was cached breakpoint() self.assertEqual(secret_provider._secret_version_cache[secret_id], "async-resolved-secret-value") - + async def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self): """Test falling back to a secret resolver if the client fails to get the secret.""" # Create a mock Key Vault reference secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) - + # Create a mock credential and secret resolver mock_credential = Mock() mock_resolver = Mock(return_value="fallback-secret-value") - + # Create a SecretProvider with both credential and resolver - secret_provider = SecretProvider( - keyvault_credential=mock_credential, - secret_resolver=mock_resolver - ) - + secret_provider = SecretProvider(keyvault_credential=mock_credential, secret_resolver=mock_resolver) + # Create a mock SecretClient that returns None for get_secret mock_client = Mock() mock_secret = Mock() mock_secret.value = None mock_client.get_secret = AsyncMock(return_value=mock_secret) - + # Add the mock client to the secret_clients dictionary vault_url = "https://myvault.vault.azure.net/" secret_provider._secret_clients[vault_url] = mock_client - + # Setup key vault identifier mock - with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() mock_id_instance._resource_id = secret_id mock_id_instance.source_id = secret_id @@ -318,35 +305,32 @@ async def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance - + # Call resolve_keyvault_reference - with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + with patch.object(secret_provider, "resolve_keyvault_reference_base") as mock_base: mock_base.return_value = (mock_id_instance, vault_url) - + result = await secret_provider.resolve_keyvault_reference(config) - + # Verify the result self.assertEqual(result, "fallback-secret-value") - mock_client.get_secret.assert_called_once_with( - mock_id_instance.name, - version=mock_id_instance.version - ) + mock_client.get_secret.assert_called_once_with(mock_id_instance.name, version=mock_id_instance.version) mock_resolver.assert_called_once_with(secret_id) # Verify the secret was cached self.assertEqual(secret_provider._secret_cache[secret_id], "fallback-secret-value") - + async def test_resolve_keyvault_reference_no_client_no_resolver(self): """Test that an error is raised when no client or resolver can resolve the reference.""" # Create a mock Key Vault reference secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) - + # Create a SecretProvider with a credential but no clients or resolvers mock_credential = Mock() secret_provider = SecretProvider(keyvault_credential=mock_credential) - + # Setup key vault identifier mock - with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() mock_id_instance._resource_id = secret_id mock_id_instance.source_id = secret_id @@ -354,35 +338,35 @@ async def test_resolve_keyvault_reference_no_client_no_resolver(self): mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance - + # Call resolve_keyvault_reference - with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + with patch.object(secret_provider, "resolve_keyvault_reference_base") as mock_base: mock_base.return_value = (mock_id_instance, "https://othervault.vault.azure.net/") - + # This should raise an error since we have no client for this vault URL with self.assertRaises(ValueError): await secret_provider.resolve_keyvault_reference(config) - + async def test_close(self): """Test closing the SecretProvider.""" # Create a SecretProvider with mock clients secret_provider = SecretProvider() - + # Create mock clients mock_client1 = Mock() mock_client1.close = AsyncMock() mock_client2 = Mock() mock_client2.close = AsyncMock() - + # Add the mock clients to the secret_clients dictionary secret_provider._secret_clients = { "https://vault1.vault.azure.net/": mock_client1, - "https://vault2.vault.azure.net/": mock_client2 + "https://vault2.vault.azure.net/": mock_client2, } - + # Call close await secret_provider.close() - + # Verify both clients were closed mock_client1.close.assert_called_once() mock_client2.close.assert_called_once() @@ -392,27 +376,23 @@ async def test_client_config_specific_credential(self): # Create a mock Key Vault reference secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) - + # Create mock credentials mock_default_credential = Mock(name="default_credential") mock_specific_credential = Mock(name="specific_credential") - + # Create client configs with a specific credential client_configs = { - "https://myvault.vault.azure.net/": { - "credential": mock_specific_credential, - "retry_total": 3 - } + "https://myvault.vault.azure.net/": {"credential": mock_specific_credential, "retry_total": 3} } - + # Create a SecretProvider with default credential and client configs secret_provider = SecretProvider( - keyvault_credential=mock_default_credential, - keyvault_client_configs=client_configs + keyvault_credential=mock_default_credential, keyvault_client_configs=client_configs ) - + # Setup key vault identifier mock - with patch('azure.keyvault.secrets.KeyVaultSecretIdentifier') as mock_kv_id: + with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() mock_id_instance._resource_id = secret_id mock_id_instance.source_id = secret_id @@ -420,27 +400,25 @@ async def test_client_config_specific_credential(self): mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance - + # Mock SecretClient creation and get_secret method - with patch('azure.appconfiguration.provider.aio._async_secret_provider.SecretClient') as mock_client_class: + with patch("azure.appconfiguration.provider.aio._async_secret_provider.SecretClient") as mock_client_class: mock_client = Mock() mock_secret = Mock() mock_secret.value = "secret-value" mock_client.get_secret = AsyncMock(return_value=mock_secret) mock_client_class.return_value = mock_client - + # Call resolve_keyvault_reference - with patch.object(secret_provider, 'resolve_keyvault_reference_base') as mock_base: + with patch.object(secret_provider, "resolve_keyvault_reference_base") as mock_base: vault_url = "https://myvault.vault.azure.net/" mock_base.return_value = (mock_id_instance, vault_url) - + result = await secret_provider.resolve_keyvault_reference(config) - + # Verify the specific credential was used instead of the default mock_client_class.assert_called_once_with( - vault_url=vault_url, - credential=mock_specific_credential, - retry_total=3 + vault_url=vault_url, credential=mock_specific_credential, retry_total=3 ) # Verify the result self.assertEqual(result, "secret-value") @@ -449,44 +427,40 @@ async def test_mapping_interface(self): """Test that the SecretProvider implements the Mapping interface.""" # Create a SecretProvider secret_provider = SecretProvider() - + # Add some items to the cache - secret_provider._secret_cache = { - "key1": "value1", - "key2": "value2", - "key3": "value3" - } - + secret_provider._secret_cache = {"key1": "value1", "key2": "value2", "key3": "value3"} + # Test __getitem__ self.assertEqual(secret_provider["key1"], "value1") self.assertEqual(secret_provider["key2"], "value2") self.assertEqual(secret_provider["key3"], "value3") - + # Test __len__ self.assertEqual(len(secret_provider), 3) - + # Test __iter__ keys = list(secret_provider) self.assertEqual(sorted(keys), ["key1", "key2", "key3"]) - + # Test __contains__ self.assertIn("key1", secret_provider) self.assertIn("key2", secret_provider) self.assertIn("key3", secret_provider) self.assertNotIn("key4", secret_provider) - + # Test keys self.assertEqual(set(secret_provider.keys()), {"key1", "key2", "key3"}) - + # Test items items = dict(secret_provider.items()) self.assertEqual(items, {"key1": "value1", "key2": "value2", "key3": "value3"}) - + # Test values values = list(secret_provider.values()) # Sort the expected values instead since values may not be comparable self.assertEqual(set(values), {"value1", "value2", "value3"}) - + # Test get self.assertEqual(secret_provider.get("key1"), "value1") self.assertEqual(secret_provider.get("key4"), None) @@ -496,20 +470,16 @@ async def test_bust_cache(self): """Test that bust_cache clears the secret cache.""" # Create a SecretProvider secret_provider = SecretProvider() - + # Add some items to the cache - secret_provider._secret_cache = { - "key1": "value1", - "key2": "value2", - "key3": "value3" - } - + secret_provider._secret_cache = {"key1": "value1", "key2": "value2", "key3": "value3"} + # Verify the cache has items self.assertEqual(len(secret_provider._secret_cache), 3) - + # Call bust_cache secret_provider.bust_cache() - + # Verify the cache is empty self.assertEqual(len(secret_provider._secret_cache), 0) self.assertEqual(secret_provider._secret_cache, {}) @@ -517,36 +487,31 @@ async def test_bust_cache(self): @recorded_by_proxy @app_config_decorator_aad async def test_integration_with_keyvault( - self, - appconfiguration_endpoint_string, - appconfiguration_keyvault_secret_url + self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url ): """Test integration with Key Vault.""" if not appconfiguration_keyvault_secret_url: self.skipTest("No Key Vault secret URL provided") - + # Get a credential credential = self.get_credential(SecretClient) - + # Create a SecretProvider with the credential secret_provider = SecretProvider(keyvault_credential=credential) - + # Create a Key Vault reference - config = SecretReferenceConfigurationSetting( - key="test-key", - secret_id=appconfiguration_keyvault_secret_url - ) - + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=appconfiguration_keyvault_secret_url) + # Resolve the reference secret_value = await secret_provider.resolve_keyvault_reference(config) - + # Verify a value was returned (we can't know the exact value) self.assertIsNotNone(secret_value) self.assertTrue(isinstance(secret_value, str)) - + # Verify the secret was cached self.assertIn(appconfiguration_keyvault_secret_url, secret_provider._secret_cache) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() From 4fb9532c15b15f80267651db2033d3255211e9bb Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 29 Aug 2025 10:47:23 -0700 Subject: [PATCH 25/43] Updating Tests --- .../assets.json | 2 +- .../_azureappconfigurationprovider.py | 2 +- .../provider/_key_vault/__init__.py | 13 +++++++++ .../{ => _key_vault}/_secret_provider.py | 0 .../{ => _key_vault}/_secret_provider_base.py | 2 +- .../_azureappconfigurationproviderasync.py | 2 +- .../provider/aio/_key_vault/__init__.py | 11 +++++++ .../_async_secret_provider.py | 2 +- .../key_vault/test_async_secret_provider.py} | 29 +++++++++---------- .../key_vault}/test_async_secret_refresh.py | 0 .../{ => key_vault}/test_secret_provider.py | 6 ++-- .../{ => key_vault}/test_secret_refresh.py | 0 12 files changed, 45 insertions(+), 24 deletions(-) create mode 100644 sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/__init__.py rename sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/{ => _key_vault}/_secret_provider.py (100%) rename sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/{ => _key_vault}/_secret_provider_base.py (98%) create mode 100644 sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/__init__.py rename sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/{ => _key_vault}/_async_secret_provider.py (98%) rename sdk/appconfiguration/azure-appconfiguration-provider/tests/{test_secret_provider_async.py => aio/key_vault/test_async_secret_provider.py} (95%) rename sdk/appconfiguration/azure-appconfiguration-provider/tests/{ => aio/key_vault}/test_async_secret_refresh.py (100%) rename sdk/appconfiguration/azure-appconfiguration-provider/tests/{ => key_vault}/test_secret_provider.py (98%) rename sdk/appconfiguration/azure-appconfiguration-provider/tests/{ => key_vault}/test_secret_refresh.py (100%) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json index ab53cdb0d738..a9f86aaa62c1 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json +++ b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/appconfiguration/azure-appconfiguration-provider", - "Tag": "python/appconfiguration/azure-appconfiguration-provider_dc8f23ef71" + "Tag": "python/appconfiguration/azure-appconfiguration-provider_1250228ab3" } diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py index e5b6d8f13407..1ce7eefc8298 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -24,7 +24,7 @@ ) from azure.core.exceptions import AzureError, HttpResponseError from ._models import AzureAppConfigurationKeyVaultOptions, SettingSelector -from ._secret_provider import SecretProvider +from ._key_vault._secret_provider import SecretProvider from ._constants import ( FEATURE_MANAGEMENT_KEY, FEATURE_FLAG_KEY, diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/__init__.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/__init__.py new file mode 100644 index 000000000000..56f3c6bd3029 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/__init__.py @@ -0,0 +1,13 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +from ._secret_provider import SecretProvider +from ._secret_provider_base import _SecretProviderBase + +__all__ = [ + "SecretProvider", + "_SecretProviderBase", +] diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py similarity index 100% rename from sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider.py rename to sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider_base.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py similarity index 98% rename from sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider_base.py rename to sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py index 8b7c06a82cf0..95c2b6895fa3 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_secret_provider_base.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py @@ -19,7 +19,7 @@ ) from azure.appconfiguration import SecretReferenceConfigurationSetting # type:ignore # pylint:disable=no-name-in-module from azure.keyvault.secrets import KeyVaultSecretIdentifier -from ._azureappconfigurationproviderbase import _RefreshTimer +from .._azureappconfigurationproviderbase import _RefreshTimer JSON = Mapping[str, Any] _T = TypeVar("_T") diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py index ccd3452e578d..9684e74e858b 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -27,7 +27,7 @@ ) from azure.core.exceptions import AzureError, HttpResponseError from .._models import AzureAppConfigurationKeyVaultOptions, SettingSelector -from ._async_secret_provider import SecretProvider +from ._key_vault._async_secret_provider import SecretProvider from .._constants import ( FEATURE_MANAGEMENT_KEY, FEATURE_FLAG_KEY, diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/__init__.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/__init__.py new file mode 100644 index 000000000000..3be7677c8603 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/__init__.py @@ -0,0 +1,11 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +from ._async_secret_provider import SecretProvider + +__all__ = [ + "SecretProvider" +] diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py similarity index 98% rename from sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py rename to sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py index 7f35c85b9cab..8451564e7dfb 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py @@ -8,7 +8,7 @@ from azure.appconfiguration import SecretReferenceConfigurationSetting # type:ignore # pylint:disable=no-name-in-module from azure.keyvault.secrets.aio import SecretClient from azure.core.exceptions import ServiceRequestError -from .._secret_provider_base import _SecretProviderBase +from ..._key_vault._secret_provider_base import _SecretProviderBase JSON = Mapping[str, Any] _T = TypeVar("_T") diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider_async.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py similarity index 95% rename from sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider_async.py rename to sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py index 32fbecbfcbee..fb6d2e58b136 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider_async.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py @@ -4,15 +4,12 @@ # license information. # -------------------------------------------------------------------------- import unittest -from unittest.mock import Mock, patch, MagicMock, AsyncMock -import asyncio -import pytest +from unittest.mock import Mock, patch, AsyncMock from azure.appconfiguration import SecretReferenceConfigurationSetting -from azure.appconfiguration.provider.aio._async_secret_provider import SecretProvider +from azure.appconfiguration.provider.aio._key_vault._async_secret_provider import SecretProvider from azure.keyvault.secrets.aio import SecretClient -from azure.core.exceptions import ServiceRequestError -from devtools_testutils import recorded_by_proxy -from preparers import app_config_decorator_aad +from devtools_testutils.aio import recorded_by_proxy_async +from async_preparers import app_config_decorator_async from testcase import AppConfigTestCase @@ -154,7 +151,7 @@ async def test_resolve_keyvault_reference_with_existing_client(self): self.assertEqual(result, "secret-value") mock_client.get_secret.assert_called_once_with(mock_id_instance.name, version=mock_id_instance.version) # Verify the secret was cached - self.assertEqual(secret_provider._secret_cache[secret_id], "secret-value") + self.assertEqual(secret_provider._secret_version_cache[secret_id], "secret-value") async def test_resolve_keyvault_reference_with_new_client(self): """Test resolving a Key Vault reference by creating a new client.""" @@ -177,7 +174,7 @@ async def test_resolve_keyvault_reference_with_new_client(self): mock_kv_id.return_value = mock_id_instance # Mock SecretClient creation and get_secret method - with patch("azure.appconfiguration.provider.aio._async_secret_provider.SecretClient") as mock_client_class: + with patch("azure.appconfiguration.provider.aio._key_vault._async_secret_provider.SecretClient") as mock_client_class: mock_client = Mock() mock_secret = Mock() mock_secret.value = "new-secret-value" @@ -200,7 +197,7 @@ async def test_resolve_keyvault_reference_with_new_client(self): # Verify the client was cached self.assertEqual(secret_provider._secret_clients[vault_url], mock_client) # Verify the secret was cached - self.assertEqual(secret_provider._secret_cache[secret_id], "new-secret-value") + self.assertEqual(secret_provider._secret_version_cache[secret_id], "new-secret-value") async def test_resolve_keyvault_reference_with_secret_resolver(self): """Test resolving a Key Vault reference using a secret resolver.""" @@ -235,7 +232,7 @@ async def test_resolve_keyvault_reference_with_secret_resolver(self): self.assertEqual(result, "resolved-secret-value") mock_resolver.assert_called_once_with(secret_id) # Verify the secret was cached - self.assertEqual(secret_provider._secret_cache[secret_id], "resolved-secret-value") + self.assertEqual(secret_provider._secret_version_cache[secret_id], "resolved-secret-value") async def test_resolve_keyvault_reference_with_async_secret_resolver(self): """Test resolving a Key Vault reference using an async secret resolver.""" @@ -270,7 +267,6 @@ async def async_resolver(secret_id): # Verify the result self.assertEqual(result, "async-resolved-secret-value") # Verify the secret was cached - breakpoint() self.assertEqual(secret_provider._secret_version_cache[secret_id], "async-resolved-secret-value") async def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self): @@ -317,7 +313,7 @@ async def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self mock_client.get_secret.assert_called_once_with(mock_id_instance.name, version=mock_id_instance.version) mock_resolver.assert_called_once_with(secret_id) # Verify the secret was cached - self.assertEqual(secret_provider._secret_cache[secret_id], "fallback-secret-value") + self.assertEqual(secret_provider._secret_version_cache[secret_id], "fallback-secret-value") async def test_resolve_keyvault_reference_no_client_no_resolver(self): """Test that an error is raised when no client or resolver can resolve the reference.""" @@ -346,6 +342,7 @@ async def test_resolve_keyvault_reference_no_client_no_resolver(self): # This should raise an error since we have no client for this vault URL with self.assertRaises(ValueError): await secret_provider.resolve_keyvault_reference(config) + await secret_provider.close() async def test_close(self): """Test closing the SecretProvider.""" @@ -402,7 +399,7 @@ async def test_client_config_specific_credential(self): mock_kv_id.return_value = mock_id_instance # Mock SecretClient creation and get_secret method - with patch("azure.appconfiguration.provider.aio._async_secret_provider.SecretClient") as mock_client_class: + with patch("azure.appconfiguration.provider.aio._key_vault._async_secret_provider.SecretClient") as mock_client_class: mock_client = Mock() mock_secret = Mock() mock_secret.value = "secret-value" @@ -484,8 +481,8 @@ async def test_bust_cache(self): self.assertEqual(len(secret_provider._secret_cache), 0) self.assertEqual(secret_provider._secret_cache, {}) - @recorded_by_proxy - @app_config_decorator_aad + @app_config_decorator_async + @recorded_by_proxy_async async def test_integration_with_keyvault( self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url ): diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_refresh.py similarity index 100% rename from sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_secret_refresh.py rename to sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_refresh.py diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/key_vault/test_secret_provider.py similarity index 98% rename from sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider.py rename to sdk/appconfiguration/azure-appconfiguration-provider/tests/key_vault/test_secret_provider.py index 34fdb562e42d..8971aa3661af 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/key_vault/test_secret_provider.py @@ -6,7 +6,7 @@ import unittest from unittest.mock import Mock, patch, MagicMock from azure.appconfiguration import SecretReferenceConfigurationSetting -from azure.appconfiguration.provider._secret_provider import SecretProvider +from azure.appconfiguration.provider._key_vault._secret_provider import SecretProvider from azure.keyvault.secrets import SecretClient, KeyVaultSecret from devtools_testutils import recorded_by_proxy from preparers import app_config_decorator_aad @@ -173,7 +173,7 @@ def test_resolve_keyvault_reference_with_new_client(self): mock_kv_id.return_value = mock_id_instance # Mock SecretClient creation and get_secret method - with patch("azure.appconfiguration.provider._secret_provider.SecretClient") as mock_client_class: + with patch("azure.appconfiguration.provider._key_vault._secret_provider.SecretClient") as mock_client_class: mock_client = Mock() mock_secret = Mock() mock_secret.value = "new-secret-value" @@ -358,7 +358,7 @@ def test_client_config_specific_credential(self): mock_kv_id.return_value = mock_id_instance # Mock SecretClient creation and get_secret method - with patch("azure.appconfiguration.provider._secret_provider.SecretClient") as mock_client_class: + with patch("azure.appconfiguration.provider._key_vault._secret_provider.SecretClient") as mock_client_class: mock_client = Mock() mock_secret = Mock() mock_secret.value = "secret-value" diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/key_vault/test_secret_refresh.py similarity index 100% rename from sdk/appconfiguration/azure-appconfiguration-provider/tests/test_secret_refresh.py rename to sdk/appconfiguration/azure-appconfiguration-provider/tests/key_vault/test_secret_refresh.py From 6f55701d83c9954d825e9146457bbd12f60a0066 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 29 Aug 2025 11:35:11 -0700 Subject: [PATCH 26/43] Updating Async to be the same as sync --- .../_azureappconfigurationprovider.py | 49 +++--- .../_azureappconfigurationproviderasync.py | 153 ++++++++---------- 2 files changed, 93 insertions(+), 109 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py index 1ce7eefc8298..eccee12ebe30 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -293,6 +293,9 @@ def _common_refresh( refresh_condition: bool, **kwargs, ) -> None: + """ + A common method for handing replicas on refresh. Along with error handling. + """ if not refresh_condition: logger.debug("Refresh called but no refresh enabled.") return @@ -343,7 +346,7 @@ def _common_refresh( if self._on_refresh_success: self._on_refresh_success() - def _refresh_operation_configuration(self, client, headers, **inner_kwargs): + def _refresh_operation_configuration(self, client, headers, **kwargs): configuration_settings: Optional[List[ConfigurationSetting]] = None need_refresh = False reset_secret_timer = False @@ -355,12 +358,12 @@ def _refresh_operation_configuration(self, client, headers, **inner_kwargs): if not need_refresh: need_refresh, self._refresh_on, configuration_settings = client.refresh_configuration_settings( - self._selects, self._refresh_on, headers=headers, **inner_kwargs + self._selects, self._refresh_on, headers=headers, **kwargs ) else: # Force a refresh to make sure secrets are up to date configuration_settings, self._refresh_on = client.load_configuration_settings( - self._selects, self._refresh_on, headers=headers, **inner_kwargs + self._selects, self._refresh_on, headers=headers, **kwargs ) configuration_settings_processed: Dict[str, Any] = {} @@ -385,13 +388,13 @@ def _refresh_operation_configuration(self, client, headers, **inner_kwargs): return True - def _refresh_operation_feature_flags(self, client, headers, **inner_kwargs): + def _refresh_operation_feature_flags(self, client, headers, **kwargs): need_ff_refresh, refresh_on_feature_flags, feature_flags, filters_used = client.refresh_feature_flags( self._refresh_on_feature_flags, self._feature_flag_selectors, headers, self._origin_endpoint or "", - **inner_kwargs, + **kwargs, ) if refresh_on_feature_flags: @@ -405,26 +408,6 @@ def _refresh_operation_feature_flags(self, client, headers, **inner_kwargs): self._feature_flag_refresh_timer.reset() return True - def _refresh_configuration_settings(self, **kwargs: Any) -> None: - self._common_refresh( - refresh_operation=self._refresh_operation_configuration, - error_log_message="Failed to refresh configurations from endpoint %s", - timer=self._refresh_timer, - refresh_condition=bool(self._refresh_on), - **kwargs, - ) - - def _refresh_feature_flags(self, **kwargs) -> None: # pylint: disable=too-many-statements - """Refresh feature flags from Azure App Configuration.""" - - self._common_refresh( - refresh_operation=self._refresh_operation_feature_flags, - error_log_message="Failed to refresh feature flags from endpoint %s", - timer=self._feature_flag_refresh_timer, - refresh_condition=self._feature_flag_refresh_enabled, - **kwargs, - ) - def refresh(self, **kwargs) -> None: # pylint: disable=too-many-statements if ( not self._refresh_on @@ -438,11 +421,23 @@ def refresh(self, **kwargs) -> None: # pylint: disable=too-many-statements return try: if self._refresh_timer and self._refresh_timer.needs_refresh(): - self._refresh_configuration_settings(**kwargs) + self._common_refresh( + refresh_operation=self._refresh_operation_configuration, + error_log_message="Failed to refresh configurations from endpoint %s", + timer=self._refresh_timer, + refresh_condition=bool(self._refresh_on), + **kwargs, + ) if self._feature_flag_refresh_enabled and ( self._feature_flag_refresh_timer and self._feature_flag_refresh_timer.needs_refresh() ): - self._refresh_feature_flags(**kwargs) + self._common_refresh( + refresh_operation=self._refresh_operation_feature_flags, + error_log_message="Failed to refresh feature flags from endpoint %s", + timer=self._feature_flag_refresh_timer, + refresh_condition=self._feature_flag_refresh_enabled, + **kwargs, + ) finally: self._refresh_lock.release() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py index 9684e74e858b..a52710d728cf 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -299,6 +299,9 @@ async def _common_refresh( refresh_condition: bool, **kwargs, ) -> None: + """ + A common method for handing replicas on refresh. Along with error handling. + """ if not refresh_condition: logger.debug("Refresh called but no refresh enabled.") return @@ -351,91 +354,70 @@ async def _common_refresh( elif self._on_refresh_success: self._on_refresh_success() - async def _refresh_configuration_settings(self, **kwargs: Any) -> None: - - async def refresh_operation(client, headers, **inner_kwargs): - configuration_settings: Optional[List[ConfigurationSetting]] = None - need_refresh = False - reset_secret_timer = False + async def _refresh_operation_configuration(self, client, headers, **kwargs): + configuration_settings: Optional[List[ConfigurationSetting]] = None + need_refresh = False + reset_secret_timer = False - if ( - self._secret_provider.secret_refresh_timer - and self._secret_provider.secret_refresh_timer.needs_refresh() - ): - self._secret_provider.bust_cache() - reset_secret_timer = True - need_refresh = True + if ( + self._secret_provider.secret_refresh_timer + and self._secret_provider.secret_refresh_timer.needs_refresh() + ): + self._secret_provider.bust_cache() + reset_secret_timer = True + need_refresh = True - if not need_refresh: - need_refresh, self._refresh_on, configuration_settings = await client.refresh_configuration_settings( - self._selects, self._refresh_on, headers=headers, **inner_kwargs - ) - else: - # Force a refresh to make sure secrets are up to date - configuration_settings, self._refresh_on = await client.load_configuration_settings( - self._selects, self._refresh_on, headers=headers, **inner_kwargs - ) + if not need_refresh: + need_refresh, self._refresh_on, configuration_settings = await client.refresh_configuration_settings( + self._selects, self._refresh_on, headers=headers, **kwargs + ) + else: + # Force a refresh to make sure secrets are up to date + configuration_settings, self._refresh_on = await client.load_configuration_settings( + self._selects, self._refresh_on, headers=headers, **kwargs + ) - configuration_settings_processed: Dict[str, Any] = {} + configuration_settings_processed: Dict[str, Any] = {} - if configuration_settings is not None: - configuration_settings_processed = await self._process_configurations(configuration_settings) + if configuration_settings is not None: + configuration_settings_processed = await self._process_configurations(configuration_settings) - if need_refresh: - feature_flags = [] - uses_feature_flags = False - if self._dict.get(FEATURE_MANAGEMENT_KEY, {}).get(FEATURE_FLAG_KEY): - uses_feature_flags = True - feature_flags = self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] - self._dict = configuration_settings_processed - if uses_feature_flags: - # If feature flags were already loaded, we need to keep them - self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags - - self._refresh_timer.reset() - if reset_secret_timer and self._secret_provider.secret_refresh_timer: - self._secret_provider.secret_refresh_timer.reset() - - return True - - await self._common_refresh( - refresh_operation=refresh_operation, - error_log_message="Failed to refresh configurations from endpoint %s", - timer=self._refresh_timer, - refresh_condition=bool(self._refresh_on), - **kwargs, - ) + if need_refresh: + feature_flags = [] + uses_feature_flags = False + if self._dict.get(FEATURE_MANAGEMENT_KEY, {}).get(FEATURE_FLAG_KEY): + uses_feature_flags = True + feature_flags = self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] + self._dict = configuration_settings_processed + if uses_feature_flags: + # If feature flags were already loaded, we need to keep them + self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags - async def _refresh_feature_flags(self, **kwargs) -> None: # pylint: disable=too-many-statements - """Refresh feature flags from Azure App Configuration.""" + self._refresh_timer.reset() + if reset_secret_timer and self._secret_provider.secret_refresh_timer: + self._secret_provider.secret_refresh_timer.reset() - async def refresh_operation(client, headers, **inner_kwargs): - need_ff_refresh, refresh_on_feature_flags, feature_flags, filters_used = await client.refresh_feature_flags( - self._refresh_on_feature_flags, - self._feature_flag_selectors, - headers, - self._origin_endpoint or "", - **inner_kwargs, - ) + return True - if refresh_on_feature_flags: - self._refresh_on_feature_flags = refresh_on_feature_flags - self._feature_filter_usage = filters_used + async def _refresh_operation_feature_flags(self, client, headers, **kwargs): + need_ff_refresh, refresh_on_feature_flags, feature_flags, filters_used = await client.refresh_feature_flags( + self._refresh_on_feature_flags, + self._feature_flag_selectors, + headers, + self._origin_endpoint or "", + **kwargs, + ) - if need_ff_refresh: - self._dict[FEATURE_MANAGEMENT_KEY] = {} - self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags + if refresh_on_feature_flags: + self._refresh_on_feature_flags = refresh_on_feature_flags + self._feature_filter_usage = filters_used - self._feature_flag_refresh_timer.reset() - return True + if need_ff_refresh: + self._dict[FEATURE_MANAGEMENT_KEY] = {} + self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags - await self._common_refresh( - refresh_operation=refresh_operation, - error_log_message="Failed to refresh feature flags from endpoint %s", - timer=self._feature_flag_refresh_timer, - refresh_condition=self._feature_flag_refresh_enabled, - **kwargs, - ) + self._feature_flag_refresh_timer.reset() + return True async def refresh(self, **kwargs) -> None: # pylint: disable=too-many-statements if ( @@ -449,17 +431,24 @@ async def refresh(self, **kwargs) -> None: # pylint: disable=too-many-statement logger.debug("Refresh called but refresh already in progress.") return try: - if ( - self._secret_provider.secret_refresh_timer - and self._secret_provider.secret_refresh_timer.needs_refresh() - ): - await self._refresh_configuration_settings(**kwargs) - elif self._refresh_timer and self._refresh_timer.needs_refresh(): - await self._refresh_configuration_settings(**kwargs) + if self._refresh_timer and self._refresh_timer.needs_refresh(): + await self._common_refresh( + refresh_operation=self._refresh_operation_configuration, + error_log_message="Failed to refresh configurations from endpoint %s", + timer=self._refresh_timer, + refresh_condition=bool(self._refresh_on), + **kwargs, + ) if self._feature_flag_refresh_enabled and ( self._feature_flag_refresh_timer and self._feature_flag_refresh_timer.needs_refresh() ): - await self._refresh_feature_flags(**kwargs) + await self._common_refresh( + refresh_operation=self._refresh_operation_feature_flags, + error_log_message="Failed to refresh feature flags from endpoint %s", + timer=self._feature_flag_refresh_timer, + refresh_condition=self._feature_flag_refresh_enabled, + **kwargs, + ) finally: self._refresh_lock.release() From 12dc565480ed27b1aa0cdd7374b748a143672cca Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 29 Aug 2025 11:44:29 -0700 Subject: [PATCH 27/43] Fixing formatting --- .../provider/aio/_azureappconfigurationproviderasync.py | 5 +---- .../appconfiguration/provider/aio/_key_vault/__init__.py | 4 +--- .../tests/aio/key_vault/test_async_secret_provider.py | 8 ++++++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py index a52710d728cf..b4996589668e 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -359,10 +359,7 @@ async def _refresh_operation_configuration(self, client, headers, **kwargs): need_refresh = False reset_secret_timer = False - if ( - self._secret_provider.secret_refresh_timer - and self._secret_provider.secret_refresh_timer.needs_refresh() - ): + if self._secret_provider.secret_refresh_timer and self._secret_provider.secret_refresh_timer.needs_refresh(): self._secret_provider.bust_cache() reset_secret_timer = True need_refresh = True diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/__init__.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/__init__.py index 3be7677c8603..96da02cf4e49 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/__init__.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/__init__.py @@ -6,6 +6,4 @@ from ._async_secret_provider import SecretProvider -__all__ = [ - "SecretProvider" -] +__all__ = ["SecretProvider"] diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py index fb6d2e58b136..b9aa7fa2db93 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py @@ -174,7 +174,9 @@ async def test_resolve_keyvault_reference_with_new_client(self): mock_kv_id.return_value = mock_id_instance # Mock SecretClient creation and get_secret method - with patch("azure.appconfiguration.provider.aio._key_vault._async_secret_provider.SecretClient") as mock_client_class: + with patch( + "azure.appconfiguration.provider.aio._key_vault._async_secret_provider.SecretClient" + ) as mock_client_class: mock_client = Mock() mock_secret = Mock() mock_secret.value = "new-secret-value" @@ -399,7 +401,9 @@ async def test_client_config_specific_credential(self): mock_kv_id.return_value = mock_id_instance # Mock SecretClient creation and get_secret method - with patch("azure.appconfiguration.provider.aio._key_vault._async_secret_provider.SecretClient") as mock_client_class: + with patch( + "azure.appconfiguration.provider.aio._key_vault._async_secret_provider.SecretClient" + ) as mock_client_class: mock_client = Mock() mock_secret = Mock() mock_secret.value = "secret-value" From 2072e76af6df527e8082185d9553ef8f972a656b Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 29 Aug 2025 12:05:10 -0700 Subject: [PATCH 28/43] fixing tox and unneeded "" --- .../provider/_azureappconfigurationprovider.py | 13 +++++++++++-- .../aio/_azureappconfigurationproviderasync.py | 13 +++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py index eccee12ebe30..689070aec53e 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -295,6 +295,15 @@ def _common_refresh( ) -> None: """ A common method for handing replicas on refresh. Along with error handling. + + :param refresh_operation: The refresh operation to execute. + :type refresh_operation: ~typing.Callable + :param error_log_message: The error message to log in case of failure. + :type error_log_message: str + :param timer: The refresh timer. + :type timer: ~._azureappconfigurationproviderbase._RefreshTimer + :param refresh_condition: Condition to check if refresh is needed. + :type refresh_condition: bool """ if not refresh_condition: logger.debug("Refresh called but no refresh enabled.") @@ -393,7 +402,7 @@ def _refresh_operation_feature_flags(self, client, headers, **kwargs): self._refresh_on_feature_flags, self._feature_flag_selectors, headers, - self._origin_endpoint or "", + self._origin_endpoint, **kwargs, ) @@ -474,7 +483,7 @@ def _load_all(self, **kwargs): feature_flags, feature_flag_sentinel_keys, used_filters = client.load_feature_flags( self._feature_flag_selectors, self._feature_flag_refresh_enabled, - self._origin_endpoint or "", + self._origin_endpoint, headers=headers, **kwargs, ) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py index b4996589668e..d229446de943 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -301,6 +301,15 @@ async def _common_refresh( ) -> None: """ A common method for handing replicas on refresh. Along with error handling. + + :param refresh_operation: The refresh operation to execute. + :type refresh_operation: ~typing.Callable + :param error_log_message: The error message to log in case of failure. + :type error_log_message: str + :param timer: The refresh timer. + :type timer: ~._azureappconfigurationproviderbase._RefreshTimer + :param refresh_condition: Condition to check if refresh is needed. + :type refresh_condition: bool """ if not refresh_condition: logger.debug("Refresh called but no refresh enabled.") @@ -401,7 +410,7 @@ async def _refresh_operation_feature_flags(self, client, headers, **kwargs): self._refresh_on_feature_flags, self._feature_flag_selectors, headers, - self._origin_endpoint or "", + self._origin_endpoint, **kwargs, ) @@ -482,7 +491,7 @@ async def _load_all(self, **kwargs): feature_flags, feature_flag_sentinel_keys, used_filters = await client.load_feature_flags( self._feature_flag_selectors, self._feature_flag_refresh_enabled, - self._origin_endpoint or "", + self._origin_endpoint, headers=headers, **kwargs, ) From 4656f83ab56560761477198cc9eda318a9805e7e Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 29 Aug 2025 14:32:59 -0700 Subject: [PATCH 29/43] fixing tox items --- .../aio/_key_vault/_async_secret_provider.py | 6 +- .../key_vault/test_async_secret_provider.py | 79 +++++++++---------- .../tests/key_vault/test_secret_provider.py | 68 ++++++++-------- 3 files changed, 75 insertions(+), 78 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py index 8451564e7dfb..4c1435ee812b 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py @@ -4,6 +4,7 @@ # license information. # ------------------------------------------------------------------------- import inspect +import logging from typing import Mapping, Any, TypeVar, Dict from azure.appconfiguration import SecretReferenceConfigurationSetting # type:ignore # pylint:disable=no-name-in-module from azure.keyvault.secrets.aio import SecretClient @@ -13,6 +14,8 @@ JSON = Mapping[str, Any] _T = TypeVar("_T") +logger = logging.getLogger(__name__) + class SecretProvider(_SecretProviderBase): @@ -54,7 +57,8 @@ async def resolve_keyvault_reference(self, config: SecretReferenceConfigurationS secret_value = self._secret_resolver(config.secret_id) if inspect.isawaitable(secret_value): # Secret resolver was async - secret_value = await secret_value + # Need to ignore type, mypy doesn't like the callback could return `Never` + secret_value = await secret_value # type: ignore if secret_value: if keyvault_identifier.version: diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py index b9aa7fa2db93..72521febe22e 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py @@ -12,6 +12,10 @@ from async_preparers import app_config_decorator_async from testcase import AppConfigTestCase +TEST_SECRET_ID = "https://myvault.vault.azure.net/secrets/mysecret" # cspell:disable-line + +TEST_SECRET_ID_VERSION = TEST_SECRET_ID + "/12345" + class TestSecretProviderAsync(AppConfigTestCase, unittest.IsolatedAsyncioTestCase): @@ -77,14 +81,13 @@ async def test_init_with_secret_refresh_interval(self): async def test_resolve_keyvault_reference_with_cached_secret(self): """Test resolving a Key Vault reference when the secret is in the cache.""" # Create a mock Key Vault reference - secret_id = "https://myvault.vault.azure.net/secrets/mysecret/" - config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=TEST_SECRET_ID) # Create a SecretProvider with a mock credential secret_provider = SecretProvider(keyvault_credential=Mock()) # Add to cache - secret_provider._secret_cache[secret_id] = "cached-secret-value" + secret_provider._secret_cache[TEST_SECRET_ID] = "cached-secret-value" # This should return the cached value without calling SecretClient result = await secret_provider.resolve_keyvault_reference(config) @@ -95,14 +98,13 @@ async def test_resolve_keyvault_reference_with_cached_secret(self): async def test_resolve_keyvault_reference_with_cached_secret_version(self): """Test resolving a Key Vault reference when the secret is in the cache.""" # Create a mock Key Vault reference - secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" - config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=TEST_SECRET_ID_VERSION) # Create a SecretProvider with a mock credential secret_provider = SecretProvider(keyvault_credential=Mock()) # Add to cache - secret_provider._secret_version_cache[secret_id] = "cached-secret-value" + secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] = "cached-secret-value" # This should return the cached value without calling SecretClient result = await secret_provider.resolve_keyvault_reference(config) @@ -113,8 +115,7 @@ async def test_resolve_keyvault_reference_with_cached_secret_version(self): async def test_resolve_keyvault_reference_with_existing_client(self): """Test resolving a Key Vault reference with an existing client.""" # Create a mock Key Vault reference - secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" - config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=TEST_SECRET_ID_VERSION) # Create a SecretProvider with a mock credential mock_credential = Mock() @@ -134,8 +135,8 @@ async def test_resolve_keyvault_reference_with_existing_client(self): # Setup key vault identifier mock with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() - mock_id_instance._resource_id = secret_id - mock_id_instance.source_id = secret_id + mock_id_instance._resource_id = TEST_SECRET_ID_VERSION + mock_id_instance.source_id = TEST_SECRET_ID_VERSION mock_id_instance.name = "mysecret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" @@ -151,13 +152,12 @@ async def test_resolve_keyvault_reference_with_existing_client(self): self.assertEqual(result, "secret-value") mock_client.get_secret.assert_called_once_with(mock_id_instance.name, version=mock_id_instance.version) # Verify the secret was cached - self.assertEqual(secret_provider._secret_version_cache[secret_id], "secret-value") + self.assertEqual(secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION], "secret-value") async def test_resolve_keyvault_reference_with_new_client(self): """Test resolving a Key Vault reference by creating a new client.""" # Create a mock Key Vault reference - secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" - config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=TEST_SECRET_ID_VERSION) # Create a SecretProvider with a mock credential mock_credential = Mock() @@ -166,8 +166,8 @@ async def test_resolve_keyvault_reference_with_new_client(self): # Setup key vault identifier mock with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() - mock_id_instance._resource_id = secret_id - mock_id_instance.source_id = secret_id + mock_id_instance._resource_id = TEST_SECRET_ID_VERSION + mock_id_instance.source_id = TEST_SECRET_ID_VERSION mock_id_instance.name = "mysecret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" @@ -199,13 +199,12 @@ async def test_resolve_keyvault_reference_with_new_client(self): # Verify the client was cached self.assertEqual(secret_provider._secret_clients[vault_url], mock_client) # Verify the secret was cached - self.assertEqual(secret_provider._secret_version_cache[secret_id], "new-secret-value") + self.assertEqual(secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION], "new-secret-value") async def test_resolve_keyvault_reference_with_secret_resolver(self): """Test resolving a Key Vault reference using a secret resolver.""" # Create a mock Key Vault reference - secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" - config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=TEST_SECRET_ID_VERSION) # Create a mock secret resolver mock_resolver = Mock(return_value="resolved-secret-value") @@ -216,8 +215,8 @@ async def test_resolve_keyvault_reference_with_secret_resolver(self): # Setup key vault identifier mock with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() - mock_id_instance._resource_id = secret_id - mock_id_instance.source_id = secret_id + mock_id_instance._resource_id = TEST_SECRET_ID_VERSION + mock_id_instance.source_id = TEST_SECRET_ID_VERSION mock_id_instance.name = "mysecret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" @@ -232,15 +231,14 @@ async def test_resolve_keyvault_reference_with_secret_resolver(self): # Verify the result self.assertEqual(result, "resolved-secret-value") - mock_resolver.assert_called_once_with(secret_id) + mock_resolver.assert_called_once_with(TEST_SECRET_ID_VERSION) # Verify the secret was cached - self.assertEqual(secret_provider._secret_version_cache[secret_id], "resolved-secret-value") + self.assertEqual(secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION], "resolved-secret-value") async def test_resolve_keyvault_reference_with_async_secret_resolver(self): """Test resolving a Key Vault reference using an async secret resolver.""" # Create a mock Key Vault reference - secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" - config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=TEST_SECRET_ID_VERSION) # Create a mock async secret resolver async def async_resolver(secret_id): @@ -252,8 +250,8 @@ async def async_resolver(secret_id): # Setup key vault identifier mock with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() - mock_id_instance._resource_id = secret_id - mock_id_instance.source_id = secret_id + mock_id_instance._resource_id = TEST_SECRET_ID_VERSION + mock_id_instance.source_id = TEST_SECRET_ID_VERSION mock_id_instance.name = "mysecret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" @@ -269,13 +267,14 @@ async def async_resolver(secret_id): # Verify the result self.assertEqual(result, "async-resolved-secret-value") # Verify the secret was cached - self.assertEqual(secret_provider._secret_version_cache[secret_id], "async-resolved-secret-value") + self.assertEqual( + secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION], "async-resolved-secret-value" + ) async def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self): """Test falling back to a secret resolver if the client fails to get the secret.""" # Create a mock Key Vault reference - secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" - config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=TEST_SECRET_ID_VERSION) # Create a mock credential and secret resolver mock_credential = Mock() @@ -297,8 +296,8 @@ async def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self # Setup key vault identifier mock with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() - mock_id_instance._resource_id = secret_id - mock_id_instance.source_id = secret_id + mock_id_instance._resource_id = TEST_SECRET_ID_VERSION + mock_id_instance.source_id = TEST_SECRET_ID_VERSION mock_id_instance.name = "mysecret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" @@ -313,15 +312,14 @@ async def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self # Verify the result self.assertEqual(result, "fallback-secret-value") mock_client.get_secret.assert_called_once_with(mock_id_instance.name, version=mock_id_instance.version) - mock_resolver.assert_called_once_with(secret_id) + mock_resolver.assert_called_once_with(TEST_SECRET_ID_VERSION) # Verify the secret was cached - self.assertEqual(secret_provider._secret_version_cache[secret_id], "fallback-secret-value") + self.assertEqual(secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION], "fallback-secret-value") async def test_resolve_keyvault_reference_no_client_no_resolver(self): """Test that an error is raised when no client or resolver can resolve the reference.""" # Create a mock Key Vault reference - secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" - config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=TEST_SECRET_ID_VERSION) # Create a SecretProvider with a credential but no clients or resolvers mock_credential = Mock() @@ -330,8 +328,8 @@ async def test_resolve_keyvault_reference_no_client_no_resolver(self): # Setup key vault identifier mock with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() - mock_id_instance._resource_id = secret_id - mock_id_instance.source_id = secret_id + mock_id_instance._resource_id = TEST_SECRET_ID_VERSION + mock_id_instance.source_id = TEST_SECRET_ID_VERSION mock_id_instance.name = "mysecret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" @@ -373,8 +371,7 @@ async def test_close(self): async def test_client_config_specific_credential(self): """Test that client configuration can specify a specific credential.""" # Create a mock Key Vault reference - secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" - config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=TEST_SECRET_ID_VERSION) # Create mock credentials mock_default_credential = Mock(name="default_credential") @@ -393,8 +390,8 @@ async def test_client_config_specific_credential(self): # Setup key vault identifier mock with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() - mock_id_instance._resource_id = secret_id - mock_id_instance.source_id = secret_id + mock_id_instance._resource_id = TEST_SECRET_ID_VERSION + mock_id_instance.source_id = TEST_SECRET_ID_VERSION mock_id_instance.name = "mysecret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/key_vault/test_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/key_vault/test_secret_provider.py index 8971aa3661af..cda775b6c26c 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/key_vault/test_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/key_vault/test_secret_provider.py @@ -12,6 +12,10 @@ from preparers import app_config_decorator_aad from testcase import AppConfigTestCase +TEST_SECRET_ID = "https://myvault.vault.azure.net/secrets/mysecret" # cspell:disable-line + +TEST_SECRET_ID_VERSION = TEST_SECRET_ID + "/12345" + class TestSecretProvider(AppConfigTestCase, unittest.TestCase): @@ -77,14 +81,13 @@ def test_init_with_secret_refresh_interval(self): def test_resolve_keyvault_reference_with_cached_secret(self): """Test resolving a Key Vault reference when the secret is in the cache.""" # Create a mock Key Vault reference - secret_id = "https://myvault.vault.azure.net/secrets/mysecret/" - config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=TEST_SECRET_ID) # Create a SecretProvider with a mock credential secret_provider = SecretProvider(keyvault_credential=Mock()) # Add to cache - secret_provider._secret_cache[secret_id] = "cached-secret-value" + secret_provider._secret_cache[TEST_SECRET_ID] = "cached-secret-value" # This should return the cached value without calling SecretClient result = secret_provider.resolve_keyvault_reference(config) @@ -95,14 +98,13 @@ def test_resolve_keyvault_reference_with_cached_secret(self): def test_resolve_keyvault_reference_with_cached_secret_version(self): """Test resolving a Key Vault reference when the secret is in the cache.""" # Create a mock Key Vault reference - secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" - config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=TEST_SECRET_ID_VERSION) # Create a SecretProvider with a mock credential secret_provider = SecretProvider(keyvault_credential=Mock()) # Add to cache - secret_provider._secret_version_cache[secret_id] = "cached-secret-value" + secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] = "cached-secret-value" # This should return the cached value without calling SecretClient result = secret_provider.resolve_keyvault_reference(config) @@ -113,8 +115,7 @@ def test_resolve_keyvault_reference_with_cached_secret_version(self): def test_resolve_keyvault_reference_with_existing_client(self): """Test resolving a Key Vault reference with an existing client.""" # Create a mock Key Vault reference - secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" - config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=TEST_SECRET_ID_VERSION) # Create a SecretProvider with a mock credential mock_credential = Mock() @@ -133,8 +134,8 @@ def test_resolve_keyvault_reference_with_existing_client(self): # Setup key vault identifier mock with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() - mock_id_instance._resource_id = secret_id - mock_id_instance.source_id = secret_id + mock_id_instance._resource_id = TEST_SECRET_ID_VERSION + mock_id_instance.source_id = TEST_SECRET_ID_VERSION mock_id_instance.name = "mysecret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" @@ -150,13 +151,12 @@ def test_resolve_keyvault_reference_with_existing_client(self): self.assertEqual(result, "secret-value") mock_client.get_secret.assert_called_once_with(mock_id_instance.name, version=mock_id_instance.version) # Verify the secret was cached - self.assertEqual(secret_provider._secret_version_cache[secret_id], "secret-value") + self.assertEqual(secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION], "secret-value") def test_resolve_keyvault_reference_with_new_client(self): """Test resolving a Key Vault reference by creating a new client.""" # Create a mock Key Vault reference - secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" - config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=TEST_SECRET_ID_VERSION) # Create a SecretProvider with a mock credential mock_credential = Mock() @@ -165,8 +165,8 @@ def test_resolve_keyvault_reference_with_new_client(self): # Setup key vault identifier mock with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() - mock_id_instance._resource_id = secret_id - mock_id_instance.source_id = secret_id + mock_id_instance._resource_id = TEST_SECRET_ID_VERSION + mock_id_instance.source_id = TEST_SECRET_ID_VERSION mock_id_instance.name = "mysecret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" @@ -196,13 +196,12 @@ def test_resolve_keyvault_reference_with_new_client(self): # Verify the client was cached self.assertEqual(secret_provider._secret_clients[vault_url], mock_client) # Verify the secret was cached - self.assertEqual(secret_provider._secret_version_cache[secret_id], "new-secret-value") + self.assertEqual(secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION], "new-secret-value") def test_resolve_keyvault_reference_with_secret_resolver(self): """Test resolving a Key Vault reference using a secret resolver.""" # Create a mock Key Vault reference - secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" - config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=TEST_SECRET_ID_VERSION) # Create a mock secret resolver mock_resolver = Mock(return_value="resolved-secret-value") @@ -213,8 +212,8 @@ def test_resolve_keyvault_reference_with_secret_resolver(self): # Setup key vault identifier mock with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() - mock_id_instance._resource_id = secret_id - mock_id_instance.source_id = secret_id + mock_id_instance._resource_id = TEST_SECRET_ID_VERSION + mock_id_instance.source_id = TEST_SECRET_ID_VERSION mock_id_instance.name = "mysecret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" @@ -229,15 +228,14 @@ def test_resolve_keyvault_reference_with_secret_resolver(self): # Verify the result self.assertEqual(result, "resolved-secret-value") - mock_resolver.assert_called_once_with(secret_id) + mock_resolver.assert_called_once_with(TEST_SECRET_ID_VERSION) # Verify the secret was cached - self.assertEqual(secret_provider._secret_version_cache[secret_id], "resolved-secret-value") + self.assertEqual(secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION], "resolved-secret-value") def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self): """Test falling back to a secret resolver if the client fails to get the secret.""" # Create a mock Key Vault reference - secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" - config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=TEST_SECRET_ID_VERSION) # Create a mock credential and secret resolver mock_credential = Mock() @@ -257,8 +255,8 @@ def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self): # Setup key vault identifier mock with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() - mock_id_instance._resource_id = secret_id - mock_id_instance.source_id = secret_id + mock_id_instance._resource_id = TEST_SECRET_ID_VERSION + mock_id_instance.source_id = TEST_SECRET_ID_VERSION mock_id_instance.name = "mysecret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" @@ -273,15 +271,14 @@ def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self): # Verify the result self.assertEqual(result, "fallback-secret-value") mock_client.get_secret.assert_called_once_with(mock_id_instance.name, version=mock_id_instance.version) - mock_resolver.assert_called_once_with(secret_id) + mock_resolver.assert_called_once_with(TEST_SECRET_ID_VERSION) # Verify the secret was cached - self.assertEqual(secret_provider._secret_version_cache[secret_id], "fallback-secret-value") + self.assertEqual(secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION], "fallback-secret-value") def test_resolve_keyvault_reference_no_client_no_resolver(self): """Test that an error is raised when no client or resolver can resolve the reference.""" # Create a mock Key Vault reference - secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" - config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=TEST_SECRET_ID_VERSION) # Create a SecretProvider with a credential but no clients or resolvers mock_credential = Mock() @@ -290,8 +287,8 @@ def test_resolve_keyvault_reference_no_client_no_resolver(self): # Setup key vault identifier mock with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() - mock_id_instance._resource_id = secret_id - mock_id_instance.source_id = secret_id + mock_id_instance._resource_id = TEST_SECRET_ID_VERSION + mock_id_instance.source_id = TEST_SECRET_ID_VERSION mock_id_instance.name = "mysecret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" @@ -330,8 +327,7 @@ def test_close(self): def test_client_config_specific_credential(self): """Test that client configuration can specify a specific credential.""" # Create a mock Key Vault reference - secret_id = "https://myvault.vault.azure.net/secrets/mysecret/12345" - config = SecretReferenceConfigurationSetting(key="test-key", secret_id=secret_id) + config = SecretReferenceConfigurationSetting(key="test-key", secret_id=TEST_SECRET_ID_VERSION) # Create mock credentials mock_default_credential = Mock(name="default_credential") @@ -350,8 +346,8 @@ def test_client_config_specific_credential(self): # Setup key vault identifier mock with patch("azure.keyvault.secrets.KeyVaultSecretIdentifier") as mock_kv_id: mock_id_instance = Mock() - mock_id_instance._resource_id = secret_id - mock_id_instance.source_id = secret_id + mock_id_instance._resource_id = TEST_SECRET_ID_VERSION + mock_id_instance.source_id = TEST_SECRET_ID_VERSION mock_id_instance.name = "mysecret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" From 08e5ada20bb185f2f4df88b63381149b705ee94f Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 29 Aug 2025 16:35:56 -0700 Subject: [PATCH 30/43] fix cspell + tests recording --- .../azure-appconfiguration-provider/assets.json | 2 +- .../aio/key_vault/test_async_secret_provider.py | 16 ++++++++-------- .../tests/key_vault/test_secret_provider.py | 15 +++++++-------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json index a9f86aaa62c1..0402f81d5941 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json +++ b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/appconfiguration/azure-appconfiguration-provider", - "Tag": "python/appconfiguration/azure-appconfiguration-provider_1250228ab3" + "Tag": "python/appconfiguration/azure-appconfiguration-provider_4d945b1799" } diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py index 72521febe22e..c0a63e9d4ec5 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py @@ -12,7 +12,7 @@ from async_preparers import app_config_decorator_async from testcase import AppConfigTestCase -TEST_SECRET_ID = "https://myvault.vault.azure.net/secrets/mysecret" # cspell:disable-line +TEST_SECRET_ID = "https://myvault.vault.azure.net/secrets/my_secret" TEST_SECRET_ID_VERSION = TEST_SECRET_ID + "/12345" @@ -137,7 +137,7 @@ async def test_resolve_keyvault_reference_with_existing_client(self): mock_id_instance = Mock() mock_id_instance._resource_id = TEST_SECRET_ID_VERSION mock_id_instance.source_id = TEST_SECRET_ID_VERSION - mock_id_instance.name = "mysecret" + mock_id_instance.name = "my_secret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance @@ -168,7 +168,7 @@ async def test_resolve_keyvault_reference_with_new_client(self): mock_id_instance = Mock() mock_id_instance._resource_id = TEST_SECRET_ID_VERSION mock_id_instance.source_id = TEST_SECRET_ID_VERSION - mock_id_instance.name = "mysecret" + mock_id_instance.name = "my_secret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance @@ -217,7 +217,7 @@ async def test_resolve_keyvault_reference_with_secret_resolver(self): mock_id_instance = Mock() mock_id_instance._resource_id = TEST_SECRET_ID_VERSION mock_id_instance.source_id = TEST_SECRET_ID_VERSION - mock_id_instance.name = "mysecret" + mock_id_instance.name = "my_secret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance @@ -252,7 +252,7 @@ async def async_resolver(secret_id): mock_id_instance = Mock() mock_id_instance._resource_id = TEST_SECRET_ID_VERSION mock_id_instance.source_id = TEST_SECRET_ID_VERSION - mock_id_instance.name = "mysecret" + mock_id_instance.name = "my_secret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance @@ -298,7 +298,7 @@ async def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self mock_id_instance = Mock() mock_id_instance._resource_id = TEST_SECRET_ID_VERSION mock_id_instance.source_id = TEST_SECRET_ID_VERSION - mock_id_instance.name = "mysecret" + mock_id_instance.name = "my_secret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance @@ -330,7 +330,7 @@ async def test_resolve_keyvault_reference_no_client_no_resolver(self): mock_id_instance = Mock() mock_id_instance._resource_id = TEST_SECRET_ID_VERSION mock_id_instance.source_id = TEST_SECRET_ID_VERSION - mock_id_instance.name = "mysecret" + mock_id_instance.name = "my_secret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance @@ -392,7 +392,7 @@ async def test_client_config_specific_credential(self): mock_id_instance = Mock() mock_id_instance._resource_id = TEST_SECRET_ID_VERSION mock_id_instance.source_id = TEST_SECRET_ID_VERSION - mock_id_instance.name = "mysecret" + mock_id_instance.name = "my_secret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/key_vault/test_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/key_vault/test_secret_provider.py index cda775b6c26c..ada500428368 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/key_vault/test_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/key_vault/test_secret_provider.py @@ -12,8 +12,7 @@ from preparers import app_config_decorator_aad from testcase import AppConfigTestCase -TEST_SECRET_ID = "https://myvault.vault.azure.net/secrets/mysecret" # cspell:disable-line - +TEST_SECRET_ID = "https://myvault.vault.azure.net/secrets/my_secret" TEST_SECRET_ID_VERSION = TEST_SECRET_ID + "/12345" @@ -136,7 +135,7 @@ def test_resolve_keyvault_reference_with_existing_client(self): mock_id_instance = Mock() mock_id_instance._resource_id = TEST_SECRET_ID_VERSION mock_id_instance.source_id = TEST_SECRET_ID_VERSION - mock_id_instance.name = "mysecret" + mock_id_instance.name = "my_secret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance @@ -167,7 +166,7 @@ def test_resolve_keyvault_reference_with_new_client(self): mock_id_instance = Mock() mock_id_instance._resource_id = TEST_SECRET_ID_VERSION mock_id_instance.source_id = TEST_SECRET_ID_VERSION - mock_id_instance.name = "mysecret" + mock_id_instance.name = "my_secret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance @@ -214,7 +213,7 @@ def test_resolve_keyvault_reference_with_secret_resolver(self): mock_id_instance = Mock() mock_id_instance._resource_id = TEST_SECRET_ID_VERSION mock_id_instance.source_id = TEST_SECRET_ID_VERSION - mock_id_instance.name = "mysecret" + mock_id_instance.name = "my_secret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance @@ -257,7 +256,7 @@ def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self): mock_id_instance = Mock() mock_id_instance._resource_id = TEST_SECRET_ID_VERSION mock_id_instance.source_id = TEST_SECRET_ID_VERSION - mock_id_instance.name = "mysecret" + mock_id_instance.name = "my_secret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance @@ -289,7 +288,7 @@ def test_resolve_keyvault_reference_no_client_no_resolver(self): mock_id_instance = Mock() mock_id_instance._resource_id = TEST_SECRET_ID_VERSION mock_id_instance.source_id = TEST_SECRET_ID_VERSION - mock_id_instance.name = "mysecret" + mock_id_instance.name = "my_secret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance @@ -348,7 +347,7 @@ def test_client_config_specific_credential(self): mock_id_instance = Mock() mock_id_instance._resource_id = TEST_SECRET_ID_VERSION mock_id_instance.source_id = TEST_SECRET_ID_VERSION - mock_id_instance.name = "mysecret" + mock_id_instance.name = "my_secret" mock_id_instance.version = "12345" mock_id_instance.vault_url = "https://myvault.vault.azure.net" mock_kv_id.return_value = mock_id_instance From 0599f685ad66505b539c6ddb4664d89fe8ba9785 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 2 Sep 2025 09:30:37 -0700 Subject: [PATCH 31/43] Update test_async_secret_provider.py --- .../tests/aio/key_vault/test_async_secret_provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py index c0a63e9d4ec5..6bcf80544662 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py @@ -492,7 +492,7 @@ async def test_integration_with_keyvault( self.skipTest("No Key Vault secret URL provided") # Get a credential - credential = self.get_credential(SecretClient) + credential = self.get_credential(SecretClient, is_async=True) # Create a SecretProvider with the credential secret_provider = SecretProvider(keyvault_credential=credential) From 35a05bf9f569a615ec5319608ead9749bc4195f0 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 30 Sep 2025 14:55:10 -0700 Subject: [PATCH 32/43] Post Merge updates --- .../azure-appconfiguration-provider/assets.json | 2 +- .../provider/_azureappconfigurationprovider.py | 5 ++--- .../provider/aio/_azureappconfigurationproviderasync.py | 9 ++++----- .../tests/test_async_provider_refresh.py | 8 ++++---- .../tests/test_provider_refresh.py | 8 ++++---- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json index d61b5fc2f219..5ca3d7d5b3ed 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json +++ b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/appconfiguration/azure-appconfiguration-provider", - "Tag": "python/appconfiguration/azure-appconfiguration-provider_8a72ac47e0" + "Tag": "python/appconfiguration/azure-appconfiguration-provider_ab0cded533" } diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py index bbf685b0d86d..419d5e05895c 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -30,7 +30,6 @@ ) from ._azureappconfigurationproviderbase import ( AzureAppConfigurationProviderBase, - _RefreshTimer, update_correlation_context_header, delay_failure, sdk_allowed_kwargs, @@ -288,7 +287,7 @@ def _attempt_refresh(self, client: ConfigurationClient, replica_count: int, is_f replica_count, self._feature_flag_enabled, self._feature_filter_usage, - self._uses_key_vault, + self._secret_provider.uses_key_vault, self._uses_load_balancing, is_failover_request, self._uses_ai_configuration, @@ -492,7 +491,7 @@ def _process_configurations(self, configuration_settings: List[ConfigurationSett def _process_key_value(self, config: ConfigurationSetting) -> Any: if isinstance(config, SecretReferenceConfigurationSetting): - return _resolve_keyvault_reference(config, self) + return self._secret_provider.resolve_keyvault_reference(config) # Use the base class helper method for non-KeyVault processing return self._process_key_value_base(config) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py index d91c932048c5..7d8411e4fcb8 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -5,7 +5,6 @@ # ------------------------------------------------------------------------- import datetime import logging -import inspect from typing import ( Any, Awaitable, @@ -25,8 +24,6 @@ SecretReferenceConfigurationSetting, ) from azure.core.exceptions import AzureError, HttpResponseError -from azure.keyvault.secrets.aio import SecretClient -from azure.keyvault.secrets import KeyVaultSecretIdentifier from .._models import AzureAppConfigurationKeyVaultOptions, SettingSelector from ._key_vault._async_secret_provider import SecretProvider from .._constants import ( @@ -299,7 +296,7 @@ async def _attempt_refresh( replica_count, self._feature_flag_enabled, self._feature_filter_usage, - self._uses_key_vault, + self._secret_provider.uses_key_vault, self._uses_load_balancing, is_failover_request, self._uses_ai_configuration, @@ -377,6 +374,8 @@ async def _attempt_refresh( raise e async def refresh(self, **kwargs) -> None: + if self._secret_provider.secret_refresh_timer and self._secret_provider.secret_refresh_timer.needs_refresh(): + self._secret_provider.bust_cache() if not self._watched_settings and not self._feature_flag_refresh_enabled: logger.debug("Refresh called but no refresh enabled.") return @@ -507,7 +506,7 @@ async def _process_configurations(self, configuration_settings: List[Configurati async def _process_key_value(self, config: ConfigurationSetting) -> Any: if isinstance(config, SecretReferenceConfigurationSetting): - return await _resolve_keyvault_reference(config, self) + return await self._secret_provider.resolve_keyvault_reference(config) # Use the base class helper method for non-KeyVault processing return self._process_key_value_base(config) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_refresh.py index 821d7cbf574e..0fb2857f68c2 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_refresh.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_async_provider_refresh.py @@ -57,7 +57,7 @@ async def test_refresh(self, appconfiguration_endpoint_string, appconfiguration_ await client.refresh() assert client["refresh_message"] == "updated value" assert has_feature_flag(client, "Alpha", True) - assert mock_callback.call_count == 2 + assert mock_callback.call_count == 1 setting.value = "original value" feature_flag.enabled = False @@ -70,7 +70,7 @@ async def test_refresh(self, appconfiguration_endpoint_string, appconfiguration_ await client.refresh() assert client["refresh_message"] == "original value" assert has_feature_flag(client, "Alpha", False) - assert mock_callback.call_count == 4 + assert mock_callback.call_count == 2 setting.value = "updated value 2" feature_flag.enabled = True @@ -81,14 +81,14 @@ async def test_refresh(self, appconfiguration_endpoint_string, appconfiguration_ await client.refresh() assert client["refresh_message"] == "original value" assert has_feature_flag(client, "Alpha", False) - assert mock_callback.call_count == 4 + assert mock_callback.call_count == 2 setting.value = "original value" await appconfig_client.set_configuration_setting(setting) await client.refresh() assert client["refresh_message"] == "original value" - assert mock_callback.call_count == 4 + assert mock_callback.call_count == 2 # method: refresh @app_config_decorator_async diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_refresh.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_refresh.py index 895b593b61b5..588ead16c0a1 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_refresh.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_refresh.py @@ -48,7 +48,7 @@ def test_refresh(self, appconfiguration_endpoint_string, appconfiguration_keyvau client.refresh() assert client["refresh_message"] == "updated value" assert has_feature_flag(client, "Alpha", True) - assert mock_callback.call_count == 2 + assert mock_callback.call_count == 1 setting.value = "original value" feature_flag.enabled = False @@ -61,7 +61,7 @@ def test_refresh(self, appconfiguration_endpoint_string, appconfiguration_keyvau client.refresh() assert client["refresh_message"] == "original value" assert has_feature_flag(client, "Alpha", False) - assert mock_callback.call_count == 4 + assert mock_callback.call_count == 2 setting.value = "updated value 2" feature_flag.enabled = True @@ -72,14 +72,14 @@ def test_refresh(self, appconfiguration_endpoint_string, appconfiguration_keyvau client.refresh() assert client["refresh_message"] == "original value" assert has_feature_flag(client, "Alpha", False) - assert mock_callback.call_count == 4 + assert mock_callback.call_count == 2 setting.value = "original value" appconfig_client.set_configuration_setting(setting) client.refresh() assert client["refresh_message"] == "original value" - assert mock_callback.call_count == 4 + assert mock_callback.call_count == 2 # method: refresh @recorded_by_proxy From 8e04133a14a6a050dd1f81c767f69232ff2cdd22 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 30 Sep 2025 15:17:52 -0700 Subject: [PATCH 33/43] Move cache to shared code --- .../provider/_key_vault/_secret_provider.py | 11 ++--------- .../provider/_key_vault/_secret_provider_base.py | 10 ++++++++++ .../aio/_key_vault/_async_secret_provider.py | 15 ++------------- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py index 9e078c1430ba..eeee69fe949e 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py @@ -3,14 +3,13 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # ------------------------------------------------------------------------- -from typing import Mapping, Any, TypeVar, Dict +from typing import Mapping, Any, Dict from azure.appconfiguration import SecretReferenceConfigurationSetting # type:ignore # pylint:disable=no-name-in-module from azure.keyvault.secrets import SecretClient from azure.core.exceptions import ServiceRequestError from ._secret_provider_base import _SecretProviderBase JSON = Mapping[str, Any] -_T = TypeVar("_T") class SecretProvider(_SecretProviderBase): @@ -51,14 +50,8 @@ def resolve_keyvault_reference(self, config: SecretReferenceConfigurationSetting if self._secret_resolver and secret_value is None: secret_value = self._secret_resolver(config.secret_id) - if secret_value: - if keyvault_identifier.version: - self._secret_version_cache[keyvault_identifier.source_id] = secret_value - else: - self._secret_cache[keyvault_identifier.source_id] = secret_value - return secret_value - raise ValueError("No Secret Client found for Key Vault reference %s" % (vault_url)) + return self._cache_value(keyvault_identifier, secret_value, vault_url) def close(self) -> None: """ diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py index 95c2b6895fa3..0164393d0758 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py @@ -47,6 +47,16 @@ def bust_cache(self) -> None: """ self._secret_cache = {} + def _cache_value(self, keyvault_identifier: KeyVaultSecretIdentifier, secret_value: Any, vault_url: str) -> str: + if secret_value: + if keyvault_identifier.version: + self._secret_version_cache[keyvault_identifier.source_id] = secret_value + else: + self._secret_cache[keyvault_identifier.source_id] = secret_value + return secret_value + + raise ValueError("No Secret Client found for Key Vault reference %s" % (vault_url)) + def resolve_keyvault_reference_base( self, config: SecretReferenceConfigurationSetting ) -> Tuple[KeyVaultSecretIdentifier, str]: diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py index 4c1435ee812b..415cb9998438 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py @@ -4,17 +4,13 @@ # license information. # ------------------------------------------------------------------------- import inspect -import logging -from typing import Mapping, Any, TypeVar, Dict +from typing import Mapping, Any, Dict from azure.appconfiguration import SecretReferenceConfigurationSetting # type:ignore # pylint:disable=no-name-in-module from azure.keyvault.secrets.aio import SecretClient from azure.core.exceptions import ServiceRequestError from ..._key_vault._secret_provider_base import _SecretProviderBase JSON = Mapping[str, Any] -_T = TypeVar("_T") - -logger = logging.getLogger(__name__) class SecretProvider(_SecretProviderBase): @@ -60,14 +56,7 @@ async def resolve_keyvault_reference(self, config: SecretReferenceConfigurationS # Need to ignore type, mypy doesn't like the callback could return `Never` secret_value = await secret_value # type: ignore - if secret_value: - if keyvault_identifier.version: - self._secret_version_cache[keyvault_identifier.source_id] = secret_value - else: - self._secret_cache[keyvault_identifier.source_id] = secret_value - return secret_value - - raise ValueError("No Secret Client found for Key Vault reference %s" % (vault_url)) + return self._cache_value(keyvault_identifier, secret_value, vault_url) async def close(self) -> None: """ From f7ffe3f2faf619fe19ce4ac54477747bc59fce38 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 30 Sep 2025 15:30:01 -0700 Subject: [PATCH 34/43] removed unneeded disabled --- .../appconfiguration/provider/_key_vault/_secret_provider.py | 1 - .../provider/aio/_key_vault/_async_secret_provider.py | 1 - 2 files changed, 2 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py index eeee69fe949e..8add4a7dffa2 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py @@ -28,7 +28,6 @@ def resolve_keyvault_reference(self, config: SecretReferenceConfigurationSetting if keyvault_identifier.source_id in self._secret_version_cache: return self._secret_version_cache[keyvault_identifier.source_id] - # pylint:disable=protected-access referenced_client = self._secret_clients.get(vault_url, None) vault_config = self._keyvault_client_configs.get(vault_url, {}) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py index 415cb9998438..d76f4aa7e6fe 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py @@ -29,7 +29,6 @@ async def resolve_keyvault_reference(self, config: SecretReferenceConfigurationS if keyvault_identifier.source_id in self._secret_version_cache: return self._secret_version_cache[keyvault_identifier.source_id] - # pylint:disable=protected-access referenced_client = self._secret_clients.get(vault_url, None) vault_config = self._keyvault_client_configs.get(vault_url, {}) From 3ebcf45395759fd92150057dffa55742a1ea16a3 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 2 Oct 2025 14:26:47 -0700 Subject: [PATCH 35/43] Update Secret Provider --- .../_azureappconfigurationprovider.py | 8 +- .../_azureappconfigurationproviderbase.py | 15 +++ .../provider/_key_vault/_secret_provider.py | 31 +++++-- .../_key_vault/_secret_provider_base.py | 92 ++----------------- .../_azureappconfigurationproviderasync.py | 10 +- .../aio/_key_vault/_async_secret_provider.py | 30 ++++-- 6 files changed, 78 insertions(+), 108 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py index 419d5e05895c..969b0c1ca9c0 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -377,6 +377,8 @@ def refresh(self, **kwargs) -> None: exception: Optional[Exception] = None is_failover_request = False try: + if self._secret_provider.secret_refresh_timer and self._secret_provider.secret_refresh_timer.needs_refresh(): + secrets = self._secret_provider.refresh_secrets() self._replica_client_manager.refresh_clients() self._replica_client_manager.find_active_clients() replica_count = self._replica_client_manager.get_client_count() - 1 @@ -473,9 +475,13 @@ def _load_all(self, **kwargs): raise exception def _process_configurations(self, configuration_settings: List[ConfigurationSetting]) -> Dict[str, Any]: + # configuration_settings can contain duplicate keys, but they are in priority order, i.e. later settings take + # precedence. Only process the settings with the highest priority (i.e. the last one in the list). + unique_settings = self._deduplicate_settings(configuration_settings) + configuration_settings_processed = {} feature_flags_processed = [] - for settings in configuration_settings: + for settings in unique_settings.values(): if isinstance(settings, FeatureFlagConfigurationSetting): # Feature flags are not processed like other settings feature_flag_value = self._process_feature_flag(settings) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py index 52b364b88154..8e569be299e8 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -589,3 +589,18 @@ def _update_watched_feature_flags( for feature_flag in feature_flags: watched_feature_flags[(feature_flag.key, feature_flag.label)] = feature_flag.etag return watched_feature_flags + + def _deduplicate_settings( + self, configuration_settings: List[ConfigurationSetting] + ) -> Dict[str, ConfigurationSetting]: + """ + Deduplicates configuration settings by key. + + :param List[ConfigurationSetting] configuration_settings: The list of configuration settings to deduplicate + :return: A dictionary mapping keys to their unique configuration settings + :rtype: Dict[str, ConfigurationSetting] + """ + unique_settings: Dict[str, ConfigurationSetting] = {} + for settings in configuration_settings: + unique_settings[settings.key] = settings + return unique_settings diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py index 8add4a7dffa2..0febe74b5d84 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py @@ -5,7 +5,7 @@ # ------------------------------------------------------------------------- from typing import Mapping, Any, Dict from azure.appconfiguration import SecretReferenceConfigurationSetting # type:ignore # pylint:disable=no-name-in-module -from azure.keyvault.secrets import SecretClient +from azure.keyvault.secrets import SecretClient, KeyVaultSecretIdentifier from azure.core.exceptions import ServiceRequestError from ._secret_provider_base import _SecretProviderBase @@ -24,33 +24,44 @@ def __init__(self, **kwargs: Any) -> None: def resolve_keyvault_reference(self, config: SecretReferenceConfigurationSetting) -> str: keyvault_identifier, vault_url = self.resolve_keyvault_reference_base(config) if keyvault_identifier.source_id in self._secret_cache: - return self._secret_cache[keyvault_identifier.source_id] + value, _ = self._secret_cache[keyvault_identifier.source_id] + return value if keyvault_identifier.source_id in self._secret_version_cache: - return self._secret_version_cache[keyvault_identifier.source_id] + value, _ = self._secret_version_cache[keyvault_identifier.source_id] + return value - referenced_client = self._secret_clients.get(vault_url, None) + return self.__get_secret_value(keyvault_identifier.source_id, keyvault_identifier) - vault_config = self._keyvault_client_configs.get(vault_url, {}) + def refresh_secrets(self) -> None: + original_cache = self._secret_cache.copy() + self._secret_cache.clear() + for secret_id, (_, secret_identifier) in original_cache.items(): + self._secret_cache[secret_id] = self.__get_secret_value(secret_id, secret_identifier), secret_identifier + + def __get_secret_value(self, secret_id: str, secret_identifier: KeyVaultSecretIdentifier) -> str: + referenced_client = self._secret_clients.get(secret_identifier.vault_url, None) + + vault_config = self._keyvault_client_configs.get(secret_identifier.vault_url, {}) credential = vault_config.pop("credential", self._keyvault_credential) if referenced_client is None and credential is not None: - referenced_client = SecretClient(vault_url=vault_url, credential=credential, **vault_config) - self._secret_clients[vault_url] = referenced_client + referenced_client = SecretClient(vault_url=secret_identifier.vault_url, credential=credential, **vault_config) + self._secret_clients[secret_identifier.vault_url] = referenced_client secret_value = None if referenced_client: try: secret_value = referenced_client.get_secret( - keyvault_identifier.name, version=keyvault_identifier.version + secret_identifier.name, version=secret_identifier.version ).value except ServiceRequestError as e: raise ValueError("Failed to retrieve secret from Key Vault") from e if self._secret_resolver and secret_value is None: - secret_value = self._secret_resolver(config.secret_id) + secret_value = self._secret_resolver(secret_id) - return self._cache_value(keyvault_identifier, secret_value, vault_url) + return self._cache_value(secret_id, secret_identifier, secret_value) def close(self) -> None: """ diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py index 0164393d0758..d8983f14ed86 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py @@ -25,11 +25,11 @@ _T = TypeVar("_T") -class _SecretProviderBase(Mapping[str, Union[str, JSON]]): +class _SecretProviderBase: def __init__(self, **kwargs: Any) -> None: - self._secret_cache: Dict[str, Any] = {} - self._secret_version_cache: Dict[str, str] = {} + self._secret_cache: Dict[str, Tuple[str, KeyVaultSecretIdentifier]] = {} + self._secret_version_cache: Dict[str, Tuple[str, KeyVaultSecretIdentifier]] = {} self.uses_key_vault = ( "keyvault_credential" in kwargs or ("keyvault_client_configs" in kwargs and len(kwargs.get("keyvault_client_configs", {})) > 0) @@ -41,21 +41,15 @@ def __init__(self, **kwargs: Any) -> None: else None ) - def bust_cache(self) -> None: - """ - Clears the secret cache. - """ - self._secret_cache = {} - - def _cache_value(self, keyvault_identifier: KeyVaultSecretIdentifier, secret_value: Any, vault_url: str) -> str: + def _cache_value(self, key: str, keyvault_identifier: KeyVaultSecretIdentifier, secret_value: Any) -> str: if secret_value: if keyvault_identifier.version: - self._secret_version_cache[keyvault_identifier.source_id] = secret_value + self._secret_version_cache[key] = (secret_value, keyvault_identifier) else: - self._secret_cache[keyvault_identifier.source_id] = secret_value + self._secret_cache[key] = (secret_value, keyvault_identifier) return secret_value - raise ValueError("No Secret Client found for Key Vault reference %s" % (vault_url)) + raise ValueError("No Secret Client found for Key Vault reference %s" % (keyvault_identifier.vault_url)) def resolve_keyvault_reference_base( self, config: SecretReferenceConfigurationSetting @@ -76,75 +70,3 @@ def resolve_keyvault_reference_base( vault_url = keyvault_identifier.vault_url + "/" return keyvault_identifier, vault_url - - def __getitem__(self, key: str) -> Any: - # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype - """ - Returns the value of the specified key. - """ - return self._secret_cache[key] - - def __iter__(self) -> Iterator[str]: - return self._secret_cache.__iter__() - - def __len__(self) -> int: - return len(self._secret_cache) - - def __contains__(self, __x: object) -> bool: - # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype - """ - Returns True if the configuration settings contains the specified key. - """ - return self._secret_cache.__contains__(__x) - - def keys(self) -> KeysView[str]: - """ - Returns a list of keys loaded from Azure App Configuration. - - :return: A list of keys loaded from Azure App Configuration. - :rtype: KeysView[str] - """ - return self._secret_cache.keys() - - def items(self) -> ItemsView[str, Union[str, Mapping[str, Any]]]: - """ - Returns a set-like object of key-value pairs loaded from Azure App Configuration. Any values that are Key Vault - references will be resolved. - - :return: A set-like object of key-value pairs loaded from Azure App Configuration. - :rtype: ItemsView[str, Union[str, Mapping[str, Any]]] - """ - return self._secret_cache.items() - - def values(self) -> ValuesView[Union[str, Mapping[str, Any]]]: - """ - Returns a list of values loaded from Azure App Configuration. Any values that are Key Vault references will be - resolved. - - :return: A list of values loaded from Azure App Configuration. The values are either Strings or JSON objects, - based on there content type. - :rtype: ValuesView[Union[str, Mapping[str, Any]]] - """ - return (self._secret_cache).values() - - @overload - def get(self, key: str, default: None = None) -> Union[str, JSON, None]: ... - - @overload - def get(self, key: str, default: Union[str, JSON, _T]) -> Union[str, JSON, _T]: # pylint: disable=signature-differs - ... - - def get(self, key: str, default: Optional[Union[str, JSON, _T]] = None) -> Union[str, JSON, _T, None]: - """ - Returns the value of the specified key. If the key does not exist, returns the default value. - - :param str key: The key of the value to get. - :param default: The default value to return. - :type: str or None - :return: The value of the specified key. - :rtype: Union[str, JSON] - """ - return self._secret_cache.get(key, default) - - def __ne__(self, other: Any) -> bool: - return not self == other diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py index 7d8411e4fcb8..abf121077d7b 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -374,8 +374,6 @@ async def _attempt_refresh( raise e async def refresh(self, **kwargs) -> None: - if self._secret_provider.secret_refresh_timer and self._secret_provider.secret_refresh_timer.needs_refresh(): - self._secret_provider.bust_cache() if not self._watched_settings and not self._feature_flag_refresh_enabled: logger.debug("Refresh called but no refresh enabled.") return @@ -388,6 +386,8 @@ async def refresh(self, **kwargs) -> None: exception: Optional[Exception] = None is_failover_request = False try: + if self._secret_provider.secret_refresh_timer and self._secret_provider.secret_refresh_timer.needs_refresh(): + secrets = await self._secret_provider.refresh_secrets() await self._replica_client_manager.refresh_clients() self._replica_client_manager.find_active_clients() replica_count = self._replica_client_manager.get_client_count() - 1 @@ -488,9 +488,13 @@ async def _load_all(self, **kwargs): raise exception async def _process_configurations(self, configuration_settings: List[ConfigurationSetting]) -> Dict[str, Any]: + # configuration_settings can contain duplicate keys, but they are in priority order, i.e. later settings take + # precedence. Only process the settings with the highest priority (i.e. the last one in the list). + unique_settings = self._deduplicate_settings(configuration_settings) + configuration_settings_processed = {} feature_flags_processed = [] - for settings in configuration_settings: + for settings in unique_settings.values(): if isinstance(settings, FeatureFlagConfigurationSetting): # Feature flags are not processed like other settings feature_flag_value = self._process_feature_flag(settings) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py index d76f4aa7e6fe..0ab774d20d8d 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py @@ -6,6 +6,7 @@ import inspect from typing import Mapping, Any, Dict from azure.appconfiguration import SecretReferenceConfigurationSetting # type:ignore # pylint:disable=no-name-in-module +from azure.keyvault.secrets import KeyVaultSecretIdentifier from azure.keyvault.secrets.aio import SecretClient from azure.core.exceptions import ServiceRequestError from ..._key_vault._secret_provider_base import _SecretProviderBase @@ -25,37 +26,48 @@ def __init__(self, **kwargs: Any) -> None: async def resolve_keyvault_reference(self, config: SecretReferenceConfigurationSetting) -> str: keyvault_identifier, vault_url = self.resolve_keyvault_reference_base(config) if keyvault_identifier.source_id in self._secret_cache: - return self._secret_cache[keyvault_identifier.source_id] + value, _ = self._secret_cache[keyvault_identifier.source_id] + return value if keyvault_identifier.source_id in self._secret_version_cache: - return self._secret_version_cache[keyvault_identifier.source_id] + value, _ = self._secret_version_cache[keyvault_identifier.source_id] + return value - referenced_client = self._secret_clients.get(vault_url, None) + return await self.__get_secret_value(keyvault_identifier.source_id, keyvault_identifier) - vault_config = self._keyvault_client_configs.get(vault_url, {}) + def refresh_secrets(self) -> None: + original_cache = self._secret_cache.copy() + self._secret_cache.clear() + for secret_id, (_, secret_identifier) in original_cache.items(): + self._secret_cache[secret_id] = self.__get_secret_value(secret_id, secret_identifier), secret_identifier + + async def __get_secret_value(self, secret_id: str, secret_identifier: KeyVaultSecretIdentifier) -> str: + referenced_client = self._secret_clients.get(secret_identifier.vault_url, None) + + vault_config = self._keyvault_client_configs.get(secret_identifier.vault_url, {}) credential = vault_config.pop("credential", self._keyvault_credential) if referenced_client is None and credential is not None: - referenced_client = SecretClient(vault_url=vault_url, credential=credential, **vault_config) - self._secret_clients[vault_url] = referenced_client + referenced_client = SecretClient(vault_url=secret_identifier.vault_url, credential=credential, **vault_config) + self._secret_clients[secret_identifier.vault_url] = referenced_client secret_value = None if referenced_client: try: secret_value = ( - await referenced_client.get_secret(keyvault_identifier.name, version=keyvault_identifier.version) + await referenced_client.get_secret(secret_identifier.name, version=secret_identifier.version) ).value except ServiceRequestError as e: raise ValueError("Failed to retrieve secret from Key Vault") from e if self._secret_resolver and secret_value is None: - secret_value = self._secret_resolver(config.secret_id) + secret_value = self._secret_resolver(secret_id) if inspect.isawaitable(secret_value): # Secret resolver was async # Need to ignore type, mypy doesn't like the callback could return `Never` secret_value = await secret_value # type: ignore - return self._cache_value(keyvault_identifier, secret_value, vault_url) + return self._cache_value(secret_id, secret_identifier, secret_value) async def close(self) -> None: """ From 8c2637dc0b41f102a84ea8745e064af3a2b6ef57 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 7 Oct 2025 16:00:30 -0700 Subject: [PATCH 36/43] Updating usage --- .../_azureappconfigurationprovider.py | 7 +- .../provider/_key_vault/_secret_provider.py | 28 +++--- .../_key_vault/_secret_provider_base.py | 15 +-- .../_azureappconfigurationproviderasync.py | 7 +- .../aio/_key_vault/_async_secret_provider.py | 30 +++--- .../key_vault/test_async_secret_provider.py | 92 +++++-------------- .../tests/key_vault/test_secret_provider.py | 92 +++++-------------- 7 files changed, 93 insertions(+), 178 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py index 969b0c1ca9c0..cfb9b92540db 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -377,8 +377,11 @@ def refresh(self, **kwargs) -> None: exception: Optional[Exception] = None is_failover_request = False try: - if self._secret_provider.secret_refresh_timer and self._secret_provider.secret_refresh_timer.needs_refresh(): - secrets = self._secret_provider.refresh_secrets() + if ( + self._secret_provider.secret_refresh_timer + and self._secret_provider.secret_refresh_timer.needs_refresh() + ): + self._secret_provider.refresh_secrets() self._replica_client_manager.refresh_clients() self._replica_client_manager.find_active_clients() replica_count = self._replica_client_manager.get_client_count() - 1 diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py index 0febe74b5d84..81d2381327a9 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py @@ -24,29 +24,33 @@ def __init__(self, **kwargs: Any) -> None: def resolve_keyvault_reference(self, config: SecretReferenceConfigurationSetting) -> str: keyvault_identifier, vault_url = self.resolve_keyvault_reference_base(config) if keyvault_identifier.source_id in self._secret_cache: - value, _ = self._secret_cache[keyvault_identifier.source_id] + _, _, value = self._secret_cache[keyvault_identifier.source_id] return value if keyvault_identifier.source_id in self._secret_version_cache: - value, _ = self._secret_version_cache[keyvault_identifier.source_id] + _, _, value = self._secret_version_cache[keyvault_identifier.source_id] return value - return self.__get_secret_value(keyvault_identifier.source_id, keyvault_identifier) + return self.__get_secret_value(config.key, keyvault_identifier, vault_url) def refresh_secrets(self) -> None: original_cache = self._secret_cache.copy() self._secret_cache.clear() - for secret_id, (_, secret_identifier) in original_cache.items(): - self._secret_cache[secret_id] = self.__get_secret_value(secret_id, secret_identifier), secret_identifier + for source_id, (secret_identifier, key, _) in original_cache.items(): + self._secret_cache[source_id] = ( + secret_identifier, + key, + self.__get_secret_value(key, secret_identifier, secret_identifier.vault_url + "/"), + ) - def __get_secret_value(self, secret_id: str, secret_identifier: KeyVaultSecretIdentifier) -> str: - referenced_client = self._secret_clients.get(secret_identifier.vault_url, None) + def __get_secret_value(self, key: str, secret_identifier: KeyVaultSecretIdentifier, vault_url: str) -> str: + referenced_client = self._secret_clients.get(vault_url, None) - vault_config = self._keyvault_client_configs.get(secret_identifier.vault_url, {}) + vault_config = self._keyvault_client_configs.get(vault_url, {}) credential = vault_config.pop("credential", self._keyvault_credential) if referenced_client is None and credential is not None: - referenced_client = SecretClient(vault_url=secret_identifier.vault_url, credential=credential, **vault_config) - self._secret_clients[secret_identifier.vault_url] = referenced_client + referenced_client = SecretClient(vault_url=vault_url, credential=credential, **vault_config) + self._secret_clients[vault_url] = referenced_client secret_value = None @@ -59,9 +63,9 @@ def __get_secret_value(self, secret_id: str, secret_identifier: KeyVaultSecretId raise ValueError("Failed to retrieve secret from Key Vault") from e if self._secret_resolver and secret_value is None: - secret_value = self._secret_resolver(secret_id) + secret_value = self._secret_resolver(secret_identifier.source_id) - return self._cache_value(secret_id, secret_identifier, secret_value) + return self._cache_value(key, secret_identifier, secret_value) def close(self) -> None: """ diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py index d8983f14ed86..359acf589560 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py @@ -5,14 +5,8 @@ # ------------------------------------------------------------------------- from typing import ( Mapping, - Union, Any, - Iterator, - KeysView, - ItemsView, - ValuesView, TypeVar, - overload, Optional, Dict, Tuple, @@ -28,8 +22,9 @@ class _SecretProviderBase: def __init__(self, **kwargs: Any) -> None: - self._secret_cache: Dict[str, Tuple[str, KeyVaultSecretIdentifier]] = {} - self._secret_version_cache: Dict[str, Tuple[str, KeyVaultSecretIdentifier]] = {} + # [source_id, (KeyVaultSecretIdentifier, key, value)] + self._secret_cache: Dict[str, Tuple[KeyVaultSecretIdentifier, str, str]] = {} + self._secret_version_cache: Dict[str, Tuple[KeyVaultSecretIdentifier, str, str]] = {} self.uses_key_vault = ( "keyvault_credential" in kwargs or ("keyvault_client_configs" in kwargs and len(kwargs.get("keyvault_client_configs", {})) > 0) @@ -44,9 +39,9 @@ def __init__(self, **kwargs: Any) -> None: def _cache_value(self, key: str, keyvault_identifier: KeyVaultSecretIdentifier, secret_value: Any) -> str: if secret_value: if keyvault_identifier.version: - self._secret_version_cache[key] = (secret_value, keyvault_identifier) + self._secret_version_cache[keyvault_identifier.source_id] = (keyvault_identifier, key, secret_value) else: - self._secret_cache[key] = (secret_value, keyvault_identifier) + self._secret_cache[keyvault_identifier.source_id] = (keyvault_identifier, key, secret_value) return secret_value raise ValueError("No Secret Client found for Key Vault reference %s" % (keyvault_identifier.vault_url)) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py index abf121077d7b..69f601ab120d 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -386,8 +386,11 @@ async def refresh(self, **kwargs) -> None: exception: Optional[Exception] = None is_failover_request = False try: - if self._secret_provider.secret_refresh_timer and self._secret_provider.secret_refresh_timer.needs_refresh(): - secrets = await self._secret_provider.refresh_secrets() + if ( + self._secret_provider.secret_refresh_timer + and self._secret_provider.secret_refresh_timer.needs_refresh() + ): + await self._secret_provider.refresh_secrets() await self._replica_client_manager.refresh_clients() self._replica_client_manager.find_active_clients() replica_count = self._replica_client_manager.get_client_count() - 1 diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py index 0ab774d20d8d..21f44b1c58b4 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py @@ -26,29 +26,33 @@ def __init__(self, **kwargs: Any) -> None: async def resolve_keyvault_reference(self, config: SecretReferenceConfigurationSetting) -> str: keyvault_identifier, vault_url = self.resolve_keyvault_reference_base(config) if keyvault_identifier.source_id in self._secret_cache: - value, _ = self._secret_cache[keyvault_identifier.source_id] + _, _, value = self._secret_cache[keyvault_identifier.source_id] return value if keyvault_identifier.source_id in self._secret_version_cache: - value, _ = self._secret_version_cache[keyvault_identifier.source_id] + _, _, value = self._secret_version_cache[keyvault_identifier.source_id] return value - return await self.__get_secret_value(keyvault_identifier.source_id, keyvault_identifier) + return await self.__get_secret_value(config.key, keyvault_identifier, vault_url) - def refresh_secrets(self) -> None: + async def refresh_secrets(self) -> None: original_cache = self._secret_cache.copy() self._secret_cache.clear() - for secret_id, (_, secret_identifier) in original_cache.items(): - self._secret_cache[secret_id] = self.__get_secret_value(secret_id, secret_identifier), secret_identifier + for source_id, (secret_identifier, key, _) in original_cache.items(): + self._secret_cache[source_id] = ( + secret_identifier, + key, + await self.__get_secret_value(key, secret_identifier, secret_identifier.vault_url + "/"), + ) - async def __get_secret_value(self, secret_id: str, secret_identifier: KeyVaultSecretIdentifier) -> str: - referenced_client = self._secret_clients.get(secret_identifier.vault_url, None) + async def __get_secret_value(self, key: str, secret_identifier: KeyVaultSecretIdentifier, vault_url: str) -> str: + referenced_client = self._secret_clients.get(vault_url, None) - vault_config = self._keyvault_client_configs.get(secret_identifier.vault_url, {}) + vault_config = self._keyvault_client_configs.get(vault_url, {}) credential = vault_config.pop("credential", self._keyvault_credential) if referenced_client is None and credential is not None: - referenced_client = SecretClient(vault_url=secret_identifier.vault_url, credential=credential, **vault_config) - self._secret_clients[secret_identifier.vault_url] = referenced_client + referenced_client = SecretClient(vault_url=vault_url, credential=credential, **vault_config) + self._secret_clients[vault_url] = referenced_client secret_value = None @@ -61,13 +65,13 @@ async def __get_secret_value(self, secret_id: str, secret_identifier: KeyVaultSe raise ValueError("Failed to retrieve secret from Key Vault") from e if self._secret_resolver and secret_value is None: - secret_value = self._secret_resolver(secret_id) + secret_value = self._secret_resolver(secret_identifier.source_id) if inspect.isawaitable(secret_value): # Secret resolver was async # Need to ignore type, mypy doesn't like the callback could return `Never` secret_value = await secret_value # type: ignore - return self._cache_value(secret_id, secret_identifier, secret_value) + return self._cache_value(key, secret_identifier, secret_value) async def close(self) -> None: """ diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py index 6bcf80544662..06dc8b63c08a 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py @@ -85,9 +85,14 @@ async def test_resolve_keyvault_reference_with_cached_secret(self): # Create a SecretProvider with a mock credential secret_provider = SecretProvider(keyvault_credential=Mock()) + key_vault_identifier, _ = secret_provider.resolve_keyvault_reference_base(config) # Add to cache - secret_provider._secret_cache[TEST_SECRET_ID] = "cached-secret-value" + secret_provider._secret_cache[key_vault_identifier.source_id] = ( + key_vault_identifier, + "test-key", + "cached-secret-value", + ) # This should return the cached value without calling SecretClient result = await secret_provider.resolve_keyvault_reference(config) @@ -102,9 +107,14 @@ async def test_resolve_keyvault_reference_with_cached_secret_version(self): # Create a SecretProvider with a mock credential secret_provider = SecretProvider(keyvault_credential=Mock()) + key_vault_identifier, _ = secret_provider.resolve_keyvault_reference_base(config) # Add to cache - secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] = "cached-secret-value" + secret_provider._secret_version_cache[key_vault_identifier.source_id] = ( + key_vault_identifier, + "test-key", + "cached-secret-value", + ) # This should return the cached value without calling SecretClient result = await secret_provider.resolve_keyvault_reference(config) @@ -152,7 +162,8 @@ async def test_resolve_keyvault_reference_with_existing_client(self): self.assertEqual(result, "secret-value") mock_client.get_secret.assert_called_once_with(mock_id_instance.name, version=mock_id_instance.version) # Verify the secret was cached - self.assertEqual(secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION], "secret-value") + _, _, value = secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] + self.assertEqual(value, "secret-value") async def test_resolve_keyvault_reference_with_new_client(self): """Test resolving a Key Vault reference by creating a new client.""" @@ -199,7 +210,8 @@ async def test_resolve_keyvault_reference_with_new_client(self): # Verify the client was cached self.assertEqual(secret_provider._secret_clients[vault_url], mock_client) # Verify the secret was cached - self.assertEqual(secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION], "new-secret-value") + _, _, value = secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] + self.assertEqual(value, "new-secret-value") async def test_resolve_keyvault_reference_with_secret_resolver(self): """Test resolving a Key Vault reference using a secret resolver.""" @@ -233,7 +245,8 @@ async def test_resolve_keyvault_reference_with_secret_resolver(self): self.assertEqual(result, "resolved-secret-value") mock_resolver.assert_called_once_with(TEST_SECRET_ID_VERSION) # Verify the secret was cached - self.assertEqual(secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION], "resolved-secret-value") + _, _, value = secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] + self.assertEqual(value, "resolved-secret-value") async def test_resolve_keyvault_reference_with_async_secret_resolver(self): """Test resolving a Key Vault reference using an async secret resolver.""" @@ -267,9 +280,8 @@ async def async_resolver(secret_id): # Verify the result self.assertEqual(result, "async-resolved-secret-value") # Verify the secret was cached - self.assertEqual( - secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION], "async-resolved-secret-value" - ) + _, _, value = secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] + self.assertEqual(value, "async-resolved-secret-value") async def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self): """Test falling back to a secret resolver if the client fails to get the secret.""" @@ -314,7 +326,8 @@ async def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self mock_client.get_secret.assert_called_once_with(mock_id_instance.name, version=mock_id_instance.version) mock_resolver.assert_called_once_with(TEST_SECRET_ID_VERSION) # Verify the secret was cached - self.assertEqual(secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION], "fallback-secret-value") + _, _, value = secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] + self.assertEqual(value, "fallback-secret-value") async def test_resolve_keyvault_reference_no_client_no_resolver(self): """Test that an error is raised when no client or resolver can resolve the reference.""" @@ -421,67 +434,6 @@ async def test_client_config_specific_credential(self): # Verify the result self.assertEqual(result, "secret-value") - async def test_mapping_interface(self): - """Test that the SecretProvider implements the Mapping interface.""" - # Create a SecretProvider - secret_provider = SecretProvider() - - # Add some items to the cache - secret_provider._secret_cache = {"key1": "value1", "key2": "value2", "key3": "value3"} - - # Test __getitem__ - self.assertEqual(secret_provider["key1"], "value1") - self.assertEqual(secret_provider["key2"], "value2") - self.assertEqual(secret_provider["key3"], "value3") - - # Test __len__ - self.assertEqual(len(secret_provider), 3) - - # Test __iter__ - keys = list(secret_provider) - self.assertEqual(sorted(keys), ["key1", "key2", "key3"]) - - # Test __contains__ - self.assertIn("key1", secret_provider) - self.assertIn("key2", secret_provider) - self.assertIn("key3", secret_provider) - self.assertNotIn("key4", secret_provider) - - # Test keys - self.assertEqual(set(secret_provider.keys()), {"key1", "key2", "key3"}) - - # Test items - items = dict(secret_provider.items()) - self.assertEqual(items, {"key1": "value1", "key2": "value2", "key3": "value3"}) - - # Test values - values = list(secret_provider.values()) - # Sort the expected values instead since values may not be comparable - self.assertEqual(set(values), {"value1", "value2", "value3"}) - - # Test get - self.assertEqual(secret_provider.get("key1"), "value1") - self.assertEqual(secret_provider.get("key4"), None) - self.assertEqual(secret_provider.get("key4", "default"), "default") - - async def test_bust_cache(self): - """Test that bust_cache clears the secret cache.""" - # Create a SecretProvider - secret_provider = SecretProvider() - - # Add some items to the cache - secret_provider._secret_cache = {"key1": "value1", "key2": "value2", "key3": "value3"} - - # Verify the cache has items - self.assertEqual(len(secret_provider._secret_cache), 3) - - # Call bust_cache - secret_provider.bust_cache() - - # Verify the cache is empty - self.assertEqual(len(secret_provider._secret_cache), 0) - self.assertEqual(secret_provider._secret_cache, {}) - @app_config_decorator_async @recorded_by_proxy_async async def test_integration_with_keyvault( diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/key_vault/test_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/key_vault/test_secret_provider.py index ada500428368..056e33f79ad9 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/key_vault/test_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/key_vault/test_secret_provider.py @@ -4,15 +4,16 @@ # license information. # -------------------------------------------------------------------------- import unittest -from unittest.mock import Mock, patch, MagicMock +from unittest.mock import Mock, patch from azure.appconfiguration import SecretReferenceConfigurationSetting from azure.appconfiguration.provider._key_vault._secret_provider import SecretProvider -from azure.keyvault.secrets import SecretClient, KeyVaultSecret +from azure.keyvault.secrets import SecretClient from devtools_testutils import recorded_by_proxy from preparers import app_config_decorator_aad from testcase import AppConfigTestCase TEST_SECRET_ID = "https://myvault.vault.azure.net/secrets/my_secret" + TEST_SECRET_ID_VERSION = TEST_SECRET_ID + "/12345" @@ -84,9 +85,14 @@ def test_resolve_keyvault_reference_with_cached_secret(self): # Create a SecretProvider with a mock credential secret_provider = SecretProvider(keyvault_credential=Mock()) + key_vault_identifier, _ = secret_provider.resolve_keyvault_reference_base(config) # Add to cache - secret_provider._secret_cache[TEST_SECRET_ID] = "cached-secret-value" + secret_provider._secret_cache[key_vault_identifier.source_id] = ( + key_vault_identifier, + "test-key", + "cached-secret-value", + ) # This should return the cached value without calling SecretClient result = secret_provider.resolve_keyvault_reference(config) @@ -101,9 +107,14 @@ def test_resolve_keyvault_reference_with_cached_secret_version(self): # Create a SecretProvider with a mock credential secret_provider = SecretProvider(keyvault_credential=Mock()) + key_vault_identifier, _ = secret_provider.resolve_keyvault_reference_base(config) # Add to cache - secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] = "cached-secret-value" + secret_provider._secret_version_cache[key_vault_identifier.source_id] = ( + key_vault_identifier, + "test-key", + "cached-secret-value", + ) # This should return the cached value without calling SecretClient result = secret_provider.resolve_keyvault_reference(config) @@ -150,7 +161,8 @@ def test_resolve_keyvault_reference_with_existing_client(self): self.assertEqual(result, "secret-value") mock_client.get_secret.assert_called_once_with(mock_id_instance.name, version=mock_id_instance.version) # Verify the secret was cached - self.assertEqual(secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION], "secret-value") + _, _, value = secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] + self.assertEqual(value, "secret-value") def test_resolve_keyvault_reference_with_new_client(self): """Test resolving a Key Vault reference by creating a new client.""" @@ -195,7 +207,8 @@ def test_resolve_keyvault_reference_with_new_client(self): # Verify the client was cached self.assertEqual(secret_provider._secret_clients[vault_url], mock_client) # Verify the secret was cached - self.assertEqual(secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION], "new-secret-value") + _, _, value = secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] + self.assertEqual(value, "new-secret-value") def test_resolve_keyvault_reference_with_secret_resolver(self): """Test resolving a Key Vault reference using a secret resolver.""" @@ -229,7 +242,8 @@ def test_resolve_keyvault_reference_with_secret_resolver(self): self.assertEqual(result, "resolved-secret-value") mock_resolver.assert_called_once_with(TEST_SECRET_ID_VERSION) # Verify the secret was cached - self.assertEqual(secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION], "resolved-secret-value") + _, _, value = secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] + self.assertEqual(value, "resolved-secret-value") def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self): """Test falling back to a secret resolver if the client fails to get the secret.""" @@ -272,7 +286,8 @@ def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self): mock_client.get_secret.assert_called_once_with(mock_id_instance.name, version=mock_id_instance.version) mock_resolver.assert_called_once_with(TEST_SECRET_ID_VERSION) # Verify the secret was cached - self.assertEqual(secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION], "fallback-secret-value") + _, _, value = secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] + self.assertEqual(value, "fallback-secret-value") def test_resolve_keyvault_reference_no_client_no_resolver(self): """Test that an error is raised when no client or resolver can resolve the reference.""" @@ -374,67 +389,6 @@ def test_client_config_specific_credential(self): # Verify the result self.assertEqual(result, "secret-value") - def test_mapping_interface(self): - """Test that the SecretProvider implements the Mapping interface.""" - # Create a SecretProvider - secret_provider = SecretProvider() - - # Add some items to the cache - secret_provider._secret_cache = {"key1": "value1", "key2": "value2", "key3": "value3"} - - # Test __getitem__ - self.assertEqual(secret_provider["key1"], "value1") - self.assertEqual(secret_provider["key2"], "value2") - self.assertEqual(secret_provider["key3"], "value3") - - # Test __len__ - self.assertEqual(len(secret_provider), 3) - - # Test __iter__ - keys = list(secret_provider) - self.assertEqual(sorted(keys), ["key1", "key2", "key3"]) - - # Test __contains__ - self.assertIn("key1", secret_provider) - self.assertIn("key2", secret_provider) - self.assertIn("key3", secret_provider) - self.assertNotIn("key4", secret_provider) - - # Test keys - self.assertEqual(set(secret_provider.keys()), {"key1", "key2", "key3"}) - - # Test items - items = dict(secret_provider.items()) - self.assertEqual(items, {"key1": "value1", "key2": "value2", "key3": "value3"}) - - # Test values - values = list(secret_provider.values()) - # Sort the expected values instead since values may not be comparable - self.assertEqual(set(values), {"value1", "value2", "value3"}) - - # Test get - self.assertEqual(secret_provider.get("key1"), "value1") - self.assertEqual(secret_provider.get("key4"), None) - self.assertEqual(secret_provider.get("key4", "default"), "default") - - def test_bust_cache(self): - """Test that bust_cache clears the secret cache.""" - # Create a SecretProvider - secret_provider = SecretProvider() - - # Add some items to the cache - secret_provider._secret_cache = {"key1": "value1", "key2": "value2", "key3": "value3"} - - # Verify the cache has items - self.assertEqual(len(secret_provider._secret_cache), 3) - - # Call bust_cache - secret_provider.bust_cache() - - # Verify the cache is empty - self.assertEqual(len(secret_provider._secret_cache), 0) - self.assertEqual(secret_provider._secret_cache, {}) - @recorded_by_proxy @app_config_decorator_aad def test_integration_with_keyvault(self, appconfiguration_endpoint_string, appconfiguration_keyvault_secret_url): From 8e146971333e9eea96e35b94a9f48819a41d5aed Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 8 Oct 2025 10:34:19 -0700 Subject: [PATCH 37/43] Update assets.json --- .../azure-appconfiguration-provider/assets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json index 5ca3d7d5b3ed..9e0b93603416 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json +++ b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/appconfiguration/azure-appconfiguration-provider", - "Tag": "python/appconfiguration/azure-appconfiguration-provider_ab0cded533" + "Tag": "python/appconfiguration/azure-appconfiguration-provider_30404f3021" } From 53a0eeb23a7d0bde7e42adc73b685d81709ca971 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 10 Oct 2025 16:55:41 -0700 Subject: [PATCH 38/43] Updated to make secret refresh update dictionary --- .../_azureappconfigurationprovider.py | 2 +- .../provider/_key_vault/_secret_provider.py | 24 ++++++++++++------- .../_azureappconfigurationproviderasync.py | 2 +- .../aio/_key_vault/_async_secret_provider.py | 24 ++++++++++++------- 4 files changed, 32 insertions(+), 20 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py index cfb9b92540db..8fa90516c492 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -381,7 +381,7 @@ def refresh(self, **kwargs) -> None: self._secret_provider.secret_refresh_timer and self._secret_provider.secret_refresh_timer.needs_refresh() ): - self._secret_provider.refresh_secrets() + self._dict.update(self._secret_provider.refresh_secrets()) self._replica_client_manager.refresh_clients() self._replica_client_manager.find_active_clients() replica_count = self._replica_client_manager.get_client_count() - 1 diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py index 81d2381327a9..ab18dd8e20e2 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py @@ -32,15 +32,21 @@ def resolve_keyvault_reference(self, config: SecretReferenceConfigurationSetting return self.__get_secret_value(config.key, keyvault_identifier, vault_url) - def refresh_secrets(self) -> None: - original_cache = self._secret_cache.copy() - self._secret_cache.clear() - for source_id, (secret_identifier, key, _) in original_cache.items(): - self._secret_cache[source_id] = ( - secret_identifier, - key, - self.__get_secret_value(key, secret_identifier, secret_identifier.vault_url + "/"), - ) + def refresh_secrets(self) -> Dict[str, Any]: + secrets = {} + if self.secret_refresh_timer: + original_cache = self._secret_cache.copy() + self._secret_cache.clear() + for source_id, (secret_identifier, key, _) in original_cache.items(): + value = self.__get_secret_value(key, secret_identifier, secret_identifier.vault_url + "/") + self._secret_cache[source_id] = ( + secret_identifier, + key, + value, + ) + secrets[key] = value + self.secret_refresh_timer.reset() + return secrets def __get_secret_value(self, key: str, secret_identifier: KeyVaultSecretIdentifier, vault_url: str) -> str: referenced_client = self._secret_clients.get(vault_url, None) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py index 69f601ab120d..b6338f28d5d4 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -390,7 +390,7 @@ async def refresh(self, **kwargs) -> None: self._secret_provider.secret_refresh_timer and self._secret_provider.secret_refresh_timer.needs_refresh() ): - await self._secret_provider.refresh_secrets() + self._dict.update(await self._secret_provider.refresh_secrets()) await self._replica_client_manager.refresh_clients() self._replica_client_manager.find_active_clients() replica_count = self._replica_client_manager.get_client_count() - 1 diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py index 21f44b1c58b4..cb0ee5206a1a 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py @@ -34,15 +34,21 @@ async def resolve_keyvault_reference(self, config: SecretReferenceConfigurationS return await self.__get_secret_value(config.key, keyvault_identifier, vault_url) - async def refresh_secrets(self) -> None: - original_cache = self._secret_cache.copy() - self._secret_cache.clear() - for source_id, (secret_identifier, key, _) in original_cache.items(): - self._secret_cache[source_id] = ( - secret_identifier, - key, - await self.__get_secret_value(key, secret_identifier, secret_identifier.vault_url + "/"), - ) + async def refresh_secrets(self) -> Dict[str, Any]: + secrets = {} + if self.secret_refresh_timer: + original_cache = self._secret_cache.copy() + self._secret_cache.clear() + for source_id, (secret_identifier, key, _) in original_cache.items(): + value = await self.__get_secret_value(key, secret_identifier, secret_identifier.vault_url + "/") + self._secret_cache[source_id] = ( + secret_identifier, + key, + value, + ) + secrets[key] = value + self.secret_refresh_timer.reset() + return secrets async def __get_secret_value(self, key: str, secret_identifier: KeyVaultSecretIdentifier, vault_url: str) -> str: referenced_client = self._secret_clients.get(vault_url, None) From abddfd9d938e11ed51d70e2b2ce1550943c67caf Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 17 Oct 2025 09:34:41 -0700 Subject: [PATCH 39/43] removing _secret_version_cache --- .../provider/_key_vault/_secret_provider.py | 3 --- .../provider/_key_vault/_secret_provider_base.py | 6 +----- .../aio/_key_vault/_async_secret_provider.py | 3 --- .../aio/key_vault/test_async_secret_provider.py | 12 ++++++------ .../tests/key_vault/test_secret_provider.py | 10 +++++----- 5 files changed, 12 insertions(+), 22 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py index ab18dd8e20e2..d147236c1bf4 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py @@ -26,9 +26,6 @@ def resolve_keyvault_reference(self, config: SecretReferenceConfigurationSetting if keyvault_identifier.source_id in self._secret_cache: _, _, value = self._secret_cache[keyvault_identifier.source_id] return value - if keyvault_identifier.source_id in self._secret_version_cache: - _, _, value = self._secret_version_cache[keyvault_identifier.source_id] - return value return self.__get_secret_value(config.key, keyvault_identifier, vault_url) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py index 359acf589560..31f5f328ce6f 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py @@ -24,7 +24,6 @@ class _SecretProviderBase: def __init__(self, **kwargs: Any) -> None: # [source_id, (KeyVaultSecretIdentifier, key, value)] self._secret_cache: Dict[str, Tuple[KeyVaultSecretIdentifier, str, str]] = {} - self._secret_version_cache: Dict[str, Tuple[KeyVaultSecretIdentifier, str, str]] = {} self.uses_key_vault = ( "keyvault_credential" in kwargs or ("keyvault_client_configs" in kwargs and len(kwargs.get("keyvault_client_configs", {})) > 0) @@ -38,10 +37,7 @@ def __init__(self, **kwargs: Any) -> None: def _cache_value(self, key: str, keyvault_identifier: KeyVaultSecretIdentifier, secret_value: Any) -> str: if secret_value: - if keyvault_identifier.version: - self._secret_version_cache[keyvault_identifier.source_id] = (keyvault_identifier, key, secret_value) - else: - self._secret_cache[keyvault_identifier.source_id] = (keyvault_identifier, key, secret_value) + self._secret_cache[keyvault_identifier.source_id] = (keyvault_identifier, key, secret_value) return secret_value raise ValueError("No Secret Client found for Key Vault reference %s" % (keyvault_identifier.vault_url)) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py index cb0ee5206a1a..70c61840d087 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py @@ -28,9 +28,6 @@ async def resolve_keyvault_reference(self, config: SecretReferenceConfigurationS if keyvault_identifier.source_id in self._secret_cache: _, _, value = self._secret_cache[keyvault_identifier.source_id] return value - if keyvault_identifier.source_id in self._secret_version_cache: - _, _, value = self._secret_version_cache[keyvault_identifier.source_id] - return value return await self.__get_secret_value(config.key, keyvault_identifier, vault_url) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py index 06dc8b63c08a..172ae3d2d615 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/key_vault/test_async_secret_provider.py @@ -110,7 +110,7 @@ async def test_resolve_keyvault_reference_with_cached_secret_version(self): key_vault_identifier, _ = secret_provider.resolve_keyvault_reference_base(config) # Add to cache - secret_provider._secret_version_cache[key_vault_identifier.source_id] = ( + secret_provider._secret_cache[key_vault_identifier.source_id] = ( key_vault_identifier, "test-key", "cached-secret-value", @@ -162,7 +162,7 @@ async def test_resolve_keyvault_reference_with_existing_client(self): self.assertEqual(result, "secret-value") mock_client.get_secret.assert_called_once_with(mock_id_instance.name, version=mock_id_instance.version) # Verify the secret was cached - _, _, value = secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] + _, _, value = secret_provider._secret_cache[TEST_SECRET_ID_VERSION] self.assertEqual(value, "secret-value") async def test_resolve_keyvault_reference_with_new_client(self): @@ -210,7 +210,7 @@ async def test_resolve_keyvault_reference_with_new_client(self): # Verify the client was cached self.assertEqual(secret_provider._secret_clients[vault_url], mock_client) # Verify the secret was cached - _, _, value = secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] + _, _, value = secret_provider._secret_cache[TEST_SECRET_ID_VERSION] self.assertEqual(value, "new-secret-value") async def test_resolve_keyvault_reference_with_secret_resolver(self): @@ -245,7 +245,7 @@ async def test_resolve_keyvault_reference_with_secret_resolver(self): self.assertEqual(result, "resolved-secret-value") mock_resolver.assert_called_once_with(TEST_SECRET_ID_VERSION) # Verify the secret was cached - _, _, value = secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] + _, _, value = secret_provider._secret_cache[TEST_SECRET_ID_VERSION] self.assertEqual(value, "resolved-secret-value") async def test_resolve_keyvault_reference_with_async_secret_resolver(self): @@ -280,7 +280,7 @@ async def async_resolver(secret_id): # Verify the result self.assertEqual(result, "async-resolved-secret-value") # Verify the secret was cached - _, _, value = secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] + _, _, value = secret_provider._secret_cache[TEST_SECRET_ID_VERSION] self.assertEqual(value, "async-resolved-secret-value") async def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self): @@ -326,7 +326,7 @@ async def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self mock_client.get_secret.assert_called_once_with(mock_id_instance.name, version=mock_id_instance.version) mock_resolver.assert_called_once_with(TEST_SECRET_ID_VERSION) # Verify the secret was cached - _, _, value = secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] + _, _, value = secret_provider._secret_cache[TEST_SECRET_ID_VERSION] self.assertEqual(value, "fallback-secret-value") async def test_resolve_keyvault_reference_no_client_no_resolver(self): diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/key_vault/test_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/key_vault/test_secret_provider.py index 056e33f79ad9..50f589de2d4d 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/key_vault/test_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/key_vault/test_secret_provider.py @@ -110,7 +110,7 @@ def test_resolve_keyvault_reference_with_cached_secret_version(self): key_vault_identifier, _ = secret_provider.resolve_keyvault_reference_base(config) # Add to cache - secret_provider._secret_version_cache[key_vault_identifier.source_id] = ( + secret_provider._secret_cache[key_vault_identifier.source_id] = ( key_vault_identifier, "test-key", "cached-secret-value", @@ -161,7 +161,7 @@ def test_resolve_keyvault_reference_with_existing_client(self): self.assertEqual(result, "secret-value") mock_client.get_secret.assert_called_once_with(mock_id_instance.name, version=mock_id_instance.version) # Verify the secret was cached - _, _, value = secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] + _, _, value = secret_provider._secret_cache[TEST_SECRET_ID_VERSION] self.assertEqual(value, "secret-value") def test_resolve_keyvault_reference_with_new_client(self): @@ -207,7 +207,7 @@ def test_resolve_keyvault_reference_with_new_client(self): # Verify the client was cached self.assertEqual(secret_provider._secret_clients[vault_url], mock_client) # Verify the secret was cached - _, _, value = secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] + _, _, value = secret_provider._secret_cache[TEST_SECRET_ID_VERSION] self.assertEqual(value, "new-secret-value") def test_resolve_keyvault_reference_with_secret_resolver(self): @@ -242,7 +242,7 @@ def test_resolve_keyvault_reference_with_secret_resolver(self): self.assertEqual(result, "resolved-secret-value") mock_resolver.assert_called_once_with(TEST_SECRET_ID_VERSION) # Verify the secret was cached - _, _, value = secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] + _, _, value = secret_provider._secret_cache[TEST_SECRET_ID_VERSION] self.assertEqual(value, "resolved-secret-value") def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self): @@ -286,7 +286,7 @@ def test_resolve_keyvault_reference_with_client_and_resolver_fallback(self): mock_client.get_secret.assert_called_once_with(mock_id_instance.name, version=mock_id_instance.version) mock_resolver.assert_called_once_with(TEST_SECRET_ID_VERSION) # Verify the secret was cached - _, _, value = secret_provider._secret_version_cache[TEST_SECRET_ID_VERSION] + _, _, value = secret_provider._secret_cache[TEST_SECRET_ID_VERSION] self.assertEqual(value, "fallback-secret-value") def test_resolve_keyvault_reference_no_client_no_resolver(self): From d5add3b496514bbf08894f602f58bfbaffd9f959 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 17 Oct 2025 11:03:29 -0700 Subject: [PATCH 40/43] Update assets.json --- .../azure-appconfiguration-provider/assets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json index 9e0b93603416..281fbf689a48 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/assets.json +++ b/sdk/appconfiguration/azure-appconfiguration-provider/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/appconfiguration/azure-appconfiguration-provider", - "Tag": "python/appconfiguration/azure-appconfiguration-provider_30404f3021" + "Tag": "python/appconfiguration/azure-appconfiguration-provider_b13d43b82a" } From f10365c8f58abdbf55610ef538f9db3cf2b77584 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 20 Oct 2025 10:27:32 -0700 Subject: [PATCH 41/43] Update _secret_provider_base.py --- .../provider/_key_vault/_secret_provider_base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py index 31f5f328ce6f..65270c9e60a5 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py @@ -29,8 +29,14 @@ def __init__(self, **kwargs: Any) -> None: or ("keyvault_client_configs" in kwargs and len(kwargs.get("keyvault_client_configs", {})) > 0) or "secret_resolver" in kwargs ) + + refresh_interval=kwargs.pop("secret_refresh_interval", 60) + + if refresh_interval <= 1: + raise ValueError("Secret refresh interval must be greater than 1 second.") + self.secret_refresh_timer: Optional[_RefreshTimer] = ( - _RefreshTimer(refresh_interval=kwargs.pop("secret_refresh_interval", 60)) + _RefreshTimer(refresh_interval=refresh_interval) if self.uses_key_vault and "secret_refresh_interval" in kwargs else None ) From 1955caf60ce22371f0bea023a26caf0906ecbfa0 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 20 Oct 2025 10:57:29 -0700 Subject: [PATCH 42/43] Update _secret_provider_base.py --- .../provider/_key_vault/_secret_provider_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py index 65270c9e60a5..9a5af2649990 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py @@ -30,7 +30,7 @@ def __init__(self, **kwargs: Any) -> None: or "secret_resolver" in kwargs ) - refresh_interval=kwargs.pop("secret_refresh_interval", 60) + refresh_interval = kwargs.pop("secret_refresh_interval", 60) if refresh_interval <= 1: raise ValueError("Secret refresh interval must be greater than 1 second.") From b3292844f3299fddcdfdc6627e61394b897f7e7b Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 20 Oct 2025 13:01:10 -0700 Subject: [PATCH 43/43] Update _secret_provider_base.py --- .../provider/_key_vault/_secret_provider_base.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py index 9a5af2649990..7b1abb9d97dc 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py @@ -30,13 +30,11 @@ def __init__(self, **kwargs: Any) -> None: or "secret_resolver" in kwargs ) - refresh_interval = kwargs.pop("secret_refresh_interval", 60) - - if refresh_interval <= 1: + if kwargs.get("secret_refresh_interval", 60) < 1: raise ValueError("Secret refresh interval must be greater than 1 second.") self.secret_refresh_timer: Optional[_RefreshTimer] = ( - _RefreshTimer(refresh_interval=refresh_interval) + _RefreshTimer(refresh_interval=kwargs.pop("secret_refresh_interval", 60)) if self.uses_key_vault and "secret_refresh_interval" in kwargs else None )