From 394d88c2773b00c93c914e44afcdfedb135c6d06 Mon Sep 17 00:00:00 2001 From: Bogdan Trifan <222235485+bogdantrifan13@users.noreply.github.com> Date: Wed, 21 Oct 2026 11:04:05 +0300 Subject: [PATCH] [ContainerApp] `az containerapp update`: Fix `--yaml` dropping `identity.userAssignedIdentities` clean_null_values removed every empty dict, including the {} value of each userAssignedIdentities entry, so the update-from-YAML PATCH carried only identity.type and silently lost the declared user-assigned identities. Keep those entries (still stripping null read-only fields inside them). Co-Authored-By: Claude Opus 5.5 --- .../command_modules/containerapp/_utils.py | 9 +++- .../tests/latest/test_containerapp_utils.py | 41 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/azure-cli/azure/cli/command_modules/containerapp/_utils.py b/src/azure-cli/azure/cli/command_modules/containerapp/_utils.py index 35e98f1ab82..aaca2f5c259 100644 --- a/src/azure-cli/azure/cli/command_modules/containerapp/_utils.py +++ b/src/azure-cli/azure/cli/command_modules/containerapp/_utils.py @@ -965,11 +965,12 @@ def _remove_readonly_attributes(containerapp_def): # Remove null/None/empty properties in a model since the PATCH API will delete those. Not needed once we move to the SDK +# userAssignedIdentities entries are kept even when empty: the resource ID key is the payload and its value is {}. def clean_null_values(d): if isinstance(d, dict): return { k: v - for k, v in ((k, clean_null_values(v)) for k, v in d.items()) + for k, v in ((k, _clean_child_value(k, v)) for k, v in d.items()) if (isinstance(v, dict) and len(v.items()) > 0) or (not isinstance(v, dict) and v is not None) } if isinstance(d, list): @@ -977,6 +978,12 @@ def clean_null_values(d): return d +def _clean_child_value(key, value): + if key == "userAssignedIdentities" and isinstance(value, dict): + return {identity_id: clean_null_values(properties) or {} for identity_id, properties in value.items()} + return clean_null_values(value) + + def _populate_secret_values(containerapp_def, secret_values): secrets = safe_get(containerapp_def, "properties", "configuration", "secrets", default=None) if not secrets: diff --git a/src/azure-cli/azure/cli/command_modules/containerapp/tests/latest/test_containerapp_utils.py b/src/azure-cli/azure/cli/command_modules/containerapp/tests/latest/test_containerapp_utils.py index 16641e3e569..63f6df463c0 100644 --- a/src/azure-cli/azure/cli/command_modules/containerapp/tests/latest/test_containerapp_utils.py +++ b/src/azure-cli/azure/cli/command_modules/containerapp/tests/latest/test_containerapp_utils.py @@ -211,6 +211,47 @@ def test_clean_empty_values(self): self.assertEqual(expect_result_for_new, result_new) self.assertEqual(expect_result_for_old, result_old) + def test_clean_null_values_keeps_user_assigned_identities(self): + identity_id = ("/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg" + "/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id") + other_identity_id = identity_id + "2" + test_dict = { + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + identity_id: {}, + other_identity_id: {"principalId": None, "clientId": None}, + }, + }, + "properties": { + "configuration": { + "registries": [{"server": "registry.azurecr.io", "identity": identity_id}], + }, + }, + } + + result = clean_null_values(test_dict) + + self.assertEqual({ + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + identity_id: {}, + other_identity_id: {}, + }, + }, + "properties": { + "configuration": { + "registries": [{"server": "registry.azurecr.io", "identity": identity_id}], + }, + }, + }, result) + + # An empty or null map is still removed, as before + for empty in ({}, None): + self.assertEqual({"identity": {"type": "None"}}, + clean_null_values({"identity": {"type": "None", "userAssignedIdentities": empty}})) + def test_load_cert_file(self): pfx_file = os.path.join(TEST_DIR, 'data', 'cert.pfx') testpassword = 'test12'