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
38 changes: 36 additions & 2 deletions src/azure-cli/azure/cli/command_modules/acr/_client_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@

from azure.cli.core.commands.client_factory import get_mgmt_service_client

REGISTRY_TENANT_USAGE_ERROR = \
"--registry-tenant can only be used when --registry is a container registry resource ID."
REGISTRY_TENANT_AUTH_ERROR = \
"--registry-tenant is not supported when signed in with managed identity or Cloud Shell. " \
"Sign in with a user or service principal that has access to both tenants."

def get_acr_service_client(cli_ctx, api_version=None):

def get_acr_service_client(cli_ctx, api_version=None, aux_tenants=None):
"""Returns the client for managing container registries. """
from azure.cli.core.profiles import ResourceType
return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_CONTAINERREGISTRY, api_version=api_version)
return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_CONTAINERREGISTRY, api_version=api_version,
aux_tenants=aux_tenants)


def get_acr_tasks_service_client(cli_ctx, api_version=None):
Expand All @@ -23,6 +30,33 @@ def cf_acr_registries(cli_ctx, *_):
return get_acr_service_client(cli_ctx).registries


def cf_acr_import(cli_ctx, command_args):
from azure.cli.core.azclierror import ArgumentUsageError, ValidationError
from azure.cli.core._profile import Profile
from azure.mgmt.core.tools import is_valid_resource_id, parse_resource_id

registry_tenant = command_args.pop('registry_tenant', None)
source_registry = command_args.get('source_registry')

if registry_tenant:
if not is_valid_resource_id(source_registry):
raise ArgumentUsageError(REGISTRY_TENANT_USAGE_ERROR)

resource_id = parse_resource_id(source_registry)
if (resource_id.get('namespace', '').lower() != 'microsoft.containerregistry' or
resource_id.get('type', '').lower() != 'registries'):
raise ArgumentUsageError(REGISTRY_TENANT_USAGE_ERROR)

profile = Profile(cli_ctx=cli_ctx)
account = profile.get_subscription()
user = account.get('user', {})
if user.get('name') in ('systemAssignedIdentity', 'userAssignedIdentity') or user.get('cloudShellID'):
raise ValidationError(REGISTRY_TENANT_AUTH_ERROR)

aux_tenants = [registry_tenant] if registry_tenant else None
return get_acr_service_client(cli_ctx, aux_tenants=aux_tenants).registries

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in 4136ed7. --registry-tenant now rejects managed identity and Cloud Shell login modes before client creation, with focused unit coverage.



def cf_acr_cache(cli_ctx, *_):
return get_acr_service_client(cli_ctx).cache_rules

