Skip to content

Commit 574d1f3

Browse files
authored
{ACR} Update commands to support Regional Endpoints feature (#33133)
1 parent 1e57969 commit 574d1f3

38 files changed

Lines changed: 11307 additions & 14079 deletions

File tree

src/azure-cli/azure/cli/command_modules/acr/_help.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@
179179
- name: Create a registry with ABAC-based Repository Permission enabled.
180180
text: >
181181
az acr create -n myregistry -g MyResourceGroup --sku Standard --role-assignment-mode rbac-abac
182+
- name: Create a managed container registry with the Premium SKU and regional endpoints enabled.
183+
text: >
184+
az acr create -n myregistry -g MyResourceGroup --sku Premium --regional-endpoints enabled
182185
"""
183186

184187
helps['acr credential'] = """
@@ -325,6 +328,9 @@
325328
- name: Import an image without waiting for successful completion. Failures during import will not be reflected. Run `az acr repository show-tags` to confirm that import succeeded.
326329
text: >
327330
az acr import -n myregistry --source sourceregistry.azurecr.io/sourcerepository:sourcetag --no-wait
331+
- name: Import an image using a regional endpoint URI as the source.
332+
text: >
333+
az acr import -n myregistry --source sourceregistry.eastus.geo.azurecr.io/sourcerepository:sourcetag
328334
"""
329335

330336
helps['acr list'] = """
@@ -350,6 +356,9 @@
350356
- name: Get an Azure Container Registry access token
351357
text: >
352358
az acr login -n myregistry --expose-token
359+
- name: Log in to a specific regional endpoint of an Azure Container Registry. Requires regional endpoints to be enabled on the registry.
360+
text: >
361+
az acr login -n myregistry --endpoint eastus
353362
"""
354363

355364
helps['acr network-rule'] = """
@@ -1514,6 +1523,9 @@
15141523
- name: Turn on ABAC-based Repository Permission on an existing registry.
15151524
text: >
15161525
az acr update -n myregistry --role-assignment-mode rbac-abac
1526+
- name: Enable regional endpoints on an existing registry.
1527+
text: >
1528+
az acr update -n myregistry --regional-endpoints enabled
15171529
"""
15181530

15191531
helps['acr webhook'] = """
@@ -1873,6 +1885,10 @@
18731885

18741886
helps['acr show-endpoints'] = """
18751887
type: command
1876-
short-summary: Display registry endpoints
1888+
short-summary: Display registry endpoints including data endpoints and regional endpoints if configured.
1889+
examples:
1890+
- name: Show the endpoints for a registry.
1891+
text: >
1892+
az acr show-endpoints -n myregistry
18771893
"""
18781894
# endregion

src/azure-cli/azure/cli/command_modules/acr/_params.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
get_resource_name_completion_list,
1717
quotes,
1818
get_three_state_flag,
19-
get_enum_type
19+
get_enum_type,
20+
get_location_completion_list,
21+
get_location_name_type
2022
)
2123
from azure.cli.core.commands.validators import get_default_location_from_resource_group
2224
from .policy import RetentionType
@@ -75,9 +77,9 @@
7577

7678
def load_arguments(self, _): # pylint: disable=too-many-statements
7779
PasswordName, DefaultAction, PolicyStatus, WebhookAction, WebhookStatus, \
78-
TokenStatus, ZoneRedundancy, AutoGeneratedDomainNameLabelScope = self.get_models(
80+
TokenStatus, ZoneRedundancy, AutoGeneratedDomainNameLabelScope, RegionalEndpoints = self.get_models(
7981
'PasswordName', 'DefaultAction', 'PolicyStatus', 'WebhookAction', 'WebhookStatus',
80-
'TokenStatus', 'ZoneRedundancy', 'AutoGeneratedDomainNameLabelScope')
82+
'TokenStatus', 'ZoneRedundancy', 'AutoGeneratedDomainNameLabelScope', 'RegionalEndpoints')
8183
from azure.mgmt.containerregistrytasks.models import (
8284
TaskStatus, BaseImageTriggerType, SourceRegistryLoginMode, UpdateTriggerPayloadType, RunStatus)
8385

