Skip to content

Commit dbe2440

Browse files
Natalietroydai
authored andcommitted
Add ACR Image Import Commands (#6375)
1 parent 6ffea89 commit dbe2440

8 files changed

Lines changed: 1192 additions & 0 deletions

File tree

src/command_modules/azure-cli-acr/HISTORY.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Release History
55
2.0.26
66
++++++
77
* Allow VSTS as a remote source location
8+
* Add 'acr import' command.
89

910
2.0.25
1011
++++++

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,3 +429,15 @@
429429
text: >
430430
az acr build -r MyRegistry .
431431
"""
432+
433+
helps['acr import'] = """
434+
type: command
435+
short-summary: Imports an image to the container registry from source.
436+
examples:
437+
- name: Import an image to the target registry and inherits sourcerepository:sourcetag from source.
438+
text: >
439+
az acr import -n MyRegistry --source sourceregistry.azurecr.io/sourcerepository:sourcetag
440+
- name: Import an image from a registry in a different subscription.
441+
text: >
442+
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
443+
"""

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ def load_arguments(self, _): # pylint: disable=too-many-statements
6262
c.argument('no_push', help="Indicates whether the image built should be pushed to the registry.", arg_type=get_three_state_flag())
6363
c.argument('no_logs', help="Do not show logs after successfully queuing the build.", action='store_true')
6464

65+
with self.argument_context('acr import') as c:
66+
c.argument('source', help="The source identifier in the format '[registry.azurecr.io/]repository[:tag]' or '[registry.azurecr.io/]repository@digest'.")
67+
c.argument('source_registry', options_list=['--registry', '-r'], help='The source container registry can be name, login server or resource ID of the source registry.')
68+
c.argument('target_tags', options_list=['--image', '-t'], help="The repository and optionally a tag in the 'repository:tag' format for target images.", action='append')
69+
c.argument('repository', help='The repository name to do a manifest-only copy for images.', action='append')
70+
c.argument('force', help='Overwrite the existing tag of the image to be imported.', action='store_true')
71+
6572
with self.argument_context('acr repository delete') as c:
6673
c.argument('manifest', nargs='?', required=False, const='', default=None, help=argparse.SUPPRESS)
6774
c.argument('yes', options_list=['--yes', '-y'], action='store_true', help='Do not prompt for confirmation')

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# --------------------------------------------------------------------------------------------
55

66
from knack.util import CLIError
7+
from knack.log import get_logger
78
from azure.cli.core.commands.parameters import get_resources_in_subscription
89

910
from azure.mgmt.containerregistry.v2017_10_01.models import SkuName, Sku
@@ -21,6 +22,8 @@
2122
get_acr_service_client
2223
)
2324

25+
logger = get_logger(__name__)
26+
2427

2528
def _arm_get_resource_by_name(cli_ctx, resource_name, resource_type):
2629
"""Returns the ARM resource in the current subscription with resource_name.
@@ -91,6 +94,27 @@ def get_registry_by_name(cli_ctx, registry_name, resource_group_name=None):
9194
return client.get(resource_group_name, registry_name), resource_group_name
9295

9396

97+
def get_registry_from_name_or_login_server(cli_ctx, login_server, registry_name=None):
98+
"""Returns a Registry object for the specified name.
99+
:param str name: either the registry name or the login server of the registry.
100+
"""
101+
client = get_acr_service_client(cli_ctx).registries
102+
registry_list = client.list()
103+
104+
if registry_name:
105+
elements = [item for item in registry_list if
106+
item.login_server.lower() == login_server.lower() or item.name.lower() == registry_name.lower()]
107+
else:
108+
elements = [item for item in registry_list if
109+
item.login_server.lower() == login_server.lower()]
110+
111+
if len(elements) == 1:
112+
return elements[0]
113+
elif len(elements) > 1:
114+
logger.warning("More than one registries were found by %s.", login_server)
115+
return None
116+
117+
94118
def arm_deploy_template_new_storage(cli_ctx,
95119
resource_group_name,
96120
registry_name,

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ def load_command_table(self, _): # pylint: disable=too-many-statements
3636
client_factory=cf_acr_registries
3737
)
3838

39+
acr_import_util = CliCommandType(
40+
operations_tmpl='azure.cli.command_modules.acr.import#{}',
41+
client_factory=cf_acr_registries
42+
)
43+
3944
acr_cred_util = CliCommandType(
4045
operations_tmpl='azure.cli.command_modules.acr.credential#{}',
4146
table_transformer=credential_output_format,
@@ -86,6 +91,9 @@ def load_command_table(self, _): # pylint: disable=too-many-statements
8691
client_factory=cf_acr_registries,
8792
table_transformer=registry_output_format)
8893

94+
with self.command_group('acr', acr_import_util) as g:
95+
g.command('import', 'acr_import')
96+
8997
with self.command_group('acr credential', acr_cred_util) as g:
9098
g.command('show', 'acr_credential_show', exception_handler=empty_on_404)
9199
g.command('renew', 'acr_credential_renew')
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
from knack.util import CLIError
7+
from msrestazure.tools import is_valid_resource_id
8+
from azure.mgmt.containerregistry.v2018_02_01_preview.models import (
9+
ImportImageParameters,
10+
ImportSource,
11+
ImportMode
12+
)
13+
from ._utils import (
14+
validate_managed_registry,
15+
get_registry_from_name_or_login_server
16+
)
17+
18+
SOURCE_REGISTRY_MISSING = "Please specify the source container registry name, login server or resource ID: "
19+
IMPORT_NOT_SUPPORTED = "Imports are only supported for managed registries."
20+
INVALID_SOURCE_IMAGE = "Please specify source image in the format '[registry.azurecr.io/]repository[:tag]' or " \
21+
"'[registry.azurecr.io/]repository@digest'"
22+
SOURCE_REGISTRY_NOT_FOUND = "Source registry could not be found in the current subscription. " \
23+
"Please specify the full resource ID for it: "
24+
NO_TTY_ERROR = "Please specify source registry ID by passing parameters to import command directly."
25+
REGISTRY_MISMATCH = "Registry mismatch. Please check either source-image or resource ID " \
26+
"to make sure that they are referring to the same registry and try again."
27+
28+
29+
def acr_import(cmd,
30+
client,
31+
registry_name,
32+
source,
33+
source_registry=None,
34+
target_tags=None,
35+
resource_group_name=None,
36+
repository=None,
37+
force=False):
38+
_, resource_group_name = validate_managed_registry(
39+
cmd.cli_ctx, registry_name, resource_group_name, IMPORT_NOT_SUPPORTED)
40+
41+
if not source:
42+
raise CLIError(INVALID_SOURCE_IMAGE)
43+
source_image = source
44+
45+
slash = source.find('/')
46+
47+
if slash < 0:
48+
if not source_registry:
49+
from knack.prompting import prompt, NoTTYException
50+
try:
51+
source_registry = prompt(SOURCE_REGISTRY_MISSING)
52+
except NoTTYException:
53+
raise CLIError(NO_TTY_ERROR)
54+
if not is_valid_resource_id(source_registry):
55+
registry = get_registry_from_name_or_login_server(cmd.cli_ctx, source_registry, source_registry)
56+
if registry:
57+
source_registry = registry.id
58+
else:
59+
source_image = source[slash + 1:]
60+
source_registry_login_server = source[:slash]
61+
if not source_image or not source_registry_login_server:
62+
raise CLIError(INVALID_SOURCE_IMAGE)
63+
registry = get_registry_from_name_or_login_server(cmd.cli_ctx, source_registry_login_server)
64+
if registry:
65+
if source_registry and \
66+
source_registry.lower() != registry.id.lower() and \
67+
source_registry.lower() != registry.name.lower() and \
68+
source_registry.lower() != registry.login_server.lower():
69+
raise CLIError(REGISTRY_MISMATCH)
70+
source_registry = registry.id
71+
72+
if not is_valid_resource_id(source_registry):
73+
from knack.prompting import prompt, NoTTYException
74+
try:
75+
source_registry = prompt(SOURCE_REGISTRY_NOT_FOUND)
76+
except NoTTYException:
77+
raise CLIError(NO_TTY_ERROR)
78+
79+
image_source = ImportSource(resource_id=source_registry, source_image=source_image)
80+
81+
if not target_tags and not repository:
82+
target_tags = [source_image]
83+
84+
import_parameters = ImportImageParameters(source=image_source,
85+
target_tags=target_tags,
86+
untagged_target_repositories=repository,
87+
mode=ImportMode.force.value if force else ImportMode.no_force.value)
88+
89+
return client.import_image(
90+
resource_group_name=resource_group_name,
91+
registry_name=registry_name,
92+
parameters=import_parameters)

src/command_modules/azure-cli-acr/azure/cli/command_modules/acr/tests/latest/recordings/test_acr_image_import.yaml

Lines changed: 985 additions & 0 deletions
Large diffs are not rendered by default.

src/command_modules/azure-cli-acr/azure/cli/command_modules/acr/tests/latest/test_acr_commands.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,3 +400,66 @@ def test_acr_create_build_task(self, resource_group, resource_group_location):
400400

401401
# test acr delete
402402
self.cmd('acr delete -n {registry_name} -g {rg}')
403+
404+
@ResourceGroupPreparer()
405+
def test_acr_image_import(self, resource_group):
406+
'''There are six test cases in the function.
407+
Case 1: Import image from a regsitry in a different subscription from the current one
408+
Case 2: Import image from one regsitry to another where both registries belong to the same subscription
409+
Case 3: Import image to the target regsitry and keep the repository:tag the same as that in the source
410+
Case 4: Import image to enable multiple tags in the target registry
411+
Case 5: Import image within the same registry
412+
Case 6: Import image by manifest digest
413+
'''
414+
415+
registry_name = self.create_random_name("targetregsitry", 20)
416+
417+
'''
418+
To be able to run the tests, we are assuming the following resources before the test:
419+
Current active cloud account and subscription.
420+
Two source registries, one in the subscription other than the current one and another in the subscription the same as the current one.
421+
Two source images each of which stays in a different source registries mentioned above.
422+
'''
423+
self.kwargs.update({
424+
'registry_name': registry_name,
425+
'rg_loc': 'eastus',
426+
'sku': 'Standard',
427+
'resource_id': '/subscriptions/a7ee80a4-3d5e-45c2-9378-36e8d98f4d13/resourceGroups/resourcegroupdiffsub/providers/Microsoft.ContainerRegistry/registries/sourceregistrydiffsub',
428+
'source_image_diff_sub': 'builder:latest',
429+
'source_image_same_sub': 'sourceregistrysamesub.azurecr.io/builder:latest',
430+
'source_image_same_registry': '{}.azurecr.io/builder:latest'.format(registry_name),
431+
'source_image_by_digest': 'sourceregistrysamesub.azurecr.io/builder@sha256:bc3842ba36fcc182317c07a8643daa4a8e4e7aed45958b1f7e2a2b30c2f5a64f',
432+
'tag_diff_sub': 'repository_diff_sub:tag_diff_sub',
433+
'tag_same_sub': 'repository_same_sub:tag_same_sub',
434+
'tag_multitag1': 'repository_multi1:tag_multi1',
435+
'tag_multitag2': 'repository_multi2:tag_multi2',
436+
'tag_same_registry': 'repository_same_registry:tag_same_registry',
437+
'tag_by_digest': 'repository_by_digest:tag_by_digest'
438+
})
439+
440+
# create a target registry to hold the imported images
441+
self.cmd('acr create -n {registry_name} -g {rg} -l {rg_loc} --sku {sku}',
442+
checks=[self.check('name', '{registry_name}'),
443+
self.check('location', '{rg_loc}'),
444+
self.check('adminUserEnabled', False),
445+
self.check('sku.name', 'Standard'),
446+
self.check('sku.tier', 'Standard'),
447+
self.check('provisioningState', 'Succeeded')])
448+
449+
# Case 1: Import image from a regsitry in a different subscription from the current one
450+
self.cmd('acr import -n {registry_name} -r {resource_id} --source {source_image_diff_sub} -t {tag_diff_sub}')
451+
452+
# Case 2: Import image from one regsitry to another where both registries belong to the same subscription
453+
self.cmd('acr import -n {registry_name} --source {source_image_same_sub} -t {tag_same_sub}')
454+
455+
# Case 3: Import image to the target regsitry and keep the repository:tag the same as that in the source
456+
self.cmd('acr import -n {registry_name} --source {source_image_same_sub}')
457+
458+
# Case 4: Import image to enable multiple tags in the target registry
459+
self.cmd('acr import -n {registry_name} --source {source_image_same_sub} -t {tag_multitag1} -t {tag_multitag2}')
460+
461+
# Case 5: Import image within the same registry
462+
self.cmd('acr import -n {registry_name} --source {source_image_same_registry} -t {tag_same_registry}')
463+
464+
# Case 6: Import image by manifest digest
465+
self.cmd('acr import -n {registry_name} --source {source_image_by_digest} -t {tag_by_digest}')

0 commit comments

Comments
 (0)