Expand Down
5 changes: 5 additions & 0 deletions src/azure-cli/azure/cli/command_modules/acr/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,11 @@
text: |
az acr import -n myregistry --source sourcerepository:sourcetag -t targetrepository:targettag \\
-r /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/sourceResourceGroup/providers/Microsoft.ContainerRegistry/registries/sourceRegistry
- name: Import an image from an Azure container registry in a different tenant.
text: |
az acr import -n myregistry --source sourcerepository:sourcetag -t targetrepository:targettag \\
-r /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/sourceResourceGroup/providers/Microsoft.ContainerRegistry/registries/sourceRegistry \\
--registry-tenant 00000000-0000-0000-0000-000000000000
- 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
Expand Down
3 changes: 3 additions & 0 deletions src/azure-cli/azure/cli/command_modules/acr/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ def load_arguments(self, _): # pylint: disable=too-many-statements
with self.argument_context('acr import') as c:
c.argument('source_image', options_list=['--source'], help="Source image name or fully qualified source containing the registry login server. If `--registry` is used, `--source` will always be interpreted as a source image, even if it contains the login server.")
c.argument('source_registry', options_list=['--registry', '-r'], help='The source Azure container registry. This can be name, login server or resource ID of the source registry.')
c.argument('registry_tenant', options_list=['--registry-tenant'],
help='The tenant ID of the source Azure container registry. '
'Only supported when --registry is a container registry resource ID.')
c.argument('source_registry_username', options_list=['--username', '-u'], help='The username of source container registry')
c.argument('source_registry_password', options_list=['--password', '-p'], help='The password of source container registry')
c.argument('target_tags', options_list=['--image', '-t'], help="The name and tag of the image using the format: '-t repo/image:tag'. Multiple tags are supported by passing -t multiple times.", action='append')
Expand Down
3 changes: 2 additions & 1 deletion src/azure-cli/azure/cli/command_modules/acr/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
)
from ._client_factory import (
cf_acr_registries,
cf_acr_import,
cf_acr_replications,
cf_acr_webhooks,
cf_acr_tasks,
Expand Down Expand Up @@ -69,7 +70,7 @@ def load_command_table(self, _):

acr_import_util = CliCommandType(
operations_tmpl='azure.cli.command_modules.acr.import#{}',
client_factory=cf_acr_registries
client_factory=cf_acr_import
)

acr_policy_util = CliCommandType(
Expand Down
1 change: 1 addition & 0 deletions src/azure-cli/azure/cli/command_modules/acr/import.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def acr_import(cmd, # pylint: disable=too-many-locals
resource_group_name=None,
repository=None,
force=False,
registry_tenant=None, # pylint: disable=unused-argument
no_wait=False):

if source_registry_username and not source_registry_password:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import unittest
from unittest import mock

from azure.cli.core.azclierror import ArgumentUsageError, ValidationError
from azure.cli.command_modules.acr import _client_factory


class AcrClientFactoryTests(unittest.TestCase):

@mock.patch.object(_client_factory, 'get_mgmt_service_client')
def test_acr_service_client_uses_auxiliary_tenants(self, get_mgmt_service_client):
_client_factory.get_acr_service_client(
mock.sentinel.cli_ctx, aux_tenants=['source-tenant'])

_, kwargs = get_mgmt_service_client.call_args
self.assertEqual(kwargs['aux_tenants'], ['source-tenant'])

@mock.patch.object(_client_factory, 'get_acr_service_client')
@mock.patch('azure.cli.core._profile.Profile')
def test_import_client_uses_registry_tenant(self, profile, get_acr_service_client):
registries = mock.sentinel.registries
get_acr_service_client.return_value.registries = registries
profile.return_value.get_subscription.return_value = {'user': {'type': 'user'}}
command_args = {
'source_registry': (
'/subscriptions/source-subscription/resourceGroups/source-rg/providers/'
'Microsoft.ContainerRegistry/registries/source-registry'),
'registry_tenant': 'source-tenant'
}

result = _client_factory.cf_acr_import(mock.sentinel.cli_ctx, command_args)

self.assertIs(result, registries)
get_acr_service_client.assert_called_once_with(
mock.sentinel.cli_ctx, aux_tenants=['source-tenant'])
self.assertNotIn('registry_tenant', command_args)

@mock.patch.object(_client_factory, 'get_acr_service_client')
def test_import_client_rejects_registry_name_with_registry_tenant(self, get_acr_service_client):
command_args = {
'source_registry': 'source-registry',
'registry_tenant': 'source-tenant'
}

with self.assertRaisesRegex(
ArgumentUsageError,
'--registry-tenant can only be used when --registry is a container registry resource ID'):
_client_factory.cf_acr_import(mock.sentinel.cli_ctx, command_args)

get_acr_service_client.assert_not_called()

@mock.patch.object(_client_factory, 'get_acr_service_client')
def test_import_client_rejects_missing_registry_with_registry_tenant(self, get_acr_service_client):
command_args = {'registry_tenant': 'source-tenant'}

with self.assertRaisesRegex(
ArgumentUsageError,
'--registry-tenant can only be used when --registry is a container registry resource ID'):
_client_factory.cf_acr_import(mock.sentinel.cli_ctx, command_args)

get_acr_service_client.assert_not_called()

@mock.patch.object(_client_factory, 'get_acr_service_client')
def test_import_client_rejects_non_registry_resource_id(self, get_acr_service_client):
command_args = {
'source_registry': (
'/subscriptions/source-subscription/resourceGroups/source-rg/providers/'
'Microsoft.Storage/storageAccounts/source-storage'),
'registry_tenant': 'source-tenant'
}

with self.assertRaisesRegex(
ArgumentUsageError,
'--registry-tenant can only be used when --registry is a container registry resource ID'):
_client_factory.cf_acr_import(mock.sentinel.cli_ctx, command_args)

get_acr_service_client.assert_not_called()

@mock.patch.object(_client_factory, 'get_acr_service_client')
@mock.patch('azure.cli.core._profile.Profile')
def test_import_client_rejects_managed_identity(self, profile, get_acr_service_client):
profile.return_value.get_subscription.return_value = {
'user': {'name': 'systemAssignedIdentity'}
}
command_args = {
'source_registry': (
'/subscriptions/source-subscription/resourceGroups/source-rg/providers/'
'Microsoft.ContainerRegistry/registries/source-registry'),
'registry_tenant': 'source-tenant'
}

with self.assertRaisesRegex(
ValidationError,
'--registry-tenant is not supported when signed in with managed identity'):
_client_factory.cf_acr_import(mock.sentinel.cli_ctx, command_args)

get_acr_service_client.assert_not_called()

@mock.patch.object(_client_factory, 'get_acr_service_client')
@mock.patch('azure.cli.core._profile.Profile')
def test_import_client_rejects_cloud_shell(self, profile, get_acr_service_client):
profile.return_value.get_subscription.return_value = {'user': {'cloudShellID': True}}
command_args = {
'source_registry': (
'/subscriptions/source-subscription/resourceGroups/source-rg/providers/'
'Microsoft.ContainerRegistry/registries/source-registry'),
'registry_tenant': 'source-tenant'
}

with self.assertRaisesRegex(
ValidationError,
'--registry-tenant is not supported when signed in with managed identity or Cloud Shell'):
_client_factory.cf_acr_import(mock.sentinel.cli_ctx, command_args)

get_acr_service_client.assert_not_called()

@mock.patch.object(_client_factory, 'get_acr_service_client')
def test_import_client_without_registry_tenant(self, get_acr_service_client):
registries = mock.sentinel.registries
get_acr_service_client.return_value.registries = registries
command_args = {'source_registry': 'source-registry'}

result = _client_factory.cf_acr_import(mock.sentinel.cli_ctx, command_args)

self.assertIs(result, registries)
get_acr_service_client.assert_called_once_with(mock.sentinel.cli_ctx, aux_tenants=None)


if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,12 @@ def test_acr_import_no_wait(self, resource_group):
# import image using no-wait
self.cmd('acr import -n {source_registry_name} -r {resource_id} --source {source_image} --no-wait')

# --registry-tenant requires --registry to be a source registry resource ID
self.cmd(
'acr import -n {source_registry_name} -r {source_registry_name} --source {source_image} '
'--registry-tenant 00000000-0000-0000-0000-000000000000',
expect_failure=True)

@AllowLargeResponse()
@ResourceGroupPreparer()
@record_only()
Expand Down Expand Up @@ -1138,4 +1144,3 @@ def test_acr_create_with_domain_name_label_scope_name_in_use(self, resource_grou
with self.assertRaises(Exception) as ex:
self.cmd('acr create -n {registry_name} -g {rg2} -l {rg_loc} --sku {sku} --dnl-scope resourcegroupreuse',
checks=['code','RegistryNameAlreadyInUse'])

Loading