@@ -114,6 +116,7 @@ def load_arguments(self, _): # pylint: disable=too-many-statements
114116
help='Default action to apply when no rule matches. Only applicable to Premium SKU.')
115117
c.argument('public_network_enabled', get_three_state_flag(), help="Allow public network access for the container registry.{suffix}".format(suffix=default_allow_suffix))
116118
c.argument('allow_trusted_services', get_three_state_flag(), help="Allow trusted Azure Services to access network restricted registries. For more information, please visit https://aka.ms/acr/trusted-services.{suffix}".format(suffix=default_allow_suffix))
119+
c.argument('regional_endpoints', arg_type=get_enum_type(RegionalEndpoints), is_preview=True, help="Indicates whether or not regional endpoints should be enabled for the registry.")
117120

118121
for scope in ['acr create', 'acr update']:
119122
with self.argument_context(scope, arg_group="Permissions and Role Assignment") as c:
@@ -169,6 +172,7 @@ def load_arguments(self, _): # pylint: disable=too-many-statements
169172

170173
with self.argument_context('acr login') as c:
171174
c.argument('expose_token', options_list=['--expose-token', '-t'], help='Expose refresh token instead of automatically logging in through Docker CLI', action='store_true')
175+
c.argument("endpoint", completer=get_location_completion_list, type=get_location_name_type(self.cli_ctx), is_preview=True, help="Log in to a specific regional endpoint of the container registry. Specify the region name (e.g., eastus, westus2). Only applicable when regional endpoints are enabled.")
172176

173177
with self.argument_context('acr repository') as c:
174178
c.argument('resource_group_name', deprecate_info=c.deprecate(hide=True))

src/azure-cli/azure/cli/command_modules/acr/custom.py

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
DENY_ACTION = 'Deny'
3030
DOMAIN_NAME_LABEL_SCOPE_UNSECURE = 'Unsecure'
3131
DOMAIN_NAME_LABEL_SCOPE_RESOURCE_GROUP_REUSE = 'ResourceGroupReuse'
32+
REGIONAL_ENDPOINTS_NOT_SUPPORTED = "Regional endpoints are only supported for managed registries in Premium SKU."
33+
REGIONAL_ENDPOINTS_NOT_SUPPORTED_FOR_DCT = "Regional endpoints cannot be enabled when Content Trust is enabled. " \
34+
"Please disable Content Trust and try again."
3235

3336

3437
def acr_check_name(cmd, client, registry_name, resource_group_name=None, dnl_scope=DOMAIN_NAME_LABEL_SCOPE_UNSECURE):
@@ -73,7 +76,8 @@ def acr_create(cmd,
7376
tags=None,
7477
allow_metadata_search=None,
7578
dnl_scope=None,
76-
role_assignment_mode=None):
79+
role_assignment_mode=None,
80+
regional_endpoints=None):
7781
if default_action and sku not in get_premium_sku(cmd):
7882
raise CLIError(NETWORK_RULE_NOT_SUPPORTED)
7983

