Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion src/azure-cli/azure/cli/command_modules/acr/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@
- name: Create a registry with ABAC-based Repository Permission enabled.
text: >
az acr create -n myregistry -g MyResourceGroup --sku Standard --role-assignment-mode rbac-abac
- name: Create a managed container registry with the Premium SKU and regional endpoints enabled.
text: >
az acr create -n myregistry -g MyResourceGroup --sku Premium --regional-endpoints enabled
"""

helps['acr credential'] = """
Expand Down Expand Up @@ -325,6 +328,9 @@
- 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.
text: >
az acr import -n myregistry --source sourceregistry.azurecr.io/sourcerepository:sourcetag --no-wait
- name: Import an image using a regional endpoint URI as the source.
text: >
az acr import -n myregistry --source sourceregistry.eastus.geo.azurecr.io/sourcerepository:sourcetag
"""

helps['acr list'] = """
Expand All @@ -350,6 +356,9 @@
- name: Get an Azure Container Registry access token
text: >
az acr login -n myregistry --expose-token
- name: Log in to a specific regional endpoint of an Azure Container Registry
text: >
az acr login -n myregistry --endpoint eastus
"""

helps['acr network-rule'] = """
Expand Down Expand Up @@ -1514,6 +1523,9 @@
- name: Turn on ABAC-based Repository Permission on an existing registry.
text: >
az acr update -n myregistry --role-assignment-mode rbac-abac
- name: Enable regional endpoints on an existing registry.
text: >
az acr update -n myregistry --regional-endpoints enabled
"""

helps['acr webhook'] = """
Expand Down Expand Up @@ -1873,6 +1885,10 @@

helps['acr show-endpoints'] = """
type: command
short-summary: Display registry endpoints
short-summary: Display registry endpoints including data endpoints and regional endpoints if configured.
examples:
- name: Show the endpoints for a registry.
text: >
az acr show-endpoints -n myregistry
"""
# endregion
10 changes: 7 additions & 3 deletions src/azure-cli/azure/cli/command_modules/acr/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
get_resource_name_completion_list,
quotes,
get_three_state_flag,
get_enum_type
get_enum_type,
get_location_completion_list,
get_location_name_type
)
from azure.cli.core.commands.validators import get_default_location_from_resource_group
from .policy import RetentionType
Expand Down Expand Up @@ -75,9 +77,9 @@

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

Expand Down Expand Up @@ -114,6 +116,7 @@ def load_arguments(self, _): # pylint: disable=too-many-statements
help='Default action to apply when no rule matches. Only applicable to Premium SKU.')
c.argument('public_network_enabled', get_three_state_flag(), help="Allow public network access for the container registry.{suffix}".format(suffix=default_allow_suffix))
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))
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.")

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

with self.argument_context('acr login') as c:
c.argument('expose_token', options_list=['--expose-token', '-t'], help='Expose refresh token instead of automatically logging in through Docker CLI', action='store_true')
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.")

with self.argument_context('acr repository') as c:
c.argument('resource_group_name', deprecate_info=c.deprecate(hide=True))
Expand Down
88 changes: 85 additions & 3 deletions src/azure-cli/azure/cli/command_modules/acr/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
DENY_ACTION = 'Deny'
DOMAIN_NAME_LABEL_SCOPE_UNSECURE = 'Unsecure'
DOMAIN_NAME_LABEL_SCOPE_RESOURCE_GROUP_REUSE = 'ResourceGroupReuse'
REGIONAL_ENDPOINTS_NOT_SUPPORTED = "Regional endpoints are only supported for managed registries in Premium SKU."
REGIONAL_ENDPOINTS_NOT_SUPPORTED_FOR_DCT = "Regional endpoints cannot be enabled when Content Trust is enabled. " \
"Please disable Content Trust and try again."


def acr_check_name(cmd, client, registry_name, resource_group_name=None, dnl_scope=DOMAIN_NAME_LABEL_SCOPE_UNSECURE):
Expand Down Expand Up @@ -72,7 +75,8 @@ def acr_create(cmd,
tags=None,
allow_metadata_search=None,
dnl_scope=None,
role_assignment_mode=None):
role_assignment_mode=None,
regional_endpoints=None):
if default_action and sku not in get_premium_sku(cmd):
raise CLIError(NETWORK_RULE_NOT_SUPPORTED)

