From a55b8f4ee898a1f730bbf8995ca73005e012480c Mon Sep 17 00:00:00 2001 From: Jordan Selig Date: Thu, 26 Mar 2026 09:56:55 -0400 Subject: [PATCH 1/5] [App Service] Fix #28858: `az webapp config container set`: Add managed identity ACR pull support Add managed identity support for ACR image pulling in `az webapp config container set`: - Add --assign-identity, --scope, --role params for identity assignment - Add --acr-use-identity flag to enable/disable ACR pull via managed identity - Add --acr-identity param to specify which identity to use for ACR pull - Update update_container_settings() to call assign_identity() and update_site_configs() with ACR identity parameters - Add unit tests for system identity, user identity, custom role, disable identity, and no-identity scenarios - Update help text with managed identity example This follows the same patterns used by `az webapp create` for ACR identity support, using the existing assign_identity() and update_site_configs() infrastructure. Fixes #28858 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cli/command_modules/appservice/_help.py | 3 + .../cli/command_modules/appservice/_params.py | 17 +++ .../cli/command_modules/appservice/custom.py | 11 +- .../latest/test_webapp_commands_thru_mock.py | 123 +++++++++++++++++- 4 files changed, 152 insertions(+), 2 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_help.py b/src/azure-cli/azure/cli/command_modules/appservice/_help.py index e0dad92e98c..585033170ba 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_help.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_help.py @@ -1607,6 +1607,9 @@ - name: Set a web app container's settings. (autogenerated) text: az webapp config container set --docker-custom-image-name MyDockerCustomImage --docker-registry-server-password StrongPassword --docker-registry-server-url https://{azure-container-registry-name}.azurecr.io --docker-registry-server-user DockerUserId --name MyWebApp --resource-group MyResourceGroup crafted: true + - name: Set a web app container to pull from ACR using a managed identity. + text: az webapp config container set --container-image-name myregistry.azurecr.io/myimage:latest --container-registry-url https://myregistry.azurecr.io --assign-identity [system] --acr-use-identity --acr-identity [system] --name MyWebApp --resource-group MyResourceGroup + crafted: true """ helps['webapp config container show'] = """ diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_params.py b/src/azure-cli/azure/cli/command_modules/appservice/_params.py index bd4dc0b7ea5..47bcea97db2 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -646,6 +646,23 @@ def load_arguments(self, _): c.ignore('min_replicas') c.ignore('max_replicas') + with self.argument_context('webapp config container set') as c: + c.argument('assign_identities', nargs='*', options_list=['--assign-identity'], + help="Accept system or user assigned identities separated by spaces. " + "Use '[system]' to refer to system assigned identity, " + "or a resource id to refer to user assigned identity. " + "Check out help for more examples") + c.argument('scope', options_list=['--scope'], + help="Scope that the system assigned identity can access") + c.argument('role', options_list=['--role'], + help="Role name or id the system assigned identity will have") + c.argument('acr_use_identity', arg_type=get_three_state_flag(return_label=True), + help="Enable or disable pulling images from ACR using a managed identity") + c.argument('acr_identity', + help="Accept system or user assigned identity which will be used for ACR image pull. " + "Use '[system]' to refer to system assigned identity, " + "or a resource id to refer to user assigned identity.") + with self.argument_context('functionapp config container') as c: c.argument('registry_server', options_list=['--registry-server', '-r', c.deprecate(target='--docker-registry-server-url', redirect='--registry-server')], help='the container registry server url') diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index 386ba088608..44113b629e9 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -3602,7 +3602,9 @@ def update_container_settings(cmd, resource_group_name, name, container_registry container_image_name=None, container_registry_user=None, websites_enable_app_service_storage=None, container_registry_password=None, multicontainer_config_type=None, multicontainer_config_file=None, - slot=None, min_replicas=None, max_replicas=None): + slot=None, min_replicas=None, max_replicas=None, + assign_identities=None, role='AcrPull', scope=None, + acr_use_identity=None, acr_identity=None): settings = [] if container_registry_url is not None: settings.append('DOCKER_REGISTRY_SERVER_URL=' + container_registry_url) @@ -3640,6 +3642,13 @@ def update_container_settings(cmd, resource_group_name, name, container_registry if min_replicas is not None or max_replicas is not None: update_site_configs(cmd, resource_group_name, name, min_replicas=min_replicas, max_replicas=max_replicas) + if assign_identities is not None: + assign_identity(cmd, resource_group_name, name, assign_identities, role, slot, scope) + + if acr_use_identity is not None or acr_identity is not None: + update_site_configs(cmd, resource_group_name, name, slot=slot, + acr_use_identity=acr_use_identity, acr_identity=acr_identity) + return _mask_creds_related_appsettings(_filter_for_container_settings(cmd, resource_group_name, name, settings, slot=slot)) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py index 853eadc1edd..8d13cd42de2 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py @@ -33,7 +33,8 @@ add_github_actions, update_app_settings, update_application_settings_polling, - update_webapp) + update_webapp, + update_container_settings) # pylint: disable=line-too-long from azure.cli.core.profiles import ResourceType @@ -639,6 +640,126 @@ def test_update_webapp_platform_release_channel_latest(self): self.assertEqual(result.additional_properties["properties"]["platformReleaseChannel"], "Latest") +class TestUpdateContainerSettingsIdentity(unittest.TestCase): + """Tests for managed identity support in update_container_settings.""" + + @mock.patch('azure.cli.command_modules.appservice.custom._mask_creds_related_appsettings') + @mock.patch('azure.cli.command_modules.appservice.custom._filter_for_container_settings') + @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[]) + @mock.patch('azure.cli.command_modules.appservice.custom.update_app_settings') + @mock.patch('azure.cli.command_modules.appservice.custom._add_fx_version') + @mock.patch('azure.cli.command_modules.appservice.custom.assign_identity') + @mock.patch('azure.cli.command_modules.appservice.custom.update_site_configs') + def test_container_set_with_system_identity_and_acr( + self, mock_update_site_configs, mock_assign_identity, + mock_add_fx, mock_update_app, mock_get_app, mock_filter, mock_mask): + mock_mask.return_value = {} + cmd = _get_test_cmd() + update_container_settings( + cmd, 'rg', 'web1', + container_registry_url='https://myregistry.azurecr.io', + container_image_name='myregistry.azurecr.io/myimage:latest', + assign_identities=['[system]'], + acr_use_identity='true', + acr_identity='[system]') + mock_assign_identity.assert_called_once_with( + cmd, 'rg', 'web1', ['[system]'], 'AcrPull', None, None) + mock_update_site_configs.assert_called_once_with( + cmd, 'rg', 'web1', slot=None, + acr_use_identity='true', acr_identity='[system]') + + @mock.patch('azure.cli.command_modules.appservice.custom._mask_creds_related_appsettings') + @mock.patch('azure.cli.command_modules.appservice.custom._filter_for_container_settings') + @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[]) + @mock.patch('azure.cli.command_modules.appservice.custom.update_app_settings') + @mock.patch('azure.cli.command_modules.appservice.custom._add_fx_version') + @mock.patch('azure.cli.command_modules.appservice.custom.assign_identity') + @mock.patch('azure.cli.command_modules.appservice.custom.update_site_configs') + def test_container_set_with_user_identity( + self, mock_update_site_configs, mock_assign_identity, + mock_add_fx, mock_update_app, mock_get_app, mock_filter, mock_mask): + mock_mask.return_value = {} + cmd = _get_test_cmd() + user_identity = '/subscriptions/sub1/resourcegroups/rg1/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id1' + update_container_settings( + cmd, 'rg', 'web1', + container_registry_url='https://myregistry.azurecr.io', + container_image_name='myregistry.azurecr.io/myimage:latest', + assign_identities=[user_identity], + acr_use_identity='true', + acr_identity=user_identity) + mock_assign_identity.assert_called_once_with( + cmd, 'rg', 'web1', [user_identity], 'AcrPull', None, None) + mock_update_site_configs.assert_called_once_with( + cmd, 'rg', 'web1', slot=None, + acr_use_identity='true', acr_identity=user_identity) + + @mock.patch('azure.cli.command_modules.appservice.custom._mask_creds_related_appsettings') + @mock.patch('azure.cli.command_modules.appservice.custom._filter_for_container_settings') + @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[]) + @mock.patch('azure.cli.command_modules.appservice.custom.update_app_settings') + @mock.patch('azure.cli.command_modules.appservice.custom._add_fx_version') + @mock.patch('azure.cli.command_modules.appservice.custom.assign_identity') + @mock.patch('azure.cli.command_modules.appservice.custom.update_site_configs') + def test_container_set_without_identity_does_not_call_identity_apis( + self, mock_update_site_configs, mock_assign_identity, + mock_add_fx, mock_update_app, mock_get_app, mock_filter, mock_mask): + mock_mask.return_value = {} + cmd = _get_test_cmd() + update_container_settings( + cmd, 'rg', 'web1', + container_registry_url='https://myregistry.azurecr.io', + container_image_name='myregistry.azurecr.io/myimage:latest', + container_registry_user='user', + container_registry_password='pass') + mock_assign_identity.assert_not_called() + mock_update_site_configs.assert_not_called() + + @mock.patch('azure.cli.command_modules.appservice.custom._mask_creds_related_appsettings') + @mock.patch('azure.cli.command_modules.appservice.custom._filter_for_container_settings') + @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[]) + @mock.patch('azure.cli.command_modules.appservice.custom.update_app_settings') + @mock.patch('azure.cli.command_modules.appservice.custom._add_fx_version') + @mock.patch('azure.cli.command_modules.appservice.custom.assign_identity') + @mock.patch('azure.cli.command_modules.appservice.custom.update_site_configs') + def test_container_set_with_custom_role( + self, mock_update_site_configs, mock_assign_identity, + mock_add_fx, mock_update_app, mock_get_app, mock_filter, mock_mask): + mock_mask.return_value = {} + cmd = _get_test_cmd() + update_container_settings( + cmd, 'rg', 'web1', + container_registry_url='https://myregistry.azurecr.io', + container_image_name='myregistry.azurecr.io/myimage:latest', + assign_identities=['[system]'], + role='Reader', + acr_use_identity='true', + acr_identity='[system]') + mock_assign_identity.assert_called_once_with( + cmd, 'rg', 'web1', ['[system]'], 'Reader', None, None) + + @mock.patch('azure.cli.command_modules.appservice.custom._mask_creds_related_appsettings') + @mock.patch('azure.cli.command_modules.appservice.custom._filter_for_container_settings') + @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[]) + @mock.patch('azure.cli.command_modules.appservice.custom.update_app_settings') + @mock.patch('azure.cli.command_modules.appservice.custom._add_fx_version') + @mock.patch('azure.cli.command_modules.appservice.custom.assign_identity') + @mock.patch('azure.cli.command_modules.appservice.custom.update_site_configs') + def test_container_set_disable_acr_identity( + self, mock_update_site_configs, mock_assign_identity, + mock_add_fx, mock_update_app, mock_get_app, mock_filter, mock_mask): + mock_mask.return_value = {} + cmd = _get_test_cmd() + update_container_settings( + cmd, 'rg', 'web1', + container_registry_url='https://myregistry.azurecr.io', + acr_use_identity='false') + mock_assign_identity.assert_not_called() + mock_update_site_configs.assert_called_once_with( + cmd, 'rg', 'web1', slot=None, + acr_use_identity='false', acr_identity=None) + + class FakedResponse: # pylint: disable=too-few-public-methods def __init__(self, status_code): self.status_code = status_code From a69095483740b517bda11bd5ca058b6ac71674a5 Mon Sep 17 00:00:00 2001 From: Jordan Selig Date: Thu, 26 Mar 2026 10:22:37 -0400 Subject: [PATCH 2/5] Address review: skip ACR cred lookup when using managed identity - Short-circuit _get_acr_cred when acr_use_identity is set to avoid persisting admin credentials that defeat MI-based pulls - Add regression test asserting _get_acr_cred is not called in MI flow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cli/command_modules/appservice/custom.py | 1 + .../latest/test_webapp_commands_thru_mock.py | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index 44113b629e9..ccbb1fa4e82 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -3610,6 +3610,7 @@ def update_container_settings(cmd, resource_group_name, name, container_registry settings.append('DOCKER_REGISTRY_SERVER_URL=' + container_registry_url) if (not container_registry_user and not container_registry_password and + not acr_use_identity and container_registry_url and '.azurecr.io' in container_registry_url): logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') parsed = urlparse(container_registry_url) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py index 8d13cd42de2..0e6a2044cfd 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py @@ -759,6 +759,30 @@ def test_container_set_disable_acr_identity( cmd, 'rg', 'web1', slot=None, acr_use_identity='false', acr_identity=None) + @mock.patch('azure.cli.command_modules.appservice.custom._get_acr_cred') + @mock.patch('azure.cli.command_modules.appservice.custom._mask_creds_related_appsettings') + @mock.patch('azure.cli.command_modules.appservice.custom._filter_for_container_settings') + @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[]) + @mock.patch('azure.cli.command_modules.appservice.custom.update_app_settings') + @mock.patch('azure.cli.command_modules.appservice.custom._add_fx_version') + @mock.patch('azure.cli.command_modules.appservice.custom.assign_identity') + @mock.patch('azure.cli.command_modules.appservice.custom.update_site_configs') + def test_acr_use_identity_skips_credential_lookup( + self, mock_update_site_configs, mock_assign_identity, + mock_add_fx, mock_update_app, mock_get_app, mock_filter, mock_mask, + mock_get_acr_cred): + """When acr_use_identity is set, _get_acr_cred should NOT be called.""" + mock_mask.return_value = {} + cmd = _get_test_cmd() + update_container_settings( + cmd, 'rg', 'web1', + container_registry_url='https://myregistry.azurecr.io', + container_image_name='myregistry.azurecr.io/myimage:latest', + assign_identities=['[system]'], + acr_use_identity='true', + acr_identity='[system]') + mock_get_acr_cred.assert_not_called() + class FakedResponse: # pylint: disable=too-few-public-methods def __init__(self, status_code): From 814f3ef0da1938a65e97f6a01274709e1e8cb0b4 Mon Sep 17 00:00:00 2001 From: Jordan Selig Date: Thu, 26 Mar 2026 11:30:13 -0400 Subject: [PATCH 3/5] [App Service] Fix #21907: `az webapp config container set`: preserve Key Vault references When running `az webapp config container set`, existing app settings containing Key Vault references (e.g., @Microsoft.KeyVault(...)) were being overwritten. This happened because: 1. ACR credential auto-detection replaced KV-ref credentials with raw values when a .azurecr.io URL was provided without explicit username/password. 2. The settings update used a read-modify-write cycle through update_app_settings that could interfere with non-container settings. This fix: - Adds _is_key_vault_reference() helper to detect KV reference values. - Skips ACR credential auto-detection when existing username/password settings are Key Vault references. - Merges only container-specific keys directly into the existing properties dict, preserving all other app settings as-is. - Adds unit tests verifying KV references survive container set operations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cli/command_modules/appservice/custom.py | 59 +++++-- .../latest/test_webapp_commands_thru_mock.py | 147 ++++++++++++++++-- 2 files changed, 178 insertions(+), 28 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index ccbb1fa4e82..f924026b2dc 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -3598,6 +3598,11 @@ def _redact_connection_strings(properties): APPSETTINGS_TO_MASK = ['DOCKER_REGISTRY_SERVER_PASSWORD'] +def _is_key_vault_reference(value): + """Check if a setting value is a Key Vault reference.""" + return isinstance(value, str) and value.strip().startswith('@Microsoft.KeyVault(') + + def update_container_settings(cmd, resource_group_name, name, container_registry_url=None, container_image_name=None, container_registry_user=None, websites_enable_app_service_storage=None, container_registry_password=None, @@ -3605,30 +3610,54 @@ def update_container_settings(cmd, resource_group_name, name, container_registry slot=None, min_replicas=None, max_replicas=None, assign_identities=None, role='AcrPull', scope=None, acr_use_identity=None, acr_identity=None): - settings = [] - if container_registry_url is not None: - settings.append('DOCKER_REGISTRY_SERVER_URL=' + container_registry_url) + # Read existing app settings so we can preserve non-container settings and Key Vault references + existing_app_settings = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, + 'list_application_settings', slot) + existing_properties = existing_app_settings.properties or {} if (not container_registry_user and not container_registry_password and not acr_use_identity and container_registry_url and '.azurecr.io' in container_registry_url): - logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') - parsed = urlparse(container_registry_url) - registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] - try: - container_registry_user, container_registry_password = _get_acr_cred(cmd.cli_ctx, registry_name) - except Exception as ex: # pylint: disable=broad-except - logger.warning("Retrieving credentials failed with an exception:'%s'", ex) # consider throw if needed + existing_user_val = existing_properties.get('DOCKER_REGISTRY_SERVER_USERNAME', '') + existing_pass_val = existing_properties.get('DOCKER_REGISTRY_SERVER_PASSWORD', '') + if _is_key_vault_reference(existing_user_val) or _is_key_vault_reference(existing_pass_val): + logger.warning('Existing registry credentials use Key Vault references. ' + 'Skipping automatic credential lookup.') + else: + logger.warning('No credential was provided to access Azure Container Registry. ' + 'Trying to look up...') + parsed = urlparse(container_registry_url) + registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] + try: + container_registry_user, container_registry_password = _get_acr_cred(cmd.cli_ctx, + registry_name) + except Exception as ex: # pylint: disable=broad-except + logger.warning("Retrieving credentials failed with an exception:'%s'", ex) + # Build dict of only the container-specific settings that were explicitly provided + container_updates = {} + if container_registry_url is not None: + container_updates['DOCKER_REGISTRY_SERVER_URL'] = container_registry_url if container_registry_user is not None: - settings.append('DOCKER_REGISTRY_SERVER_USERNAME=' + container_registry_user) + container_updates['DOCKER_REGISTRY_SERVER_USERNAME'] = container_registry_user if container_registry_password is not None: - settings.append('DOCKER_REGISTRY_SERVER_PASSWORD=' + container_registry_password) + container_updates['DOCKER_REGISTRY_SERVER_PASSWORD'] = container_registry_password if websites_enable_app_service_storage: - settings.append('WEBSITES_ENABLE_APP_SERVICE_STORAGE=' + websites_enable_app_service_storage) + container_updates['WEBSITES_ENABLE_APP_SERVICE_STORAGE'] = websites_enable_app_service_storage - if container_registry_user or container_registry_password or container_registry_url or websites_enable_app_service_storage: # pylint: disable=line-too-long - update_app_settings(cmd, resource_group_name, name, settings, slot) + if container_updates: + # Merge only container-specific keys into the existing settings, + # preserving all other app settings (including Key Vault references) as-is + for key, value in container_updates.items(): + existing_app_settings.properties[key] = value + client = web_client_factory(cmd.cli_ctx) + if is_centauri_functionapp(cmd, resource_group_name, name): + update_application_settings_polling(cmd, resource_group_name, name, + existing_app_settings, slot, client) + else: + _generic_settings_operation(cmd.cli_ctx, resource_group_name, name, + 'update_application_settings', + existing_app_settings, slot, client) settings = get_app_settings(cmd, resource_group_name, name, slot) if container_image_name is not None: _add_fx_version(cmd, resource_group_name, name, container_image_name, slot) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py index 0e6a2044cfd..25f68c1e329 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py @@ -34,7 +34,8 @@ update_app_settings, update_application_settings_polling, update_webapp, - update_container_settings) + update_container_settings, + _is_key_vault_reference) # pylint: disable=line-too-long from azure.cli.core.profiles import ResourceType @@ -643,17 +644,27 @@ def test_update_webapp_platform_release_channel_latest(self): class TestUpdateContainerSettingsIdentity(unittest.TestCase): """Tests for managed identity support in update_container_settings.""" + def _make_mock_app_settings(self, properties=None): + mock_app_settings = mock.MagicMock() + mock_app_settings.properties = properties or {} + return mock_app_settings + @mock.patch('azure.cli.command_modules.appservice.custom._mask_creds_related_appsettings') @mock.patch('azure.cli.command_modules.appservice.custom._filter_for_container_settings') @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[]) - @mock.patch('azure.cli.command_modules.appservice.custom.update_app_settings') + @mock.patch('azure.cli.command_modules.appservice.custom._generic_settings_operation') + @mock.patch('azure.cli.command_modules.appservice.custom.is_centauri_functionapp', return_value=False) + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory') + @mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation') @mock.patch('azure.cli.command_modules.appservice.custom._add_fx_version') @mock.patch('azure.cli.command_modules.appservice.custom.assign_identity') @mock.patch('azure.cli.command_modules.appservice.custom.update_site_configs') def test_container_set_with_system_identity_and_acr( self, mock_update_site_configs, mock_assign_identity, - mock_add_fx, mock_update_app, mock_get_app, mock_filter, mock_mask): + mock_add_fx, mock_site_op, mock_client_factory, mock_centauri, + mock_settings_op, mock_get_app, mock_filter, mock_mask): mock_mask.return_value = {} + mock_site_op.return_value = self._make_mock_app_settings() cmd = _get_test_cmd() update_container_settings( cmd, 'rg', 'web1', @@ -671,14 +682,19 @@ def test_container_set_with_system_identity_and_acr( @mock.patch('azure.cli.command_modules.appservice.custom._mask_creds_related_appsettings') @mock.patch('azure.cli.command_modules.appservice.custom._filter_for_container_settings') @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[]) - @mock.patch('azure.cli.command_modules.appservice.custom.update_app_settings') + @mock.patch('azure.cli.command_modules.appservice.custom._generic_settings_operation') + @mock.patch('azure.cli.command_modules.appservice.custom.is_centauri_functionapp', return_value=False) + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory') + @mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation') @mock.patch('azure.cli.command_modules.appservice.custom._add_fx_version') @mock.patch('azure.cli.command_modules.appservice.custom.assign_identity') @mock.patch('azure.cli.command_modules.appservice.custom.update_site_configs') def test_container_set_with_user_identity( self, mock_update_site_configs, mock_assign_identity, - mock_add_fx, mock_update_app, mock_get_app, mock_filter, mock_mask): + mock_add_fx, mock_site_op, mock_client_factory, mock_centauri, + mock_settings_op, mock_get_app, mock_filter, mock_mask): mock_mask.return_value = {} + mock_site_op.return_value = self._make_mock_app_settings() cmd = _get_test_cmd() user_identity = '/subscriptions/sub1/resourcegroups/rg1/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id1' update_container_settings( @@ -697,14 +713,19 @@ def test_container_set_with_user_identity( @mock.patch('azure.cli.command_modules.appservice.custom._mask_creds_related_appsettings') @mock.patch('azure.cli.command_modules.appservice.custom._filter_for_container_settings') @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[]) - @mock.patch('azure.cli.command_modules.appservice.custom.update_app_settings') + @mock.patch('azure.cli.command_modules.appservice.custom._generic_settings_operation') + @mock.patch('azure.cli.command_modules.appservice.custom.is_centauri_functionapp', return_value=False) + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory') + @mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation') @mock.patch('azure.cli.command_modules.appservice.custom._add_fx_version') @mock.patch('azure.cli.command_modules.appservice.custom.assign_identity') @mock.patch('azure.cli.command_modules.appservice.custom.update_site_configs') def test_container_set_without_identity_does_not_call_identity_apis( self, mock_update_site_configs, mock_assign_identity, - mock_add_fx, mock_update_app, mock_get_app, mock_filter, mock_mask): + mock_add_fx, mock_site_op, mock_client_factory, mock_centauri, + mock_settings_op, mock_get_app, mock_filter, mock_mask): mock_mask.return_value = {} + mock_site_op.return_value = self._make_mock_app_settings() cmd = _get_test_cmd() update_container_settings( cmd, 'rg', 'web1', @@ -718,14 +739,19 @@ def test_container_set_without_identity_does_not_call_identity_apis( @mock.patch('azure.cli.command_modules.appservice.custom._mask_creds_related_appsettings') @mock.patch('azure.cli.command_modules.appservice.custom._filter_for_container_settings') @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[]) - @mock.patch('azure.cli.command_modules.appservice.custom.update_app_settings') + @mock.patch('azure.cli.command_modules.appservice.custom._generic_settings_operation') + @mock.patch('azure.cli.command_modules.appservice.custom.is_centauri_functionapp', return_value=False) + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory') + @mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation') @mock.patch('azure.cli.command_modules.appservice.custom._add_fx_version') @mock.patch('azure.cli.command_modules.appservice.custom.assign_identity') @mock.patch('azure.cli.command_modules.appservice.custom.update_site_configs') def test_container_set_with_custom_role( self, mock_update_site_configs, mock_assign_identity, - mock_add_fx, mock_update_app, mock_get_app, mock_filter, mock_mask): + mock_add_fx, mock_site_op, mock_client_factory, mock_centauri, + mock_settings_op, mock_get_app, mock_filter, mock_mask): mock_mask.return_value = {} + mock_site_op.return_value = self._make_mock_app_settings() cmd = _get_test_cmd() update_container_settings( cmd, 'rg', 'web1', @@ -741,14 +767,19 @@ def test_container_set_with_custom_role( @mock.patch('azure.cli.command_modules.appservice.custom._mask_creds_related_appsettings') @mock.patch('azure.cli.command_modules.appservice.custom._filter_for_container_settings') @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[]) - @mock.patch('azure.cli.command_modules.appservice.custom.update_app_settings') + @mock.patch('azure.cli.command_modules.appservice.custom._generic_settings_operation') + @mock.patch('azure.cli.command_modules.appservice.custom.is_centauri_functionapp', return_value=False) + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory') + @mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation') @mock.patch('azure.cli.command_modules.appservice.custom._add_fx_version') @mock.patch('azure.cli.command_modules.appservice.custom.assign_identity') @mock.patch('azure.cli.command_modules.appservice.custom.update_site_configs') def test_container_set_disable_acr_identity( self, mock_update_site_configs, mock_assign_identity, - mock_add_fx, mock_update_app, mock_get_app, mock_filter, mock_mask): + mock_add_fx, mock_site_op, mock_client_factory, mock_centauri, + mock_settings_op, mock_get_app, mock_filter, mock_mask): mock_mask.return_value = {} + mock_site_op.return_value = self._make_mock_app_settings() cmd = _get_test_cmd() update_container_settings( cmd, 'rg', 'web1', @@ -763,16 +794,21 @@ def test_container_set_disable_acr_identity( @mock.patch('azure.cli.command_modules.appservice.custom._mask_creds_related_appsettings') @mock.patch('azure.cli.command_modules.appservice.custom._filter_for_container_settings') @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[]) - @mock.patch('azure.cli.command_modules.appservice.custom.update_app_settings') + @mock.patch('azure.cli.command_modules.appservice.custom._generic_settings_operation') + @mock.patch('azure.cli.command_modules.appservice.custom.is_centauri_functionapp', return_value=False) + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory') + @mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation') @mock.patch('azure.cli.command_modules.appservice.custom._add_fx_version') @mock.patch('azure.cli.command_modules.appservice.custom.assign_identity') @mock.patch('azure.cli.command_modules.appservice.custom.update_site_configs') def test_acr_use_identity_skips_credential_lookup( self, mock_update_site_configs, mock_assign_identity, - mock_add_fx, mock_update_app, mock_get_app, mock_filter, mock_mask, + mock_add_fx, mock_site_op, mock_client_factory, mock_centauri, + mock_settings_op, mock_get_app, mock_filter, mock_mask, mock_get_acr_cred): """When acr_use_identity is set, _get_acr_cred should NOT be called.""" mock_mask.return_value = {} + mock_site_op.return_value = self._make_mock_app_settings() cmd = _get_test_cmd() update_container_settings( cmd, 'rg', 'web1', @@ -784,6 +820,91 @@ def test_acr_use_identity_skips_credential_lookup( mock_get_acr_cred.assert_not_called() +class TestUpdateContainerSettingsPreservesKeyVaultRefs(unittest.TestCase): + + @mock.patch('azure.cli.command_modules.appservice.custom._get_fx_version', return_value='DOCKER|myimage:latest') + @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings') + @mock.patch('azure.cli.command_modules.appservice.custom._generic_settings_operation') + @mock.patch('azure.cli.command_modules.appservice.custom.is_centauri_functionapp', return_value=False) + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory') + @mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation') + def test_container_set_preserves_kv_ref_settings(self, mock_site_op, mock_client_factory, + mock_centauri, mock_settings_op, + mock_get_app_settings, mock_get_fx): + """Key Vault reference app settings must survive az webapp config container set.""" + cmd_mock = _get_test_cmd() + + kv_ref = '@Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.net/secrets/mysecret)' + existing_properties = { + 'MY_KV_SECRET': kv_ref, + 'NORMAL_SETTING': 'normal_value', + 'DOCKER_REGISTRY_SERVER_URL': 'https://old.azurecr.io', + } + mock_app_settings = mock.MagicMock() + mock_app_settings.properties = dict(existing_properties) + mock_site_op.return_value = mock_app_settings + + mock_get_app_settings.return_value = [ + {'name': 'MY_KV_SECRET', 'value': kv_ref, 'slotSetting': False}, + {'name': 'NORMAL_SETTING', 'value': 'normal_value', 'slotSetting': False}, + {'name': 'DOCKER_REGISTRY_SERVER_URL', 'value': 'https://new.example.io', 'slotSetting': False}, + ] + + update_container_settings(cmd_mock, 'test-rg', 'test-app', + container_registry_url='https://new.example.io') + + # The settings written back must still contain the KV ref and normal setting + written_props = mock_app_settings.properties + self.assertEqual(written_props['MY_KV_SECRET'], kv_ref) + self.assertEqual(written_props['NORMAL_SETTING'], 'normal_value') + self.assertEqual(written_props['DOCKER_REGISTRY_SERVER_URL'], 'https://new.example.io') + + @mock.patch('azure.cli.command_modules.appservice.custom._get_fx_version', return_value='DOCKER|myimage:latest') + @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings') + @mock.patch('azure.cli.command_modules.appservice.custom._generic_settings_operation') + @mock.patch('azure.cli.command_modules.appservice.custom.is_centauri_functionapp', return_value=False) + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory') + @mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation') + def test_container_set_skips_acr_auto_detect_when_kv_refs(self, mock_site_op, mock_client_factory, + mock_centauri, mock_settings_op, + mock_get_app_settings, mock_get_fx): + """ACR credential auto-detection must be skipped when existing creds are KV references.""" + cmd_mock = _get_test_cmd() + + kv_user = '@Microsoft.KeyVault(SecretUri=https://vault.vault.azure.net/secrets/user)' + kv_pass = '@Microsoft.KeyVault(SecretUri=https://vault.vault.azure.net/secrets/pass)' + existing_properties = { + 'DOCKER_REGISTRY_SERVER_URL': 'https://old.azurecr.io', + 'DOCKER_REGISTRY_SERVER_USERNAME': kv_user, + 'DOCKER_REGISTRY_SERVER_PASSWORD': kv_pass, + } + mock_app_settings = mock.MagicMock() + mock_app_settings.properties = dict(existing_properties) + mock_site_op.return_value = mock_app_settings + + mock_get_app_settings.return_value = [ + {'name': 'DOCKER_REGISTRY_SERVER_URL', 'value': 'https://myregistry.azurecr.io', 'slotSetting': False}, + ] + + with mock.patch('azure.cli.command_modules.appservice.custom._get_acr_cred') as mock_acr_cred: + update_container_settings(cmd_mock, 'test-rg', 'test-app', + container_registry_url='https://myregistry.azurecr.io') + # _get_acr_cred should NOT have been called because existing creds are KV refs + mock_acr_cred.assert_not_called() + + # Existing KV references for username/password must be preserved + written_props = mock_app_settings.properties + self.assertEqual(written_props['DOCKER_REGISTRY_SERVER_USERNAME'], kv_user) + self.assertEqual(written_props['DOCKER_REGISTRY_SERVER_PASSWORD'], kv_pass) + + def test_is_key_vault_reference_detects_kv_refs(self): + self.assertTrue(_is_key_vault_reference('@Microsoft.KeyVault(SecretUri=https://v.vault.azure.net/secrets/s)')) + self.assertTrue(_is_key_vault_reference(' @Microsoft.KeyVault(VaultName=v;SecretName=s)')) + self.assertFalse(_is_key_vault_reference('plain_value')) + self.assertFalse(_is_key_vault_reference('')) + self.assertFalse(_is_key_vault_reference(None)) + + class FakedResponse: # pylint: disable=too-few-public-methods def __init__(self, status_code): self.status_code = status_code From 38a88a520fde3289b9f0e56d297fa5400de6320e Mon Sep 17 00:00:00 2001 From: Jordan Selig Date: Thu, 26 Mar 2026 15:22:57 -0400 Subject: [PATCH 4/5] Address review: early-return ACR cred skip, consolidate site-config calls, strengthen test assertions - When acr_use_identity is set, short-circuit credential lookup entirely (explicit early branch instead of relying on compound condition) - Accumulate all site-config changes (multicontainer, replicas, ACR identity) into a single update_site_configs call to reduce latency and failure surface - Add _get_acr_cred mock to all MI tests and assert it is never called - Add update_site_configs value assertions to custom-role test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cli/command_modules/appservice/custom.py | 32 ++++++++++++------- .../latest/test_webapp_commands_thru_mock.py | 24 +++++++++++--- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index f924026b2dc..959b6eae232 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -3615,9 +3615,11 @@ def update_container_settings(cmd, resource_group_name, name, container_registry 'list_application_settings', slot) existing_properties = existing_app_settings.properties or {} - if (not container_registry_user and not container_registry_password and - not acr_use_identity and - container_registry_url and '.azurecr.io' in container_registry_url): + # Skip credential lookup entirely when managed identity ACR pull is enabled + if acr_use_identity: + logger.info('Managed identity ACR pull is enabled; skipping automatic credential lookup.') + elif (not container_registry_user and not container_registry_password and + container_registry_url and '.azurecr.io' in container_registry_url): existing_user_val = existing_properties.get('DOCKER_REGISTRY_SERVER_USERNAME', '') existing_pass_val = existing_properties.get('DOCKER_REGISTRY_SERVER_PASSWORD', '') if _is_key_vault_reference(existing_user_val) or _is_key_vault_reference(existing_pass_val): @@ -3662,23 +3664,31 @@ def update_container_settings(cmd, resource_group_name, name, container_registry if container_image_name is not None: _add_fx_version(cmd, resource_group_name, name, container_image_name, slot) + # Accumulate site-config changes and issue a single update_site_configs call + site_config_kwargs = {} if multicontainer_config_file and multicontainer_config_type: encoded_config_file = _get_linux_multicontainer_encoded_config_from_file(multicontainer_config_file) - linux_fx_version = _format_fx_version(encoded_config_file, multicontainer_config_type) - update_site_configs(cmd, resource_group_name, name, linux_fx_version=linux_fx_version, slot=slot) + site_config_kwargs['linux_fx_version'] = _format_fx_version(encoded_config_file, + multicontainer_config_type) elif multicontainer_config_file or multicontainer_config_type: logger.warning('Must change both settings --multicontainer-config-file FILE --multicontainer-config-type TYPE') - if min_replicas is not None or max_replicas is not None: - update_site_configs(cmd, resource_group_name, name, min_replicas=min_replicas, max_replicas=max_replicas) + if min_replicas is not None: + site_config_kwargs['min_replicas'] = min_replicas + if max_replicas is not None: + site_config_kwargs['max_replicas'] = max_replicas + + if acr_use_identity is not None: + site_config_kwargs['acr_use_identity'] = acr_use_identity + if acr_identity is not None: + site_config_kwargs['acr_identity'] = acr_identity + + if site_config_kwargs: + update_site_configs(cmd, resource_group_name, name, slot=slot, **site_config_kwargs) if assign_identities is not None: assign_identity(cmd, resource_group_name, name, assign_identities, role, slot, scope) - if acr_use_identity is not None or acr_identity is not None: - update_site_configs(cmd, resource_group_name, name, slot=slot, - acr_use_identity=acr_use_identity, acr_identity=acr_identity) - return _mask_creds_related_appsettings(_filter_for_container_settings(cmd, resource_group_name, name, settings, slot=slot)) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py index 25f68c1e329..ca78b7ebb6a 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py @@ -649,6 +649,7 @@ def _make_mock_app_settings(self, properties=None): mock_app_settings.properties = properties or {} return mock_app_settings + @mock.patch('azure.cli.command_modules.appservice.custom._get_acr_cred') @mock.patch('azure.cli.command_modules.appservice.custom._mask_creds_related_appsettings') @mock.patch('azure.cli.command_modules.appservice.custom._filter_for_container_settings') @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[]) @@ -662,7 +663,8 @@ def _make_mock_app_settings(self, properties=None): def test_container_set_with_system_identity_and_acr( self, mock_update_site_configs, mock_assign_identity, mock_add_fx, mock_site_op, mock_client_factory, mock_centauri, - mock_settings_op, mock_get_app, mock_filter, mock_mask): + mock_settings_op, mock_get_app, mock_filter, mock_mask, + mock_get_acr_cred): mock_mask.return_value = {} mock_site_op.return_value = self._make_mock_app_settings() cmd = _get_test_cmd() @@ -673,12 +675,14 @@ def test_container_set_with_system_identity_and_acr( assign_identities=['[system]'], acr_use_identity='true', acr_identity='[system]') + mock_get_acr_cred.assert_not_called() mock_assign_identity.assert_called_once_with( cmd, 'rg', 'web1', ['[system]'], 'AcrPull', None, None) mock_update_site_configs.assert_called_once_with( cmd, 'rg', 'web1', slot=None, acr_use_identity='true', acr_identity='[system]') + @mock.patch('azure.cli.command_modules.appservice.custom._get_acr_cred') @mock.patch('azure.cli.command_modules.appservice.custom._mask_creds_related_appsettings') @mock.patch('azure.cli.command_modules.appservice.custom._filter_for_container_settings') @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[]) @@ -692,7 +696,8 @@ def test_container_set_with_system_identity_and_acr( def test_container_set_with_user_identity( self, mock_update_site_configs, mock_assign_identity, mock_add_fx, mock_site_op, mock_client_factory, mock_centauri, - mock_settings_op, mock_get_app, mock_filter, mock_mask): + mock_settings_op, mock_get_app, mock_filter, mock_mask, + mock_get_acr_cred): mock_mask.return_value = {} mock_site_op.return_value = self._make_mock_app_settings() cmd = _get_test_cmd() @@ -704,6 +709,7 @@ def test_container_set_with_user_identity( assign_identities=[user_identity], acr_use_identity='true', acr_identity=user_identity) + mock_get_acr_cred.assert_not_called() mock_assign_identity.assert_called_once_with( cmd, 'rg', 'web1', [user_identity], 'AcrPull', None, None) mock_update_site_configs.assert_called_once_with( @@ -736,6 +742,7 @@ def test_container_set_without_identity_does_not_call_identity_apis( mock_assign_identity.assert_not_called() mock_update_site_configs.assert_not_called() + @mock.patch('azure.cli.command_modules.appservice.custom._get_acr_cred') @mock.patch('azure.cli.command_modules.appservice.custom._mask_creds_related_appsettings') @mock.patch('azure.cli.command_modules.appservice.custom._filter_for_container_settings') @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[]) @@ -749,7 +756,8 @@ def test_container_set_without_identity_does_not_call_identity_apis( def test_container_set_with_custom_role( self, mock_update_site_configs, mock_assign_identity, mock_add_fx, mock_site_op, mock_client_factory, mock_centauri, - mock_settings_op, mock_get_app, mock_filter, mock_mask): + mock_settings_op, mock_get_app, mock_filter, mock_mask, + mock_get_acr_cred): mock_mask.return_value = {} mock_site_op.return_value = self._make_mock_app_settings() cmd = _get_test_cmd() @@ -761,9 +769,14 @@ def test_container_set_with_custom_role( role='Reader', acr_use_identity='true', acr_identity='[system]') + mock_get_acr_cred.assert_not_called() mock_assign_identity.assert_called_once_with( cmd, 'rg', 'web1', ['[system]'], 'Reader', None, None) + mock_update_site_configs.assert_called_once_with( + cmd, 'rg', 'web1', slot=None, + acr_use_identity='true', acr_identity='[system]') + @mock.patch('azure.cli.command_modules.appservice.custom._get_acr_cred') @mock.patch('azure.cli.command_modules.appservice.custom._mask_creds_related_appsettings') @mock.patch('azure.cli.command_modules.appservice.custom._filter_for_container_settings') @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[]) @@ -777,7 +790,8 @@ def test_container_set_with_custom_role( def test_container_set_disable_acr_identity( self, mock_update_site_configs, mock_assign_identity, mock_add_fx, mock_site_op, mock_client_factory, mock_centauri, - mock_settings_op, mock_get_app, mock_filter, mock_mask): + mock_settings_op, mock_get_app, mock_filter, mock_mask, + mock_get_acr_cred): mock_mask.return_value = {} mock_site_op.return_value = self._make_mock_app_settings() cmd = _get_test_cmd() @@ -788,7 +802,7 @@ def test_container_set_disable_acr_identity( mock_assign_identity.assert_not_called() mock_update_site_configs.assert_called_once_with( cmd, 'rg', 'web1', slot=None, - acr_use_identity='false', acr_identity=None) + acr_use_identity='false') @mock.patch('azure.cli.command_modules.appservice.custom._get_acr_cred') @mock.patch('azure.cli.command_modules.appservice.custom._mask_creds_related_appsettings') From 1e1d6d77d59def04fde2bc1ef891c8c7d34dcaaf Mon Sep 17 00:00:00 2001 From: Jordan Selig Date: Fri, 27 Mar 2026 10:29:01 -0400 Subject: [PATCH 5/5] Fix pylint too-many-branches and flake8 E128 indent Extract _build_container_updates helper to reduce branch count in update_container_settings from 22 to 18 (max 20). Fix continuation line indentation for visual alignment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cli/command_modules/appservice/custom.py | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index 959b6eae232..d1bffe9781b 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -3603,6 +3603,21 @@ def _is_key_vault_reference(value): return isinstance(value, str) and value.strip().startswith('@Microsoft.KeyVault(') +def _build_container_updates(container_registry_url, container_registry_user, + container_registry_password, websites_enable_app_service_storage): + """Build dict of container-specific app settings that were explicitly provided.""" + updates = {} + if container_registry_url is not None: + updates['DOCKER_REGISTRY_SERVER_URL'] = container_registry_url + if container_registry_user is not None: + updates['DOCKER_REGISTRY_SERVER_USERNAME'] = container_registry_user + if container_registry_password is not None: + updates['DOCKER_REGISTRY_SERVER_PASSWORD'] = container_registry_password + if websites_enable_app_service_storage: + updates['WEBSITES_ENABLE_APP_SERVICE_STORAGE'] = websites_enable_app_service_storage + return updates + + def update_container_settings(cmd, resource_group_name, name, container_registry_url=None, container_image_name=None, container_registry_user=None, websites_enable_app_service_storage=None, container_registry_password=None, @@ -3632,20 +3647,14 @@ def update_container_settings(cmd, resource_group_name, name, container_registry registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] try: container_registry_user, container_registry_password = _get_acr_cred(cmd.cli_ctx, - registry_name) + registry_name) except Exception as ex: # pylint: disable=broad-except logger.warning("Retrieving credentials failed with an exception:'%s'", ex) # Build dict of only the container-specific settings that were explicitly provided - container_updates = {} - if container_registry_url is not None: - container_updates['DOCKER_REGISTRY_SERVER_URL'] = container_registry_url - if container_registry_user is not None: - container_updates['DOCKER_REGISTRY_SERVER_USERNAME'] = container_registry_user - if container_registry_password is not None: - container_updates['DOCKER_REGISTRY_SERVER_PASSWORD'] = container_registry_password - if websites_enable_app_service_storage: - container_updates['WEBSITES_ENABLE_APP_SERVICE_STORAGE'] = websites_enable_app_service_storage + container_updates = _build_container_updates(container_registry_url, container_registry_user, + container_registry_password, + websites_enable_app_service_storage) if container_updates: # Merge only container-specific keys into the existing settings,