@@ -107,6 +111,9 @@ def acr_create(cmd,
107111
if role_assignment_mode is not None:
108112
_configure_role_assignment_mode(cmd, registry, role_assignment_mode)
109113

114+
if regional_endpoints is not None:
115+
_configure_regional_endpoints(cmd, registry, sku, regional_endpoints)
116+
110117
_handle_network_bypass(cmd, registry, allow_trusted_services)
111118
_handle_export_policy(cmd, registry, allow_exports)
112119

@@ -152,7 +159,8 @@ def acr_update_custom(cmd,
152159
allow_exports=None,
153160
tags=None,
154161
allow_metadata_search=None,
155-
role_assignment_mode=None):
162+
role_assignment_mode=None,
163+
regional_endpoints=None):
156164
if sku is not None:
157165
Sku = cmd.get_models('Sku')
158166
instance.sku = Sku(name=sku)
@@ -181,6 +189,9 @@ def acr_update_custom(cmd,
181189
if role_assignment_mode is not None:
182190
_configure_role_assignment_mode(cmd, instance, role_assignment_mode)
183191

192+
if regional_endpoints is not None:
193+
_configure_regional_endpoints(cmd, instance, sku, regional_endpoints)
194+
184195
_handle_network_bypass(cmd, instance, allow_trusted_services)
185196
_handle_export_policy(cmd, instance, allow_exports)
186197

@@ -243,6 +254,27 @@ def acr_update_set(cmd,
243254

244255
validate_sku_update(cmd, registry.sku.name, parameters.sku)
245256

257+
# Determine the effective SKU (new SKU if being updated, otherwise current SKU)
258+
sku = parameters.sku.name if parameters.sku else registry.sku.name
259+
260+
RegionalEndpoints = cmd.get_models('RegionalEndpoints')
261+
if parameters.regional_endpoints == RegionalEndpoints.ENABLED:
262+
# Regional endpoints require Premium SKU, validate registry tier compatibility
263+
if sku not in get_premium_sku(cmd):
264+
raise CLIError(REGIONAL_ENDPOINTS_NOT_SUPPORTED)
265+
266+
# Regional endpoints are incompatible with Docker Content Trust (DCT), check for conflicts
267+
if registry.policies and registry.policies.trust_policy and registry.policies.trust_policy.status == 'enabled':
268+
raise CLIError(REGIONAL_ENDPOINTS_NOT_SUPPORTED_FOR_DCT)
269+
270+
# Recommend enabling data endpoints for optimal performance when using regional endpoints
271+
if registry.data_endpoint_enabled is False:
272+
logger.warning(
273+
"It is recommended to also enable dedicated data endpoints "
274+
"(--enable-data-endpoint) for optimal in-region performance "
275+
"when using regional endpoints."
276+
)
277+
246278
return client.begin_update(resource_group_name, registry_name, parameters)
247279

248280

@@ -278,6 +310,15 @@ def acr_show_endpoints(cmd,
278310
'endpoint': '*.blob.' + cmd.cli_ctx.cloud.suffixes.storage_endpoint,
279311
})
280312

313+
RegionalEndpoints = cmd.get_models('RegionalEndpoints')
314+
if registry.regional_endpoints == RegionalEndpoints.ENABLED:
315+
info['regionalEndpoints'] = []
316+
for host in registry.regional_endpoint_host_names:
317+
info['regionalEndpoints'].append({
318+
'region': host.split('.')[1],
319+
'endpoint': host,
320+
})
321+
281322
return info
282323

283324

@@ -287,11 +328,15 @@ def acr_login(cmd,
287328
tenant_suffix=None,
288329
username=None,
289330
password=None,
290-
expose_token=False):
331+
expose_token=False,
332+
endpoint=None):
291333
if expose_token:
292334
if username or password:
293335
raise CLIError("`--expose-token` cannot be combined with `--username` or `--password`.")
294336