Expand Down Expand Up @@ -106,6 +110,9 @@ def acr_create(cmd,
if role_assignment_mode is not None:
_configure_role_assignment_mode(cmd, registry, role_assignment_mode)

if regional_endpoints is not None:
_configure_regional_endpoints(cmd, registry, sku, regional_endpoints)

_handle_network_bypass(cmd, registry, allow_trusted_services)
_handle_export_policy(cmd, registry, allow_exports)

Expand Down Expand Up @@ -151,7 +158,8 @@ def acr_update_custom(cmd,
allow_exports=None,
tags=None,
allow_metadata_search=None,
role_assignment_mode=None):
role_assignment_mode=None,
regional_endpoints=None):
if sku is not None:
Sku = cmd.get_models('Sku')
instance.sku = Sku(name=sku)
Expand Down Expand Up @@ -180,6 +188,9 @@ def acr_update_custom(cmd,
if role_assignment_mode is not None:
_configure_role_assignment_mode(cmd, instance, role_assignment_mode)

if regional_endpoints is not None:
_configure_regional_endpoints(cmd, instance, sku, regional_endpoints)

_handle_network_bypass(cmd, instance, allow_trusted_services)
_handle_export_policy(cmd, instance, allow_exports)

Expand Down Expand Up @@ -242,6 +253,27 @@ def acr_update_set(cmd,

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

# Determine the effective SKU (new SKU if being updated, otherwise current SKU)
sku = parameters.sku.name if parameters.sku else registry.sku.name

RegionalEndpoints = cmd.get_models('RegionalEndpoints')
if parameters.regional_endpoints == RegionalEndpoints.ENABLED:
# Regional endpoints require Premium SKU, validate registry tier compatibility
if sku not in get_premium_sku(cmd):
raise CLIError(REGIONAL_ENDPOINTS_NOT_SUPPORTED)

# Regional endpoints are incompatible with Docker Content Trust (DCT), check for conflicts
if registry.policies and registry.policies.trust_policy and registry.policies.trust_policy.status == 'enabled':
raise CLIError(REGIONAL_ENDPOINTS_NOT_SUPPORTED_FOR_DCT)

# Recommend enabling data endpoints for optimal performance when using regional endpoints
if registry.data_endpoint_enabled is False:
logger.warning(
"It is recommended to also enable dedicated data endpoints "
"(--enable-data-endpoint) for optimal in-region performance "
"when using regional endpoints."
)

return client.begin_update(resource_group_name, registry_name, parameters)


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

RegionalEndpoints = cmd.get_models('RegionalEndpoints')
if registry.regional_endpoints == RegionalEndpoints.ENABLED:
info['regionalEndpoints'] = []
for host in registry.regional_endpoint_host_names:
info['regionalEndpoints'].append({
'region': host.split('.')[1],
'endpoint': host,
})

return info


Expand All @@ -286,11 +327,15 @@ def acr_login(cmd,
tenant_suffix=None,
username=None,
password=None,
expose_token=False):
expose_token=False,
endpoint=None):
if expose_token:
if username or password:
raise CLIError("`--expose-token` cannot be combined with `--username` or `--password`.")

if endpoint:
raise CLIError("`--expose-token` cannot be combined with `--endpoint`.")

login_server, _, password = get_login_credentials(
cmd=cmd,
registry_name=registry_name,
Expand Down Expand Up @@ -345,6 +390,34 @@ def acr_login(cmd,
logger.warning('Uppercase characters are detected in the registry name. When using its server url in '
'docker commands, to avoid authentication errors, use all lowercase.')

if endpoint:
registry, _ = get_registry_by_name(cmd.cli_ctx, registry_name, resource_group_name)
matching_endpoint = None

RegionalEndpoints = cmd.get_models('RegionalEndpoints')
if registry.regional_endpoints == RegionalEndpoints.ENABLED and registry.regional_endpoint_host_names:
# Build the expected regional endpoint prefix: registryname.region.geo.
regional_endpoint_prefix = f"{registry_name}.{endpoint}.geo.".lower()
matching_endpoint = next(
(url for url in registry.regional_endpoint_host_names
if url.lower().strip().startswith(regional_endpoint_prefix)), None)

if matching_endpoint:
logger.warning("Logging in to regional endpoint: %s", matching_endpoint)
_perform_registry_login(matching_endpoint, docker_command, username, password)
else:
raise CLIError(
"Regional endpoint for '{}' not found. Aborting login. "
"Run 'az acr show-endpoints -n {}' to list available regional endpoints.".format(
endpoint, registry_name)
)
else:
_perform_registry_login(login_server, docker_command, username, password)

return None


def _perform_registry_login(login_server, docker_command, username, password):
from subprocess import PIPE, Popen
logger.debug("Invoking '%s login --username %s --password <redacted> %s'",
docker_command, username, login_server)
Expand Down Expand Up @@ -687,3 +760,12 @@ def _configure_role_assignment_mode(cmd, registry, role_assignment_mode):
"'--source-registry-auth-id' flag in 'az acr task update'. Please refer to "
"https://aka.ms/acr/auth/abac for more details.")
registry.role_assignment_mode = mode


def _configure_regional_endpoints(cmd, registry, sku, regional_endpoints):
RegionalEndpoints = cmd.get_models('RegionalEndpoints')

if regional_endpoints == RegionalEndpoints.ENABLED and sku and sku not in get_premium_sku(cmd):
raise CLIError(REGIONAL_ENDPOINTS_NOT_SUPPORTED)

registry.regional_endpoints = regional_endpoints
40 changes: 34 additions & 6 deletions src/azure-cli/azure/cli/command_modules/acr/import.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def acr_import(cmd, # pylint: disable=too-many-locals
if is_valid_resource_id(source_registry):
source = ImportSource(resource_id=source_registry, source_image=source_image)
else:
registry = get_registry_from_name_or_login_server(cmd.cli_ctx, source_registry, source_registry)
registry = _get_azure_registry(cmd, source_registry)
if registry:
# trim away redundant login server name, a common error
prefix = registry.login_server + '/'
Expand All @@ -78,10 +78,7 @@ def acr_import(cmd, # pylint: disable=too-many-locals
credentials=ImportSourceCredentials(password=source_registry_password,
username=source_registry_username))
else:
# Try to get the pre-defined login server suffix.
login_server_suffix = get_login_server_suffix(cmd.cli_ctx)
if not login_server_suffix or registry_uri.endswith(login_server_suffix):
registry = get_registry_from_name_or_login_server(cmd.cli_ctx, registry_uri)
registry = _get_azure_registry(cmd, registry_uri)
if registry:
# For Azure container registry
source = ImportSource(resource_id=registry.id, source_image=source_image)
Expand Down Expand Up @@ -117,6 +114,37 @@ def acr_import(cmd, # pylint: disable=too-many-locals
_handle_import_exception(e, cmd, source_registry, source_image, registry)


def _regional_endpoint_uri_to_login_server(uri, login_server_suffix):
"""Convert regional endpoint URI to standard login server URI.

Example: testregistry.eastus.geo.azurecr.io -> testregistry.azurecr.io
"""
if not uri or not login_server_suffix:
return uri

parts = uri.split('.')

if len(parts) == 5 and parts[2] == 'geo' and uri.endswith(login_server_suffix):
return f"{parts[0]}{login_server_suffix}"

# If not a regional endpoint format, return as-is
return uri


def _get_azure_registry(cmd, source_registry):
"""Get Azure registry from login server URI or registry name, handling regional endpoint URI."""
lookup_uri = source_registry

# Try to get the pre-defined login server suffix.
login_server_suffix = get_login_server_suffix(cmd.cli_ctx)
# Convert regional endpoint to standard format if applicable
if login_server_suffix and source_registry.endswith(f".geo{login_server_suffix}"):
lookup_uri = _regional_endpoint_uri_to_login_server(source_registry, login_server_suffix)

# Search by login server (lookup_uri) and registry name (source_registry) to handle both URI and name inputs
return get_registry_from_name_or_login_server(cmd.cli_ctx, lookup_uri, source_registry)
Comment on lines +125 to +145
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regional-endpoint detection/conversion is case-sensitive (endswith + parts[2] == 'geo'). If users pass an uppercase/mixed-case login server (common for hostnames), conversion will be skipped and the registry lookup will fail because the regional endpoint hostname won't match any registry loginServer. Consider normalizing source_registry/uri via .strip().lower() for the .endswith() checks and parts[...] comparisons, while still returning the original-cased input when no conversion applies.

Copilot uses AI. Check for mistakes.


def _handle_import_exception(e, cmd, source_registry, source_image, registry):
from msrest.exceptions import ClientException
try:
Expand All @@ -127,7 +155,7 @@ def _handle_import_exception(e, cmd, source_registry, source_image, registry):
if is_valid_resource_id(source_registry):
registry, _ = get_registry_by_name(cmd.cli_ctx, parse_resource_id(source_registry)["name"])
else:
registry = get_registry_from_name_or_login_server(cmd.cli_ctx, source_registry, source_registry)
registry = _get_azure_registry(cmd, source_registry)

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