Skip to content
Merged
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
15 changes: 15 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 @@ -1327,6 +1327,21 @@
text: >
az acr connected-registry install renew-credentials -r mycloudregistry -n myconnectedregistry
"""

helps['acr connected-registry repo'] = """
type: command
short-summary: Updates all the necessary connected registry sync scope maps repository permissions.
examples:
- name: Adds permissions to synchronize images from 'repo1' and 'repo2' to the connected registry 'myconnectedregistry' and its ancestors.
text: >
az acr connected-registry repo -r mycloudregistry -n myconnectedregistry --add repo1 repo2
- name: Removes permissions to synchronize images from 'repo1' and 'repo2' to the connected registry 'myconnectedregistry' and its descendants.
text: >
az acr connected-registry repo -r mycloudregistry -n myconnectedregistry --remove repo1 repo2
- name: Removes permissions to synchronize 'repo1' images and adds permissions for 'repo2' images.
text: >
az acr connected-registry repo -r mycloudregistry -n myconnectedregistry --remove repo1 --add repo2
"""
# endregion

# region private-endpoint-connection
Expand Down
8 changes: 7 additions & 1 deletion src/azure-cli/azure/cli/command_modules/acr/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ def load_arguments(self, _): # pylint: disable=too-many-statements

with self.argument_context('acr connected-registry create') as c:
c.argument('log_level', help='Sets the log level for logging on the instance. Accepted log levels are Debug, Information, Warning, Error, and None.', required=False, default="Information")
c.argument('mode', options_list=['--mode', '-m'], help='Can be one of the two operating modes: registry or mirror(pull-only mode).', required=False, default="registry")
c.argument('mode', options_list=['--mode', '-m'], help='Can be one of the two operating modes: registry or mirror(pull-only mode).', required=False, default="Registry")
c.argument('client_token_list', options_list=['--client-tokens'], nargs='+', help='Specifies the client access to the repositories in the connected registry. It can be in the format [TOKEN_NAME01] [TOKEN_NAME02]...', required=False)
c.argument('sync_window', options_list=['--sync-window', '-w'], help='Required parameter if --sync-schedule is present. Used to determine the schedule duration. Uses ISO 8601 duration format.', required=False)
c.argument('sync_schedule', options_list=['--sync-schedule', '-s'], help='Optional parameter to define the sync schedule. Uses cron expression to determine the schedule. If not specified, the instance is considered always online and attempts to sync every minute.', required=False, default="* * * * *")
Expand All @@ -423,6 +423,12 @@ def load_arguments(self, _): # pylint: disable=too-many-statements
c.argument('sync_schedule', options_list=['--sync-schedule', '-s'], help='Optional parameter to define the sync schedule. Uses cron expression to determine the schedule. If not specified, the instance is considered always online and attempts to sync every minute.', required=False)
c.argument('sync_message_ttl', help='Determines how long the sync messages will be kept in the cloud. Uses ISO 8601 duration format.', required=False)

with self.argument_context('acr connected-registry repo') as c:
c.argument('add_repos', options_list=['--add'], nargs='*', required=False,
help='repository permissions to be added to the targeted connected registry and it\'s ancestors sync scope maps. Use the format "--add [REPO1 REPO2 ...]" per flag. ' + repo_valid_actions)
c.argument('remove_repos', options_list=['--remove'], nargs='*', required=False,
help='respsitory permissions to be removed from the targeted connected registry and it\'s succesors sync scope maps. Use the format "--remove [REPO1 REPO2 ...]" per flag. ' + repo_valid_actions)


def _get_helm_default_install_location():
exe_name = 'helm'
Expand Down
4 changes: 2 additions & 2 deletions src/azure-cli/azure/cli/command_modules/acr/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ def parse_repositories_from_actions(actions):
return list(set(repositories))


def parse_scope_map_actions(repository_actions_list, gateway_actions_list):
def parse_scope_map_actions(repository_actions_list=None, gateway_actions_list=None):
from .scope_map import RepoScopeMapActions, GatewayScopeMapActions
valid_actions = {action.value for action in RepoScopeMapActions}
actions = _parse_scope_map_actions(repository_actions_list, valid_actions, 'repositories')
Expand All @@ -486,7 +486,7 @@ def _parse_scope_map_actions(actions_list, valid_actions, action_prefix):
return []
actions = []
for rule in actions_list:
resource = rule[0]
resource = rule[0].lower()
if len(rule) < 2:
raise CLIError('At least one action must be specified with "{}".'.format(resource))
for action in rule[1:]:
Expand Down
1 change: 1 addition & 0 deletions src/azure-cli/azure/cli/command_modules/acr/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ def _helm_deprecate_message(self):
g.show_command('show', 'acr_connected_registry_show')
g.command('deactivate', 'acr_connected_registry_deactivate')
g.command('update', 'acr_connected_registry_update')
g.command('repo', 'acr_connected_registry_repo')
g.command('install info', 'acr_connected_registry_install_info')
g.command('install renew-credentials', 'acr_connected_registry_install_renew_credentials')
g.command('list', 'acr_connected_registry_list',
Expand Down
232 changes: 203 additions & 29 deletions src/azure-cli/azure/cli/command_modules/acr/connected_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,25 @@
from azure.cli.core.commands.client_factory import get_subscription_id
from ._client_factory import cf_acr_tokens, cf_acr_scope_maps
from ._utils import (
get_registry_by_name,
validate_managed_registry,
user_confirmation,
build_token_id,
create_default_scope_map,
get_registry_by_name,
get_scope_map_from_id,
get_token_from_id,
build_token_id
parse_scope_map_actions,
user_confirmation,
validate_managed_registry
)


class ConnectedRegistryModes(Enum):
MIRROR = 'mirror'
REGISTRY = 'registry'
MIRROR = 'Mirror'
REGISTRY = 'Registry'


class ConnectedRegistryActivationStatus(Enum):
ACTIVE = 'Active'
INACTIVE = 'Inactive'


DEFAULT_GATEWAY_SCOPE = ['config/read', 'config/write', 'message/read', 'message/write']
Expand Down Expand Up @@ -68,18 +75,28 @@ def acr_connected_registry_create(cmd, # pylint: disable=too-many-locals, too-m

ErrorResponseException = cmd.get_models('ErrorResponseException')
parent = None
mode = mode.capitalize()
if parent_name:
try:
parent = acr_connected_registry_show(cmd, client, parent_name, registry_name, resource_group_name)
connected_registry_list = list(client.list(resource_group_name, registry_name))
family_tree, _ = _get_family_tree(connected_registry_list, None)
except ErrorResponseException as ex:
if ex.response.status_code == 404:
raise CLIError("The parent connected registry '{}' could not be found.".format(parent_name))
raise CLIError(ex)
if parent.mode.lower() != ConnectedRegistryModes.REGISTRY.value and parent.mode.lower() != mode.lower():
if parent.mode != ConnectedRegistryModes.REGISTRY.value and parent.mode != mode:
raise CLIError("Can't create the registry '{}' with mode '{}' ".format(connected_registry_name, mode) +
"when the connected registry parent '{}' mode is '{}'. ".format(parent_name, parent.mode) +
"For more information on connected registries " +
"please visit https://aka.ms/acr/connected-registry.")
msg = "Can't create the registry '{}'. The ancestor connected ".format(connected_registry_name) +\
"registry activation status is not '{}'. ".format(ConnectedRegistryActivationStatus.ACTIVE.value) +\
"Please install the parent connected registry and try again. For more information on connected " +\
"registries, please visit https://aka.ms/acr/connected-registry."
_check_ancestors_are_active(family_tree, parent.id, msg)
_update_ancestor_permissions(cmd, family_tree, resource_group_name, registry_name, parent.id,
connected_registry_name, repositories, mode, False)

if sync_token_name:
sync_token_id = build_token_id(subscription_id, resource_group_name, registry_name, sync_token_name)
Expand Down Expand Up @@ -202,7 +219,6 @@ def acr_connected_registry_delete(cmd,
cleanup=False,
yes=False,
resource_group_name=None):

_, resource_group_name = validate_managed_registry(
cmd, registry_name, resource_group_name)
user_confirmation("Are you sure you want to delete the connected registry '{}' in '{}'?".format(
Expand All @@ -220,8 +236,14 @@ def acr_connected_registry_delete(cmd,
token_client = cf_acr_tokens(cmd.cli_ctx)
scope_map_client = cf_acr_scope_maps(cmd.cli_ctx)

# Delete target sync scope map and token.
acr_token_delete(cmd, token_client, registry_name, sync_token_name, yes, resource_group_name)
acr_scope_map_delete(cmd, scope_map_client, registry_name, sync_scope_map_name, yes, resource_group_name)
# Cleanup gateway permissions from ancestors
connected_registry_list = list(client.list(resource_group_name, registry_name))
family_tree, _ = _get_family_tree(connected_registry_list, None)
_update_ancestor_permissions(cmd, family_tree, resource_group_name, registry_name,
connected_registry.parent.id, connected_registry_name, remove_access=True)
else:
msg = "Connected registry successfully deleted. Please cleanup your sync tokens and scope maps. " + \
"Run the following commands for cleanup: \n\t" + \
Expand Down Expand Up @@ -271,19 +293,10 @@ def acr_connected_registry_list(cmd,
else:
result = [registry for registry in connected_registry_list if not registry.parent.id]
elif parent_name:
family_tree = {}
for registry in connected_registry_list:
family_tree[registry.id] = {
"registry": registry,
"childs": []
}
if registry.name == parent_name:
root_parent_id = registry.id
for registry in connected_registry_list:
parent_id = registry.parent.id
if parent_id and not parent_id.isspace():
family_tree[parent_id]["childs"].append(registry.id)
result = _get_descendancy(family_tree, root_parent_id)
family_tree, parent = _get_family_tree(connected_registry_list, parent_name)
if parent is None:
raise CLIError("Parent connected registry '{}' doesn't exist.".format(parent_name))
result = _get_descendants(family_tree, parent.id)
else:
result = connected_registry_list
return result
Expand Down Expand Up @@ -327,7 +340,7 @@ def _create_sync_token(cmd,
mode):
token_client = cf_acr_tokens(cmd.cli_ctx)

mode = mode.lower()
mode = mode.capitalize()
if not any(option for option in ConnectedRegistryModes if option.value == mode):
raise CLIError("usage error: --mode supports only 'registry' and 'mirror' values.")
repository_actions_list = [[repo] + REPO_SCOPES_BY_MODE[mode] for repo in repositories]
Expand Down Expand Up @@ -360,14 +373,34 @@ def _create_sync_token(cmd,
raise CLIError(e)


def _get_descendancy(family_tree, parent_id):
childs = family_tree[parent_id]['childs']
def _get_family_tree(connected_registry_list, target_connected_registry_name):
family_tree = {}
targetConnectedRegistry = None
# Populate the dictionary
for ConnectedRegistry in connected_registry_list:
family_tree[ConnectedRegistry.id] = {
"connectedRegistry": ConnectedRegistry,
"children": []
}
if ConnectedRegistry.name == target_connected_registry_name:
targetConnectedRegistry = ConnectedRegistry

# Populate Children dependencies
for ConnectedRegistry in connected_registry_list:
parent_id = ConnectedRegistry.parent.id
if parent_id and not parent_id.isspace():
family_tree[parent_id]["children"].append(ConnectedRegistry.id)
return family_tree, targetConnectedRegistry


def _get_descendants(family_tree, parent_id):
children = family_tree[parent_id]['children']
result = []
for child_id in childs:
result = [family_tree[child_id]["registry"]]
descendancy = _get_descendancy(family_tree, child_id)
if descendancy:
result.extend(descendancy)
for child_id in children:
result = [family_tree[child_id]["connectedRegistry"]]
descendants = _get_descendants(family_tree, child_id)
if descendants:
result.extend(descendants)
return result


Expand Down Expand Up @@ -440,3 +473,144 @@ def _get_install_info(cmd,
"ACR_PARENT_PROTOCOL": "https"
}
# endregion


def _check_ancestors_are_active(family_tree, parent_id, msg):
while parent_id and not parent_id.isspace():
ancestor = family_tree[parent_id]["connectedRegistry"]
if ancestor.activation.status != ConnectedRegistryActivationStatus.ACTIVE.value:
raise CLIError(msg)
parent_id = ancestor.parent.id


def _update_ancestor_permissions(cmd,
family_tree,
resource_group_name,
registry_name,
parent_id,
gateway,
repositories=None,
mode=None,
remove_access=False):
gateway_actions_list = [[gateway.lower()] + DEFAULT_GATEWAY_SCOPE]
if repositories is not None:
repository_actions_list = [[repo] + REPO_SCOPES_BY_MODE[mode] for repo in repositories]
repo_msg = ", ".join(repositories)
repo_msg = " and repo(s) '{}' {} permissions".format(repo_msg, mode)
if remove_access:
action_txt = "Removing"
add_actions_set = set()
remove_actions_set = set(parse_scope_map_actions(gateway_actions_list=gateway_actions_list))
else:
action_txt = "Adding"
add_actions_set = set(parse_scope_map_actions(repository_actions_list, gateway_actions_list))
remove_actions_set = set()

while parent_id and not parent_id.isspace():
ancestor = family_tree[parent_id]["connectedRegistry"]
msg = "{} '{}' gateway permissions{} to connected registry '{}' sync scope map.".format(
action_txt, gateway, repo_msg, ancestor.name)
_update_repo_permissions(cmd, resource_group_name, registry_name,
ancestor, add_actions_set, remove_actions_set, msg=msg)
parent_id = ancestor.parent.id


# region connected-registry repo update
def _update_repo_permissions(cmd,
resource_group_name,
registry_name,
connected_registry,
add_actions_set,
remove_actions_set,
msg=None,
description=None):
scope_map_client = cf_acr_scope_maps(cmd.cli_ctx)
sync_token = get_token_from_id(cmd, connected_registry.parent.sync_properties.token_id)
sync_scope_map = get_scope_map_from_id(cmd, sync_token.scope_map_id)
sync_scope_map_name = sync_scope_map.name
current_actions_set = set(sync_scope_map.actions)
final_actions_set = current_actions_set.union(add_actions_set).difference(remove_actions_set)
if final_actions_set == current_actions_set:
return None
current_actions = list(final_actions_set)
logger.warning(msg)
return scope_map_client.update(
resource_group_name,
registry_name,
sync_scope_map_name,
description,
current_actions
)


def _get_scope_map_actions_set(repos, actions):
for i, repo_name in enumerate(repos):
repos[i] = [repo_name] + actions
return set(parse_scope_map_actions(repos))


def acr_connected_registry_repo(cmd,
client,
connected_registry_name,
registry_name,
add_repos=None,
remove_repos=None,
resource_group_name=None):
if not (add_repos or remove_repos):
raise CLIError('No repository permissions to update.')
_, resource_group_name = validate_managed_registry(
cmd, registry_name, resource_group_name)

add_repos_set = set(add_repos) if add_repos is not None else set()
remove_repos_set = set(remove_repos) if remove_repos is not None else set()
duplicate_repos = set.intersection(add_repos_set, remove_repos_set)
if duplicate_repos:
errors = sorted(map(lambda action: action[action.rfind('/') + 1:], duplicate_repos))
raise CLIError(
'Update ambiguity. Duplicate repository names were provided with ' +
'--add and --remove arguments.\n{}'.format(errors))

connected_registry_list = list(client.list(resource_group_name, registry_name))
family_tree, target_connected_registry = _get_family_tree(connected_registry_list, connected_registry_name)
if target_connected_registry is None:
raise CLIError("Connected registry '{}' doesn't exist.".format(connected_registry_name))

# remove repo permissions from connected registry descendants.
remove_actions = REPO_SCOPES_BY_MODE[ConnectedRegistryModes.REGISTRY.value]
if remove_repos is not None:
remove_repos_txt = ", ".join(remove_repos)
remove_repos_set = _get_scope_map_actions_set(remove_repos, remove_actions)
descendants = _get_descendants(family_tree, target_connected_registry.id)
for connected_registry in descendants:
msg = "Removing '{}' permissions from {}".format(remove_repos_txt, connected_registry.name)
_update_repo_permissions(cmd, resource_group_name, registry_name,
connected_registry, set(), remove_repos_set, msg=msg)
else:
remove_repos_set = set()

# add repo permissions to ancestors.
add_actions = REPO_SCOPES_BY_MODE[target_connected_registry.mode]
if add_repos is not None:
add_repos_txt = ", ".join(add_repos)
add_repos_set = _get_scope_map_actions_set(add_repos, add_actions)
parent_id = target_connected_registry.parent.id
while parent_id and not parent_id.isspace():
connected_registry = family_tree[parent_id]["connectedRegistry"]
msg = "Adding '{}' permissions to {}".format(add_repos_txt, connected_registry.name)
_update_repo_permissions(cmd, resource_group_name, registry_name,
connected_registry, add_repos_set, set(), msg=msg)
parent_id = connected_registry.parent.id
else:
add_repos_set = set()

# update target connected registry repo permissions.
if add_repos and remove_repos:
msg = "Adding '{}' and removing '{}' permissions in {}".format(
add_repos_txt, remove_repos_txt, target_connected_registry.name)
elif add_repos:
msg = "Adding '{}' permissions to {}".format(add_repos_txt, target_connected_registry.name)
else:
msg = "Removing '{}' permissions from {}".format(remove_repos_txt, target_connected_registry.name)
_update_repo_permissions(cmd, resource_group_name, registry_name,
target_connected_registry, add_repos_set, remove_repos_set, msg=msg)
# endregion