337+
if endpoint:
338+
raise CLIError("`--expose-token` cannot be combined with `--endpoint`.")
339+
295340
login_server, _, password = get_login_credentials(
296341
cmd=cmd,
297342
registry_name=registry_name,
@@ -346,6 +391,34 @@ def acr_login(cmd,
346391
logger.warning('Uppercase characters are detected in the registry name. When using its server url in '
347392
'docker commands, to avoid authentication errors, use all lowercase.')
348393

394+
if endpoint:
395+
registry, _ = get_registry_by_name(cmd.cli_ctx, registry_name, resource_group_name)
396+
matching_endpoint = None
397+
398+
RegionalEndpoints = cmd.get_models('RegionalEndpoints')
399+
if registry.regional_endpoints == RegionalEndpoints.ENABLED and registry.regional_endpoint_host_names:
400+
# Build the expected regional endpoint prefix: registryname.region.geo.
401+
regional_endpoint_prefix = f"{registry_name}.{endpoint}.geo.".lower()
402+
matching_endpoint = next(
403+
(url for url in registry.regional_endpoint_host_names
404+
if url.lower().strip().startswith(regional_endpoint_prefix)), None)
405+
406+
if matching_endpoint:
407+
logger.warning("Logging in to regional endpoint: %s", matching_endpoint)
408+
_perform_registry_login(matching_endpoint, docker_command, username, password)
409+
else:
410+
raise CLIError(
411+
"Regional endpoint for '{}' not found. Aborting login. "
412+
"Run 'az acr show-endpoints -n {}' to list available regional endpoints.".format(
413+
endpoint, registry_name)
414+
)
415+
else:
416+
_perform_registry_login(login_server, docker_command, username, password)
417+
418+
return None
419+
420+
421+
def _perform_registry_login(login_server, docker_command, username, password):
349422
from subprocess import PIPE, Popen
350423
logger.debug("Invoking '%s login --username %s --password <redacted> %s'",
351424
docker_command, username, login_server)
@@ -690,3 +763,12 @@ def _configure_role_assignment_mode(cmd, registry, role_assignment_mode):
690763
"'--source-registry-auth-id' flag in 'az acr task update'. Please refer to "
691764
"https://aka.ms/acr/auth/abac for more details.")
692765
registry.role_assignment_mode = mode
766+
767+
768+
def _configure_regional_endpoints(cmd, registry, sku, regional_endpoints):
769+
RegionalEndpoints = cmd.get_models('RegionalEndpoints')
770+
771+
if regional_endpoints == RegionalEndpoints.ENABLED and sku and sku not in get_premium_sku(cmd):
772+
raise CLIError(REGIONAL_ENDPOINTS_NOT_SUPPORTED)
773+
774+
registry.regional_endpoints = regional_endpoints

src/azure-cli/azure/cli/command_modules/acr/import.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def acr_import(cmd, # pylint: disable=too-many-locals
5454
if is_valid_resource_id(source_registry):
5555
source = ImportSource(resource_id=source_registry, source_image=source_image)
5656
else:
57-
registry = get_registry_from_name_or_login_server(cmd.cli_ctx, source_registry, source_registry)
57+
registry = _get_azure_registry(cmd, source_registry)
5858
if registry:
5959
# trim away redundant login server name, a common error
6060
prefix = registry.login_server + '/'
@@ -78,10 +78,7 @@ def acr_import(cmd, # pylint: disable=too-many-locals
7878
credentials=ImportSourceCredentials(password=source_registry_password,
7979
username=source_registry_username))
8080
else:
81-
# Try to get the pre-defined login server suffix.
82-
login_server_suffix = get_login_server_suffix(cmd.cli_ctx)
83-
if not login_server_suffix or registry_uri.endswith(login_server_suffix):
84-
registry = get_registry_from_name_or_login_server(cmd.cli_ctx, registry_uri)
81+
registry = _get_azure_registry(cmd, registry_uri)
8582
if registry:
8683
# For Azure container registry
8784
source = ImportSource(resource_id=registry.id, source_image=source_image)
@@ -117,6 +114,39 @@ def acr_import(cmd, # pylint: disable=too-many-locals
117114
_handle_import_exception(e, cmd, source_registry, source_image, registry)
118115

119116

117+
def _regional_endpoint_uri_to_login_server(uri, login_server_suffix):
118+
"""Convert regional endpoint URI to standard login server URI.
119+
120+
Example: testregistry.eastus.geo.azurecr.io -> testregistry.azurecr.io
121+
"""
122+
if not uri or not login_server_suffix:
123+
return uri
124+
125+
uri_lower = uri.strip().lower()
126+
suffix_lower = login_server_suffix.lower()
127+
parts = uri_lower.split('.')
128+
129+
if len(parts) == 5 and parts[2] == 'geo' and uri_lower.endswith(suffix_lower):
130+
return f"{parts[0]}{login_server_suffix}"
131+
132+
# If not a regional endpoint format, return as-is
133+
return uri
134+
135+
136+
def _get_azure_registry(cmd, source_registry):
137+
"""Get Azure registry from login server URI or registry name, handling regional endpoint URI."""
138+
lookup_uri = source_registry
139+
140+
# Try to get the pre-defined login server suffix.
141+
login_server_suffix = get_login_server_suffix(cmd.cli_ctx)
142+
# Convert regional endpoint to standard format if applicable
143+
if login_server_suffix and source_registry.endswith(f".geo{login_server_suffix}"):
144+
lookup_uri = _regional_endpoint_uri_to_login_server(source_registry, login_server_suffix)
145+
146+
# Search by login server (lookup_uri) and registry name (source_registry) to handle both URI and name inputs
147+
return get_registry_from_name_or_login_server(cmd.cli_ctx, lookup_uri, source_registry)
148+
149+
120150
def _handle_import_exception(e, cmd, source_registry, source_image, registry):
121151
from msrest.exceptions import ClientException
122152
try:
@@ -127,7 +157,7 @@ def _handle_import_exception(e, cmd, source_registry, source_image, registry):
127157
if is_valid_resource_id(source_registry):
128158
registry, _ = get_registry_by_name(cmd.cli_ctx, parse_resource_id(source_registry)["name"])
129159
else:
130-
registry = get_registry_from_name_or_login_server(cmd.cli_ctx, source_registry, source_registry)
160+
registry = _get_azure_registry(cmd, source_registry)
131161

132162
if registry.login_server.lower() in source_image.lower():
133163
logger.warning("Import from source failed.\n\tsource image: '%s'\n"

0 commit comments

Comments
 (0)