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# pylint: disable=too-many-locals 7 8from knack.util import CLIError 9from knack.log import get_logger 10from azure.cli.core.util import user_confirmation 11 12from ._constants import get_managed_sku, get_premium_sku 13from ._utils import ( 14 get_registry_by_name, 15 validate_managed_registry, 16 validate_sku_update, 17 get_resource_group_name_by_registry_name, 18 resolve_identity_client_id 19) 20from ._docker_utils import get_login_credentials, EMPTY_GUID 21from .network_rule import NETWORK_RULE_NOT_SUPPORTED 22 23logger = get_logger(__name__) 24DEF_DIAG_SETTINGS_NAME_TEMPLATE = '{}-diagnostic-settings' 25SYSTEM_ASSIGNED_IDENTITY_ALIAS = '[system]' 26 27 28def acr_check_name(client, registry_name): 29 registry = { 30 'name': registry_name, 31 'type': 'Microsoft.ContainerRegistry/registries' 32 } 33 return client.check_name_availability(registry) 34 35 36def acr_list(client, resource_group_name=None): 37 if resource_group_name: 38 return client.list_by_resource_group(resource_group_name) 39 return client.list() 40 41 42def acr_create(cmd, 43 client, 44 registry_name, 45 resource_group_name, 46 sku, 47 location=None, 48 admin_enabled=False, 49 default_action=None, 50 workspace=None, 51 identity=None, 52 key_encryption_key=None, 53 public_network_enabled=None, 54 zone_redundancy=None, 55 allow_trusted_services=None, 56 allow_exports=None, 57 tags=None): 58 59 if default_action and sku not in get_premium_sku(cmd): 60 raise CLIError(NETWORK_RULE_NOT_SUPPORTED) 61 62 if sku not in get_managed_sku(cmd): 63 raise CLIError("Classic SKU is no longer supported. Please select a managed SKU.") 64 65 Registry, Sku, NetworkRuleSet = cmd.get_models('Registry', 'Sku', 'NetworkRuleSet') 66 registry = Registry(location=location, sku=Sku(name=sku), admin_user_enabled=admin_enabled, 67 zone_redundancy=zone_redundancy, tags=tags) 68 if default_action: 69 registry.network_rule_set = NetworkRuleSet(default_action=default_action) 70 71 if public_network_enabled is not None: 72 _configure_public_network_access(cmd, registry, public_network_enabled) 73 74 if identity or key_encryption_key: 75 _configure_cmk(cmd, registry, resource_group_name, identity, key_encryption_key) 76 77 _handle_network_bypass(cmd, registry, allow_trusted_services) 78 _handle_export_policy(cmd, registry, allow_exports) 79 80 lro_poller = client.begin_create(resource_group_name, registry_name, registry) 81 82 if workspace: 83 from msrestazure.tools import is_valid_resource_id, resource_id 84 from azure.cli.core.commands import LongRunningOperation 85 from azure.cli.core.commands.client_factory import get_subscription_id 86 acr = LongRunningOperation(cmd.cli_ctx)(lro_poller) 87 if not is_valid_resource_id(workspace): 88 workspace = resource_id(subscription=get_subscription_id(cmd.cli_ctx), 89 resource_group=resource_group_name, 90 namespace='microsoft.OperationalInsights', 91 type='workspaces', 92 name=workspace) 93 _create_diagnostic_settings(cmd.cli_ctx, acr, workspace) 94 return acr 95 96 return lro_poller 97 98 99def acr_delete(cmd, client, registry_name, resource_group_name=None, yes=False): 100 user_confirmation("Are you sure you want to delete the registry '{}'?".format(registry_name), yes) 101 resource_group_name = get_resource_group_name_by_registry_name(cmd.cli_ctx, registry_name, resource_group_name) 102 return client.begin_delete(resource_group_name, registry_name) 103 104 105def acr_show(cmd, client, registry_name, resource_group_name=None): 106 resource_group_name = get_resource_group_name_by_registry_name(cmd.cli_ctx, registry_name, resource_group_name) 107 return client.get(resource_group_name, registry_name) 108 109 110def acr_update_custom(cmd, 111 instance, 112 sku=None, 113 admin_enabled=None, 114 default_action=None, 115 data_endpoint_enabled=None, 116 public_network_enabled=None, 117 allow_trusted_services=None, 118 anonymous_pull_enabled=None, 119 allow_exports=None, 120 tags=None): 121 if sku is not None: 122 Sku = cmd.get_models('Sku') 123 instance.sku = Sku(name=sku) 124 125 if admin_enabled is not None: 126 instance.admin_user_enabled = admin_enabled 127 128 if tags is not None: 129 instance.tags = tags 130 131 if default_action is not None: 132 NetworkRuleSet = cmd.get_models('NetworkRuleSet') 133 instance.network_rule_set = NetworkRuleSet(default_action=default_action) 134 135 if data_endpoint_enabled is not None: 136 instance.data_endpoint_enabled = data_endpoint_enabled 137 138 if public_network_enabled is not None: 139 _configure_public_network_access(cmd, instance, public_network_enabled) 140 141 if anonymous_pull_enabled is not None: 142 instance.anonymous_pull_enabled = anonymous_pull_enabled 143 144 _handle_network_bypass(cmd, instance, allow_trusted_services) 145 _handle_export_policy(cmd, instance, allow_exports) 146 147 return instance 148 149 150def _configure_public_network_access(cmd, registry, enabled): 151 PublicNetworkAccess = cmd.get_models('PublicNetworkAccess') 152 registry.public_network_access = (PublicNetworkAccess.enabled if enabled else PublicNetworkAccess.disabled) 153 154 155def _handle_network_bypass(cmd, registry, allow_trusted_services): 156 if allow_trusted_services is not None: 157 NetworkRuleBypassOptions = cmd.get_models('NetworkRuleBypassOptions') 158 registry.network_rule_bypass_options = (NetworkRuleBypassOptions.azure_services 159 if allow_trusted_services else NetworkRuleBypassOptions.none) 160 161 162def _handle_export_policy(cmd, registry, allow_exports): 163 if allow_exports is not None: 164 Policies, ExportPolicy, ExportPolicyStatus = cmd.get_models('Policies', 'ExportPolicy', 'ExportPolicyStatus') 165 166 if registry.policies is None: 167 registry.policies = Policies() 168 169 status = ExportPolicyStatus.DISABLED if not allow_exports else ExportPolicyStatus.ENABLED 170 try: 171 registry.policies.export_policy.status = status 172 except AttributeError: 173 registry.policies.export_policy = ExportPolicy(status=status) 174 175 176def acr_update_get(cmd): 177 """Returns an empty RegistryUpdateParameters object. 178 """ 179 RegistryUpdateParameters = cmd.get_models('RegistryUpdateParameters') 180 return RegistryUpdateParameters() 181 182 183def acr_update_set(cmd, 184 client, 185 registry_name, 186 resource_group_name=None, 187 parameters=None): 188 registry, resource_group_name = get_registry_by_name(cmd.cli_ctx, registry_name, resource_group_name) 189 190 if parameters.network_rule_set and registry.sku.name not in get_premium_sku(cmd): 191 raise CLIError(NETWORK_RULE_NOT_SUPPORTED) 192 193 validate_sku_update(cmd, registry.sku.name, parameters.sku) 194 195 return client.begin_update(resource_group_name, registry_name, parameters) 196 197 198def acr_show_endpoints(cmd, 199 registry_name, 200 resource_group_name=None): 201 registry, resource_group_name = get_registry_by_name(cmd.cli_ctx, registry_name, resource_group_name) 202 info = { 203 'loginServer': registry.login_server, 204 'dataEndpoints': [] 205 } 206 if registry.data_endpoint_enabled: 207 for host in registry.data_endpoint_host_names: 208 info['dataEndpoints'].append({ 209 'region': host.split('.')[1], 210 'endpoint': host, 211 }) 212 else: 213 logger.warning('To configure client firewall w/o using wildcard storage blob urls, ' 214 'use "az acr update --name %s --data-endpoint-enabled" to enable dedicated ' 215 'data endpoints.', registry_name) 216 from ._client_factory import cf_acr_replications 217 replicate_client = cf_acr_replications(cmd.cli_ctx) 218 replicates = list(replicate_client.list(resource_group_name, registry_name)) 219 for r in replicates: 220 info['dataEndpoints'].append({ 221 'region': r.location, 222 'endpoint': '*.blob.' + cmd.cli_ctx.cloud.suffixes.storage_endpoint, 223 }) 224 if not replicates: 225 info['dataEndpoints'].append({ 226 'region': registry.location, 227 'endpoint': '*.blob.' + cmd.cli_ctx.cloud.suffixes.storage_endpoint, 228 }) 229 230 return info 231 232 233def acr_login(cmd, 234 registry_name, 235 resource_group_name=None, # pylint: disable=unused-argument 236 tenant_suffix=None, 237 username=None, 238 password=None, 239 expose_token=False): 240 if expose_token: 241 if username or password: 242 raise CLIError("`--expose-token` cannot be combined with `--username` or `--password`.") 243 244 login_server, _, password = get_login_credentials( 245 cmd=cmd, 246 registry_name=registry_name, 247 tenant_suffix=tenant_suffix, 248 username=username, 249 password=password) 250 251 logger.warning("You can perform manual login using the provided access token below, " 252 "for example: 'docker login loginServer -u %s -p accessToken'", EMPTY_GUID) 253 254 token_info = { 255 "loginServer": login_server, 256 "accessToken": password 257 } 258 259 return token_info 260 261 tips = "You may want to use 'az acr login -n {} --expose-token' to get an access token, " \ 262 "which does not require Docker to be installed.".format(registry_name) 263 264 from azure.cli.core.util import in_cloud_console 265 if in_cloud_console(): 266 raise CLIError("This command requires running the docker daemon, " 267 "which is not supported in Azure Cloud Shell. " + tips) 268 269 try: 270 docker_command, _ = get_docker_command() 271 except CLIError as e: 272 logger.warning(tips) 273 raise e 274 275 login_server, username, password = get_login_credentials( 276 cmd=cmd, 277 registry_name=registry_name, 278 tenant_suffix=tenant_suffix, 279 username=username, 280 password=password) 281 282 # warn casing difference caused by ACR normalizing to lower on login_server 283 parts = login_server.split('.') 284 if registry_name != parts[0] and registry_name.lower() == parts[0]: 285 logger.warning('Uppercase characters are detected in the registry name. When using its server url in ' 286 'docker commands, to avoid authentication errors, use all lowercase.') 287 288 from subprocess import PIPE, Popen 289 logger.debug("Invoking '%s --username %s --password <redacted> %s'", 290 docker_command, username, login_server) 291 p = Popen([docker_command, "login", 292 "--username", username, 293 "--password", password, 294 login_server], stderr=PIPE) 295 _, stderr = p.communicate() 296 return_code = p.returncode 297 298 if stderr: 299 if b'error storing credentials' in stderr and b'stub received bad data' in stderr \ 300 and _check_wincred(login_server): 301 # Retry once after disabling wincred 302 p = Popen([docker_command, "login", 303 "--username", username, 304 "--password", password, 305 login_server]) 306 p.wait() 307 else: 308 stderr_messages = stderr.decode() 309 # Dismiss the '--password-stdin' warning 310 if b'--password-stdin' in stderr: 311 errors = [err for err in stderr_messages.split('\n') if err and '--password-stdin' not in err] 312 # Will not raise CLIError if there is no error other than '--password-stdin' 313 if not errors: 314 return None 315 stderr_messages = '\n'.join(errors) 316 logger.warning(stderr_messages) 317 318 # Raise error only if docker returns non-zero 319 if return_code != 0: 320 raise CLIError('Login failed.') 321 322 return None 323 324 325def acr_show_usage(cmd, client, registry_name, resource_group_name=None): 326 _, resource_group_name = validate_managed_registry(cmd, 327 registry_name, 328 resource_group_name, 329 "Usage is only supported for managed registries.") 330 return client.list_usages(resource_group_name, registry_name) 331 332 333def get_docker_command(is_diagnostics_context=False): 334 from ._errors import DOCKER_COMMAND_ERROR, DOCKER_DAEMON_ERROR 335 docker_command = 'docker' 336 337 from subprocess import PIPE, Popen, CalledProcessError 338 try: 339 p = Popen([docker_command, "ps"], stdout=PIPE, stderr=PIPE) 340 _, stderr = p.communicate() 341 except OSError as e: 342 logger.debug("Could not run '%s' command. Exception: %s", docker_command, str(e)) 343 # The executable may not be discoverable in WSL so retry *.exe once 344 try: 345 docker_command = 'docker.exe' 346 p = Popen([docker_command, "ps"], stdout=PIPE, stderr=PIPE) 347 _, stderr = p.communicate() 348 except OSError as inner: 349 logger.debug("Could not run '%s' command. Exception: %s", docker_command, str(inner)) 350 if is_diagnostics_context: 351 return None, DOCKER_COMMAND_ERROR 352 raise CLIError(DOCKER_COMMAND_ERROR.get_error_message()) 353 except CalledProcessError as inner: 354 logger.debug("Could not run '%s' command. Exception: %s", docker_command, str(inner)) 355 if is_diagnostics_context: 356 return docker_command, DOCKER_DAEMON_ERROR 357 raise CLIError(DOCKER_DAEMON_ERROR.get_error_message()) 358 except CalledProcessError as e: 359 logger.debug("Could not run '%s' command. Exception: %s", docker_command, str(e)) 360 if is_diagnostics_context: 361 return docker_command, DOCKER_DAEMON_ERROR 362 raise CLIError(DOCKER_DAEMON_ERROR.get_error_message()) 363 364 if stderr: 365 if is_diagnostics_context: 366 return None, DOCKER_COMMAND_ERROR.set_error_message(stderr.decode()) 367 raise CLIError(DOCKER_COMMAND_ERROR.set_error_message(stderr.decode()).get_error_message()) 368 369 return docker_command, None 370 371 372def _check_wincred(login_server): 373 import platform 374 if platform.system() == 'Windows': 375 import json 376 from os.path import expanduser, isfile, join 377 docker_directory = join(expanduser('~'), '.docker') 378 config_path = join(docker_directory, 'config.json') 379 logger.debug("Docker config file path %s", config_path) 380 if isfile(config_path): 381 with open(config_path) as input_file: 382 content = json.load(input_file) 383 input_file.close() 384 wincred = content.pop('credsStore', None) 385 if wincred and wincred.lower() == 'wincred': 386 # Ask for confirmation 387 from knack.prompting import prompt_y_n, NoTTYException 388 message = "This operation will disable wincred and use file system to store docker credentials." \ 389 " All registries that are currently logged in will be logged out." \ 390 "\nAre you sure you want to continue?" 391 try: 392 if prompt_y_n(message): 393 with open(config_path, 'w') as output_file: 394 json.dump(content, output_file, indent=4) 395 output_file.close() 396 return True 397 return False 398 except NoTTYException: 399 return False 400 # Don't update config file or retry as this doesn't seem to be a wincred issue 401 return False 402 403 import os 404 content = { 405 "auths": { 406 login_server: {} 407 } 408 } 409 try: 410 os.makedirs(docker_directory) 411 except OSError as e: 412 logger.debug("Could not create docker directory '%s'. Exception: %s", docker_directory, str(e)) 413 with open(config_path, 'w') as output_file: 414 json.dump(content, output_file, indent=4) 415 output_file.close() 416 return True 417 418 return False 419 420 421def _create_diagnostic_settings(cli_ctx, acr, workspace): 422 from azure.mgmt.monitor import MonitorManagementClient 423 from azure.mgmt.monitor.models import (DiagnosticSettingsResource, RetentionPolicy, 424 LogSettings, MetricSettings) 425 from azure.cli.core.commands.client_factory import get_mgmt_service_client 426 427 client = get_mgmt_service_client(cli_ctx, MonitorManagementClient) 428 def_retention_policy = RetentionPolicy(enabled=True, days=0) 429 logs = [ 430 LogSettings(enabled=True, category="ContainerRegistryRepositoryEvents", retention_policy=def_retention_policy), 431 LogSettings(enabled=True, category="ContainerRegistryLoginEvents", retention_policy=def_retention_policy) 432 ] 433 metrics = [MetricSettings(enabled=True, category="AllMetrics", retention_policy=def_retention_policy)] 434 parameters = DiagnosticSettingsResource(workspace_id=workspace, metrics=metrics, logs=logs) 435 436 client.diagnostic_settings.create_or_update(resource_uri=acr.id, parameters=parameters, 437 name=DEF_DIAG_SETTINGS_NAME_TEMPLATE.format(acr.name)) 438 439 440def _configure_cmk(cmd, registry, resource_group_name, identity, key_encryption_key): 441 from azure.cli.core.commands.client_factory import get_subscription_id 442 443 if bool(identity) != bool(key_encryption_key): 444 raise CLIError("Usage error: --identity and --key-encryption-key must be both supplied") 445 446 identity = _ensure_identity_resource_id(subscription_id=get_subscription_id(cmd.cli_ctx), 447 resource_group=resource_group_name, 448 resource=identity) 449 450 identity_client_id = resolve_identity_client_id(cmd.cli_ctx, identity) 451 452 KeyVaultProperties, EncryptionProperty = cmd.get_models('KeyVaultProperties', 'EncryptionProperty') 453 registry.encryption = EncryptionProperty(status='enabled', key_vault_properties=KeyVaultProperties( 454 key_identifier=key_encryption_key, identity=identity_client_id)) 455 456 ResourceIdentityType, IdentityProperties = cmd.get_models('ResourceIdentityType', 'IdentityProperties') 457 registry.identity = IdentityProperties(type=ResourceIdentityType.user_assigned, 458 user_assigned_identities={identity: {}}) 459 460 461def assign_identity(cmd, client, registry_name, identities, resource_group_name=None): 462 from azure.cli.core.commands.client_factory import get_subscription_id 463 assign_system_identity, assign_user_identities = _analyze_identities(identities) 464 registry, resource_group_name = get_registry_by_name(cmd.cli_ctx, registry_name, resource_group_name) 465 466 IdentityProperties, ResourceIdentityType = cmd.get_models('IdentityProperties', 'ResourceIdentityType') 467 468 # ensure registry.identity is set and is of type IdentityProperties 469 registry.identity = registry.identity or IdentityProperties(type=ResourceIdentityType.none) 470 471 if assign_system_identity and registry.identity.type != ResourceIdentityType.system_assigned: 472 registry.identity.type = (ResourceIdentityType.system_assigned 473 if registry.identity.type == ResourceIdentityType.none 474 else ResourceIdentityType.system_assigned_user_assigned) 475 if assign_user_identities and registry.identity.type != ResourceIdentityType.user_assigned: 476 registry.identity.type = (ResourceIdentityType.user_assigned 477 if registry.identity.type == ResourceIdentityType.none 478 else ResourceIdentityType.system_assigned_user_assigned) 479 480 if assign_user_identities: 481 subscription_id = get_subscription_id(cmd.cli_ctx) 482 registry.identity.user_assigned_identities = registry.identity.user_assigned_identities or {} 483 484 for r in assign_user_identities: 485 r = _ensure_identity_resource_id(subscription_id, resource_group_name, r) 486 registry.identity.user_assigned_identities[r] = {} 487 488 return client.begin_update(resource_group_name, registry_name, registry) 489 490 491def show_identity(cmd, client, registry_name, resource_group_name=None): 492 return acr_show(cmd, client, registry_name, resource_group_name).identity 493 494 495def remove_identity(cmd, client, registry_name, identities, resource_group_name=None): 496 from azure.cli.core.commands.client_factory import get_subscription_id 497 remove_system_identity, remove_user_identities = _analyze_identities(identities) 498 registry, resource_group_name = get_registry_by_name(cmd.cli_ctx, registry_name, resource_group_name) 499 500 ResourceIdentityType = cmd.get_models('ResourceIdentityType') 501 502 # if registry.identity is not set or is none, return the registry. 503 if not registry.identity or registry.identity.type == ResourceIdentityType.none: 504 raise CLIError("The registry {} has no system or user assigned identities.".format(registry_name)) 505 506 if remove_system_identity: 507 if registry.identity.type.lower() == ResourceIdentityType.user_assigned.lower(): 508 raise CLIError("The registry does not have a system identity assigned.") 509 registry.identity.type = (ResourceIdentityType.none 510 if registry.identity.type.lower() == ResourceIdentityType.system_assigned.lower() 511 else ResourceIdentityType.user_assigned) 512 # if we have no system assigned identitiy then set identity object to none 513 registry.identity.principal_id = None 514 registry.identity.tenant_id = None 515 516 if remove_user_identities: 517 subscription_id = get_subscription_id(cmd.cli_ctx) 518 registry.identity.user_assigned_identities = registry.identity.user_assigned_identities or {} 519 520 for id_to_remove in remove_user_identities: 521 original_identity = id_to_remove 522 was_removed = False 523 524 id_to_remove = _ensure_identity_resource_id(subscription_id, resource_group_name, id_to_remove) 525 526 # remove identity if it exists even if case is different 527 for existing_identity in registry.identity.user_assigned_identities.copy(): 528 if existing_identity.lower() == id_to_remove.lower(): 529 registry.identity.user_assigned_identities.pop(existing_identity) 530 was_removed = True 531 break 532 533 if not was_removed: 534 raise CLIError("The registry does not have specified user identity '{}' assigned, " 535 "so it cannot be removed.".format(original_identity)) 536 537 # all user assigned identities are gone 538 if not registry.identity.user_assigned_identities: 539 registry.identity.user_assigned_identities = None # required for put 540 registry.identity.type = (ResourceIdentityType.none 541 if registry.identity.type.lower() == ResourceIdentityType.user_assigned.lower() 542 else ResourceIdentityType.system_assigned) 543 544 # this method should be named create_or_update as it calls the PUT method 545 return client.begin_create(resource_group_name, registry_name, registry) 546 547 548def show_encryption(cmd, client, registry_name, resource_group_name=None): 549 return acr_show(cmd, client, registry_name, resource_group_name).encryption 550 551 552def rotate_key(cmd, client, registry_name, identity=None, key_encryption_key=None, resource_group_name=None): 553 registry, resource_group_name = get_registry_by_name(cmd.cli_ctx, registry_name, resource_group_name) 554 if not registry.encryption or not registry.encryption.key_vault_properties: 555 raise CLIError('usage error: key rotation is only applicable to registries with CMK enabled') 556 if key_encryption_key: 557 registry.encryption.key_vault_properties.key_identifier = key_encryption_key 558 if identity: 559 try: 560 import uuid 561 uuid.UUID(identity) 562 client_id = identity 563 except ValueError: 564 from azure.cli.core.commands.client_factory import get_subscription_id 565 if identity == SYSTEM_ASSIGNED_IDENTITY_ALIAS: 566 client_id = 'system' # reserved word on ACR service 567 else: 568 identity = _ensure_identity_resource_id(subscription_id=get_subscription_id(cmd.cli_ctx), 569 resource_group=resource_group_name, 570 resource=identity) 571 client_id = resolve_identity_client_id(cmd.cli_ctx, identity) 572 573 registry.encryption.key_vault_properties.identity = client_id 574 575 return client.begin_update(resource_group_name, registry_name, registry) 576 577 578def _analyze_identities(identities): 579 identities = identities or [] 580 return SYSTEM_ASSIGNED_IDENTITY_ALIAS in identities, [x for x in identities if x != SYSTEM_ASSIGNED_IDENTITY_ALIAS] 581 582 583def _ensure_identity_resource_id(subscription_id, resource_group, resource): 584 from msrestazure.tools import resource_id, is_valid_resource_id 585 if is_valid_resource_id(resource): 586 return resource 587 return resource_id(subscription=subscription_id, 588 resource_group=resource_group, 589 namespace='Microsoft.ManagedIdentity', 590 type='userAssignedIdentities', 591 name=resource) 592 593 594def list_private_link_resources(cmd, client, registry_name, resource_group_name=None): 595 resource_group_name = get_resource_group_name_by_registry_name(cmd.cli_ctx, registry_name, resource_group_name) 596 return client.list_private_link_resources(resource_group_name, registry_name) 597