Skip to content

Commit f498195

Browse files
committed
feat: add new flags to support regional endpoints feature
1 parent 8fbf679 commit f498195

32 files changed

Lines changed: 5936 additions & 4402 deletions

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
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
@@ -28,6 +28,9 @@
2828
DENY_ACTION = 'Deny'
2929
DOMAIN_NAME_LABEL_SCOPE_UNSECURE = 'Unsecure'
3030
DOMAIN_NAME_LABEL_SCOPE_RESOURCE_GROUP_REUSE = 'ResourceGroupReuse'
31+
REGIONAL_ENDPOINTS_NOT_SUPPORTED = "Regional endpoints are only supported for managed registries in Premium SKU."
32+
REGIONAL_ENDPOINTS_NOT_SUPPORTED_FOR_DCT = "Regional endpoints cannot be enabled when Content Trust is enabled. " \
33+
"Please disable Content Trust and try again."
3134

3235

3336
def acr_check_name(cmd, client, registry_name, resource_group_name=None, dnl_scope=DOMAIN_NAME_LABEL_SCOPE_UNSECURE):
@@ -72,7 +75,8 @@ def acr_create(cmd,
7275
tags=None,
7376
allow_metadata_search=None,
7477
dnl_scope=None,
75-
role_assignment_mode=None):
78+
role_assignment_mode=None,
79+
regional_endpoints=None):
7680
if default_action and sku not in get_premium_sku(cmd):
7781
raise CLIError(NETWORK_RULE_NOT_SUPPORTED)
7882

@@ -106,6 +110,9 @@ def acr_create(cmd,
106110
if role_assignment_mode is not None:
107111
_configure_role_assignment_mode(cmd, registry, role_assignment_mode)
108112

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

@@ -151,7 +158,8 @@ def acr_update_custom(cmd,
151158
allow_exports=None,
152159
tags=None,
153160
allow_metadata_search=None,
154-
role_assignment_mode=None):
161+
role_assignment_mode=None,
162+
regional_endpoints=None):
155163
if sku is not None:
156164
Sku = cmd.get_models('Sku')
157165
instance.sku = Sku(name=sku)
@@ -180,6 +188,9 @@ def acr_update_custom(cmd,
180188
if role_assignment_mode is not None:
181189
_configure_role_assignment_mode(cmd, instance, role_assignment_mode)
182190

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

@@ -242,6 +253,27 @@ def acr_update_set(cmd,
242253

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

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

247279

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

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

282323

@@ -286,11 +327,15 @@ def acr_login(cmd,
286327
tenant_suffix=None,
287328
username=None,
288329
password=None,
289-
expose_token=False):
330+
expose_token=False,
331+
endpoint=None):
290332
if expose_token:
291333
if username or password:
292334
raise CLIError("`--expose-token` cannot be combined with `--username` or `--password`.")
293335

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

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

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

Lines changed: 34 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,37 @@ 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+
parts = uri.split('.')
126+
127+
if len(parts) == 5 and parts[2] == 'geo' and uri.endswith(login_server_suffix):
128+
return f"{parts[0]}{login_server_suffix}"
129+
130+
# If not a regional endpoint format, return as-is
131+
return uri
132+
133+
134+
def _get_azure_registry(cmd, source_registry):
135+
"""Get Azure registry from login server URI or registry name, handling regional endpoint URI."""
136+
lookup_uri = source_registry
137+
138+
# Try to get the pre-defined login server suffix.
139+
login_server_suffix = get_login_server_suffix(cmd.cli_ctx)
140+
# Convert regional endpoint to standard format if applicable
141+
if login_server_suffix and source_registry.endswith(f".geo{login_server_suffix}"):
142+
lookup_uri = _regional_endpoint_uri_to_login_server(source_registry, login_server_suffix)
143+
144+
# Search by login server (lookup_uri) and registry name (source_registry) to handle both URI and name inputs
145+
return get_registry_from_name_or_login_server(cmd.cli_ctx, lookup_uri, source_registry)
146+
147+
120148
def _handle_import_exception(e, cmd, source_registry, source_image, registry):
121149
from msrest.exceptions import ClientException
122150
try:
@@ -127,7 +155,7 @@ def _handle_import_exception(e, cmd, source_registry, source_image, registry):
127155
if is_valid_resource_id(source_registry):
128156
registry, _ = get_registry_by_name(cmd.cli_ctx, parse_resource_id(source_registry)["name"])
129157
else:
130-
registry = get_registry_from_name_or_login_server(cmd.cli_ctx, source_registry, source_registry)
158+
registry = _get_azure_registry(cmd, source_registry)
131159

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

0 commit comments

Comments
 (0)