From eee381e747c1e5cfeee32f642edbd31864fbbdf6 Mon Sep 17 00:00:00 2001 From: Zoey Li Date: Mon, 20 Jul 2026 15:38:24 -0400 Subject: [PATCH 1/2] [ACR] az acr import: fix regional endpoint source resolution for multi-label sovereign suffixes _regional_endpoint_uri_to_login_server assumed a fixed 5-part hostname (..geo.azurecr.io), so sovereign login-server suffixes with more than one label (e.g. azurecr.sovcloud-azure.de) were left unconverted. The source then fell back to an anonymous cross-firewall pull and failed with 403 DENIED. Strip the ".geo" tail instead of counting dot-separated parts, making conversion suffix-length-agnostic. Add unit coverage for sovereign (delos .de, bleu .fr) and DNL registries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6daaab37-9f50-49e2-bbba-9dd6ff4b3727 --- .../azure/cli/command_modules/acr/import.py | 13 ++++++++++--- .../latest/test_regional_endpoint_utils.py | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/acr/import.py b/src/azure-cli/azure/cli/command_modules/acr/import.py index 17bd01ca7a5..3798647ceba 100644 --- a/src/azure-cli/azure/cli/command_modules/acr/import.py +++ b/src/azure-cli/azure/cli/command_modules/acr/import.py @@ -124,10 +124,17 @@ def _regional_endpoint_uri_to_login_server(uri, login_server_suffix): uri_lower = uri.strip().lower() suffix_lower = login_server_suffix.lower() - parts = uri_lower.split('.') - if len(parts) == 5 and parts[2] == 'geo' and uri_lower.endswith(suffix_lower): - return f"{parts[0]}{login_server_suffix}" + # A regional endpoint looks like "..geo". The suffix itself may + # contain more than one label (e.g. ".azurecr.io" in public clouds vs + # ".azurecr.sovcloud-azure.de" in sovereign clouds), so we strip the ".geo" tail + # rather than assuming a fixed number of dot-separated parts. + geo_suffix = f".geo{suffix_lower}" + if uri_lower.endswith(geo_suffix): + prefix = uri_lower[:-len(geo_suffix)] # "." + prefix_parts = prefix.split('.') + if len(prefix_parts) == 2 and prefix_parts[0]: + return f"{prefix_parts[0]}{login_server_suffix}" # If not a regional endpoint format, return as-is return uri diff --git a/src/azure-cli/azure/cli/command_modules/acr/tests/latest/test_regional_endpoint_utils.py b/src/azure-cli/azure/cli/command_modules/acr/tests/latest/test_regional_endpoint_utils.py index 28cb215bb9e..954bf9a4d7d 100644 --- a/src/azure-cli/azure/cli/command_modules/acr/tests/latest/test_regional_endpoint_utils.py +++ b/src/azure-cli/azure/cli/command_modules/acr/tests/latest/test_regional_endpoint_utils.py @@ -30,6 +30,22 @@ def test_valid_regional_endpoint_conversion(self): result = acr_import._regional_endpoint_uri_to_login_server(regional_uri, login_server_suffix) self.assertEqual(result, expected) + def test_valid_regional_endpoint_conversion_multi_label_suffix(self): + """Regional endpoints in sovereign clouds whose login-server suffix has more than one + label (e.g. '.azurecr.sovcloud-azure.de') must still be converted. """ + test_cases = [ + ('registry123.deloscloudgermanycentral.geo.azurecr.sovcloud-azure.de', + '.azurecr.sovcloud-azure.de', 'registry123.azurecr.sovcloud-azure.de'), + ('myregistry.francecentral.geo.azurecr.sovcloud-azure.fr', + '.azurecr.sovcloud-azure.fr', 'myregistry.azurecr.sovcloud-azure.fr'), + # DNL registry (hash suffix) in a multi-label sovereign cloud + ('myregistry-d7ezgzevdwfvc8ht.deloscloudgermanycentral.geo.azurecr.sovcloud-azure.de', + '.azurecr.sovcloud-azure.de', 'myregistry-d7ezgzevdwfvc8ht.azurecr.sovcloud-azure.de'), + ] + + for regional_uri, suffix, expected in test_cases: + result = acr_import._regional_endpoint_uri_to_login_server(regional_uri, suffix) + self.assertEqual(result, expected) def test_non_regional_endpoint_uris_unchanged(self): """Test that non-regional endpoint URIs are returned unchanged.""" @@ -40,6 +56,8 @@ def test_non_regional_endpoint_uris_unchanged(self): 'testregistry.azurecr.io', 'external-registry.com', 'testregistry.eastus.notgeo.azurecr.io', + 'testregistry.azurecr.sovcloud-azure.de', + 'testregistry.deloscloudgermanycentral.notgeo.azurecr.sovcloud-azure.de' ] for uri in test_cases: From 429c62dfb800e4b9a03bbe6782685f302adab98d Mon Sep 17 00:00:00 2001 From: Zoey Li Date: Mon, 20 Jul 2026 15:52:54 -0400 Subject: [PATCH 2/2] [ACR] az acr import: address review feedback on regional endpoint fix - Reject regional endpoints with an empty region label (require both and to be non-empty) so malformed URIs like "registry..geo" are no longer converted. - Clarify comment/docstring wording ("more than two labels") and drop a stray space before a docstring's closing quotes. - Add a unit case covering the empty-region rejection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6daaab37-9f50-49e2-bbba-9dd6ff4b3727 --- src/azure-cli/azure/cli/command_modules/acr/import.py | 6 +++--- .../acr/tests/latest/test_regional_endpoint_utils.py | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/acr/import.py b/src/azure-cli/azure/cli/command_modules/acr/import.py index 3798647ceba..0250578f697 100644 --- a/src/azure-cli/azure/cli/command_modules/acr/import.py +++ b/src/azure-cli/azure/cli/command_modules/acr/import.py @@ -125,15 +125,15 @@ def _regional_endpoint_uri_to_login_server(uri, login_server_suffix): uri_lower = uri.strip().lower() suffix_lower = login_server_suffix.lower() - # A regional endpoint looks like "..geo". The suffix itself may - # contain more than one label (e.g. ".azurecr.io" in public clouds vs + # A regional endpoint looks like "..geo". The suffix may contain + # more than two labels (e.g. ".azurecr.io" in public clouds vs the longer # ".azurecr.sovcloud-azure.de" in sovereign clouds), so we strip the ".geo" tail # rather than assuming a fixed number of dot-separated parts. geo_suffix = f".geo{suffix_lower}" if uri_lower.endswith(geo_suffix): prefix = uri_lower[:-len(geo_suffix)] # "." prefix_parts = prefix.split('.') - if len(prefix_parts) == 2 and prefix_parts[0]: + if len(prefix_parts) == 2 and prefix_parts[0] and prefix_parts[1]: return f"{prefix_parts[0]}{login_server_suffix}" # If not a regional endpoint format, return as-is diff --git a/src/azure-cli/azure/cli/command_modules/acr/tests/latest/test_regional_endpoint_utils.py b/src/azure-cli/azure/cli/command_modules/acr/tests/latest/test_regional_endpoint_utils.py index 954bf9a4d7d..a971da8145e 100644 --- a/src/azure-cli/azure/cli/command_modules/acr/tests/latest/test_regional_endpoint_utils.py +++ b/src/azure-cli/azure/cli/command_modules/acr/tests/latest/test_regional_endpoint_utils.py @@ -31,8 +31,8 @@ def test_valid_regional_endpoint_conversion(self): self.assertEqual(result, expected) def test_valid_regional_endpoint_conversion_multi_label_suffix(self): - """Regional endpoints in sovereign clouds whose login-server suffix has more than one - label (e.g. '.azurecr.sovcloud-azure.de') must still be converted. """ + """Regional endpoints in sovereign clouds whose login-server suffix has more than two + labels (e.g. '.azurecr.sovcloud-azure.de') must still be converted.""" test_cases = [ ('registry123.deloscloudgermanycentral.geo.azurecr.sovcloud-azure.de', '.azurecr.sovcloud-azure.de', 'registry123.azurecr.sovcloud-azure.de'), @@ -56,6 +56,8 @@ def test_non_regional_endpoint_uris_unchanged(self): 'testregistry.azurecr.io', 'external-registry.com', 'testregistry.eastus.notgeo.azurecr.io', + # Malformed: empty region label must NOT be converted + 'testregistry..geo.azurecr.io', 'testregistry.azurecr.sovcloud-azure.de', 'testregistry.deloscloudgermanycentral.notgeo.azurecr.sovcloud-azure.de' ]