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# -------------------------------------------------------------------------------------------- 5import os 6import uuid 7import tempfile 8 9from knack.util import CLIError 10from knack.log import get_logger 11 12from azure.cli.core.commands import LongRunningOperation 13from azure.cli.core.commands.parameters import get_resources_in_subscription 14from azure.core.exceptions import ResourceNotFoundError 15 16from ._constants import ( 17 REGISTRY_RESOURCE_TYPE, 18 TASK_RESOURCE_ID_TEMPLATE, 19 ACR_TASK_YAML_DEFAULT_NAME, 20 get_classic_sku, 21 get_managed_sku, 22 get_premium_sku, 23 get_valid_os, 24 get_valid_architecture, 25 get_valid_variant, 26 ACR_NULL_CONTEXT 27) 28from ._client_factory import cf_acr_registries 29 30from ._archive_utils import upload_source_code, check_remote_source_code 31 32logger = get_logger(__name__) 33 34 35def _arm_get_resource_by_name(cli_ctx, resource_name, resource_type): 36 """Returns the ARM resource in the current subscription with resource_name. 37 :param str resource_name: The name of resource 38 :param str resource_type: The type of resource 39 """ 40 result = get_resources_in_subscription(cli_ctx, resource_type) 41 elements = [item for item in result if item.name.lower() == 42 resource_name.lower()] 43 44 if not elements: 45 from azure.cli.core._profile import Profile 46 profile = Profile(cli_ctx=cli_ctx) 47 message = "The resource with name '{}' and type '{}' could not be found".format( 48 resource_name, resource_type) 49 try: 50 subscription = profile.get_subscription( 51 cli_ctx.data['subscription_id']) 52 raise ResourceNotFound( 53 "{} in subscription '{} ({})'.".format(message, subscription['name'], subscription['id'])) 54 except (KeyError, TypeError) as e: 55 logger.debug( 56 "Could not get the current subscription. Exception: %s", str(e)) 57 raise ResourceNotFound( 58 "{} in the current subscription.".format(message)) 59 60 elif len(elements) == 1: 61 return elements[0] 62 else: 63 raise CLIError( 64 "More than one resources with type '{}' are found with name '{}'.".format( 65 resource_type, resource_name)) 66 67 68def _get_resource_group_name_by_resource_id(resource_id): 69 """Returns the resource group name from parsing the resource id. 70 :param str resource_id: The resource id 71 """ 72 resource_id = resource_id.lower() 73 resource_group_keyword = '/resourcegroups/' 74 return resource_id[resource_id.index(resource_group_keyword) + len( 75 resource_group_keyword): resource_id.index('/providers/')] 76 77 78def get_resource_group_name_by_registry_name(cli_ctx, registry_name, 79 resource_group_name=None): 80 """Returns the resource group name for the container registry. 81 :param str registry_name: The name of container registry 82 :param str resource_group_name: The name of resource group 83 """ 84 if not resource_group_name: 85 arm_resource = _arm_get_resource_by_name( 86 cli_ctx, registry_name, REGISTRY_RESOURCE_TYPE) 87 resource_group_name = _get_resource_group_name_by_resource_id( 88 arm_resource.id) 89 return resource_group_name 90 91 92def get_resource_id_by_registry_name(cli_ctx, registry_name): 93 """Returns the resource id for the container registry. 94 :param str storage_account_name: The name of container registry 95 """ 96 arm_resource = _arm_get_resource_by_name( 97 cli_ctx, registry_name, REGISTRY_RESOURCE_TYPE) 98 return arm_resource.id 99 100 101def get_registry_by_name(cli_ctx, registry_name, resource_group_name=None): 102 """Returns a tuple of Registry object and resource group name. 103 :param str registry_name: The name of container registry 104 :param str resource_group_name: The name of resource group 105 """ 106 resource_group_name = get_resource_group_name_by_registry_name( 107 cli_ctx, registry_name, resource_group_name) 108 client = cf_acr_registries(cli_ctx) 109 110 return client.get(resource_group_name, registry_name), resource_group_name 111 112 113def get_registry_from_name_or_login_server(cli_ctx, login_server, registry_name=None): 114 """Returns a Registry object for the specified name. 115 :param str name: either the registry name or the login server of the registry. 116 """ 117 client = cf_acr_registries(cli_ctx) 118 registry_list = client.list() 119 120 if registry_name: 121 elements = [item for item in registry_list if 122 item.login_server.lower() == login_server.lower() or item.name.lower() == registry_name.lower()] 123 else: 124 elements = [item for item in registry_list if 125 item.login_server.lower() == login_server.lower()] 126 127 if len(elements) == 1: 128 return elements[0] 129 if len(elements) > 1: 130 logger.warning( 131 "More than one registries were found by %s.", login_server) 132 return None 133 134 135def validate_managed_registry(cmd, registry_name, resource_group_name=None, message=None): 136 """Raise CLIError if the registry in not in Managed SKU. 137 :param str registry_name: The name of container registry 138 :param str resource_group_name: The name of resource group 139 """ 140 registry, resource_group_name = get_registry_by_name( 141 cmd.cli_ctx, registry_name, resource_group_name) 142 143 if not registry.sku or registry.sku.name not in get_managed_sku(cmd): 144 raise CLIError( 145 message or "This operation is only supported for managed registries.") 146 147 return registry, resource_group_name 148 149 150def validate_premium_registry(cmd, registry_name, resource_group_name=None, message=None): 151 """Raise CLIError if the registry in not in Premium SKU. 152 :param str registry_name: The name of container registry 153 :param str resource_group_name: The name of resource group 154 """ 155 registry, resource_group_name = get_registry_by_name( 156 cmd.cli_ctx, registry_name, resource_group_name) 157 158 if not registry.sku or registry.sku.name not in get_premium_sku(cmd): 159 raise CLIError( 160 message or "This operation is only supported for managed registries in Premium SKU.") 161 162 return registry, resource_group_name 163 164 165def validate_sku_update(cmd, current_sku, sku_parameter): 166 """Validates a registry SKU update parameter. 167 :param object sku_parameter: The registry SKU update parameter 168 """ 169 if sku_parameter is None: 170 return 171 172 Sku = cmd.get_models('Sku') 173 if isinstance(sku_parameter, dict): 174 if 'name' not in sku_parameter: 175 _invalid_sku_update(cmd) 176 if sku_parameter['name'] not in get_classic_sku(cmd) and sku_parameter['name'] not in get_managed_sku(cmd): 177 _invalid_sku_update(cmd) 178 if current_sku in get_managed_sku(cmd) and sku_parameter['name'] in get_classic_sku(cmd): 179 _invalid_sku_downgrade() 180 elif isinstance(sku_parameter, Sku): 181 if current_sku in get_managed_sku(cmd) and sku_parameter.name in get_classic_sku(cmd): 182 _invalid_sku_downgrade() 183 else: 184 _invalid_sku_update(cmd) 185 186 187def _invalid_sku_update(cmd): 188 raise CLIError("Please specify SKU by '--sku SKU' or '--set sku.name=SKU'. Allowed SKUs: {0}".format( 189 get_managed_sku(cmd))) 190 191 192def _invalid_sku_downgrade(): 193 raise CLIError( 194 "Managed registries could not be downgraded to Classic SKU.") 195 196 197def get_validate_platform(cmd, platform): 198 """Gets and validates the Platform from both flags 199 :param str platform: The name of Platform passed by user in --platform flag 200 """ 201 OS, Architecture = cmd.get_models('OS', 'Architecture', operation_group='runs') 202 203 # Defaults 204 platform_os = OS.linux.value 205 platform_arch = Architecture.amd64.value 206 platform_variant = None 207 208 if platform: 209 platform_split = platform.split('/') 210 platform_os = platform_split[0] 211 platform_arch = platform_split[1] if len(platform_split) > 1 else Architecture.amd64.value 212 platform_variant = platform_split[2] if len(platform_split) > 2 else None 213 214 platform_os = platform_os.lower() 215 platform_arch = platform_arch.lower() 216 217 valid_os = get_valid_os(cmd) 218 valid_arch = get_valid_architecture(cmd) 219 valid_variant = get_valid_variant(cmd) 220 221 if platform_os not in valid_os: 222 raise CLIError( 223 "'{0}' is not a valid value for OS specified in --platform. " 224 "Valid options are {1}.".format(platform_os, ','.join(valid_os)) 225 ) 226 if platform_arch not in valid_arch: 227 raise CLIError( 228 "'{0}' is not a valid value for Architecture specified in --platform. " 229 "Valid options are {1}.".format( 230 platform_arch, ','.join(valid_arch)) 231 ) 232 if platform_variant and (platform_variant not in valid_variant): 233 raise CLIError( 234 "'{0}' is not a valid value for Variant specified in --platform. " 235 "Valid options are {1}.".format( 236 platform_variant, ','.join(valid_variant)) 237 ) 238 239 return platform_os, platform_arch, platform_variant 240 241 242def get_yaml_template(cmd_value, timeout, file): 243 """Generates yaml template 244 :param str cmd_value: The command to execute in each step. Task version defaults to v1.1.0 245 :param str timeout: The timeout for each step 246 :param str file: The task definition 247 """ 248 yaml_template = "version: v1.1.0\n" 249 if cmd_value: 250 yaml_template += "steps: \n - cmd: {0}\n disableWorkingDirectoryOverride: true\n".format(cmd_value) 251 if timeout: 252 yaml_template += " timeout: {0}\n".format(timeout) 253 else: 254 if not file: 255 file = ACR_TASK_YAML_DEFAULT_NAME 256 257 if file == "-": 258 import sys 259 for s in sys.stdin.readlines(): 260 yaml_template += s 261 else: 262 if os.path.exists(file): 263 f = open(file, 'r') 264 for line in f: 265 yaml_template += line 266 else: 267 raise CLIError("{0} does not exist.".format(file)) 268 269 if not yaml_template: 270 raise CLIError("Failed to initialize yaml template.") 271 272 return yaml_template 273 274 275def get_custom_registry_credentials(cmd, 276 auth_mode=None, 277 login_server=None, 278 username=None, 279 password=None, 280 identity=None, 281 is_remove=False): 282 """Get the credential object from the input 283 :param str auth_mode: The login mode for the source registry 284 :param str login_server: The login server of custom registry 285 :param str username: The username for custom registry (plain text or a key vault secret URI) 286 :param str password: The password for custom registry (plain text or a key vault secret URI) 287 :param str identity: The task managed identity used for the credential 288 """ 289 Credentials, CustomRegistryCredentials, SourceRegistryCredentials, SecretObject, \ 290 SecretObjectType = cmd.get_models( 291 'Credentials', 'CustomRegistryCredentials', 'SourceRegistryCredentials', 'SecretObject', 292 'SecretObjectType', 293 operation_group='tasks') 294 295 source_registry_credentials = None 296 if auth_mode: 297 source_registry_credentials = SourceRegistryCredentials( 298 login_mode=auth_mode) 299 300 custom_registries = None 301 if login_server: 302 # if null username and password (or identity), then remove the credential 303 custom_reg_credential = None 304 305 is_identity_credential = False 306 if not username and not password: 307 is_identity_credential = identity is not None 308 309 if not is_remove: 310 if is_identity_credential: 311 custom_reg_credential = CustomRegistryCredentials( 312 identity=identity 313 ) 314 else: 315 custom_reg_credential = CustomRegistryCredentials( 316 user_name=SecretObject( 317 type=SecretObjectType.vaultsecret if is_vault_secret( 318 cmd, username)else SecretObjectType.opaque, 319 value=username 320 ), 321 password=SecretObject( 322 type=SecretObjectType.vaultsecret if is_vault_secret( 323 cmd, password) else SecretObjectType.opaque, 324 value=password 325 ), 326 identity=identity 327 ) 328 329 custom_registries = {login_server: custom_reg_credential} 330 331 return Credentials( 332 source_registry=source_registry_credentials, 333 custom_registries=custom_registries 334 ) 335 336 337def build_timers_info(cmd, schedules): 338 timer_triggers = [] 339 TriggerStatus, TimerTrigger = cmd.get_models('TriggerStatus', 'TimerTrigger', operation_group='tasks') 340 341 # Provide a default name for the timer if no name was provided. 342 for index, schedule in enumerate(schedules, start=1): 343 split_schedule = None 344 if ':' in schedule: 345 split_schedule = schedule.split(":") 346 timer_triggers.append( 347 TimerTrigger( 348 name=(split_schedule[0] if split_schedule else "t" + str(index)).strip(), 349 status=TriggerStatus.enabled.value, 350 schedule=split_schedule[1] if split_schedule else schedule 351 )) 352 return timer_triggers 353 354 355def remove_timer_trigger(task_name, 356 timer_name, 357 timer_triggers): 358 """Remove the timer trigger from the list of existing timer triggers for a task. 359 :param str task_name: The name of the task 360 :param str timer_name: The name of the timer trigger to be removed 361 :param str timer_triggers: The list of existing timer_triggers for a task 362 """ 363 364 if not timer_triggers: 365 raise CLIError("No timer triggers exist for the task '{}'.".format(task_name)) 366 367 # Check that the timer trigger exists in the list and if not exit 368 if any(timer.name == timer_name for timer in timer_triggers): 369 for timer in timer_triggers: 370 if timer.name == timer_name: 371 timer_triggers.remove(timer) 372 else: 373 raise CLIError("The timer '{}' does not exist for the task '{}'.".format(timer_name, task_name)) 374 375 return timer_triggers 376 377 378def add_days_to_now(days): 379 if days <= 0: 380 raise CLIError('Days must be positive.') 381 from datetime import datetime, timedelta 382 try: 383 return datetime.utcnow() + timedelta(days=days) 384 except OverflowError: 385 return datetime.max 386 387 388def is_vault_secret(cmd, credential): 389 keyvault_dns = None 390 try: 391 keyvault_dns = cmd.cli_ctx.cloud.suffixes.keyvault_dns 392 except ResourceNotFound: 393 return False 394 return keyvault_dns.upper() in credential.upper() 395 396 397def get_task_id_from_task_name(cli_ctx, resource_group, registry_name, task_name): 398 from azure.cli.core.commands.client_factory import get_subscription_id 399 subscription_id = get_subscription_id(cli_ctx) 400 return TASK_RESOURCE_ID_TEMPLATE.format( 401 sub_id=subscription_id, 402 rg=resource_group, 403 reg=registry_name, 404 name=task_name 405 ) 406 407 408def prepare_source_location(cmd, source_location, client_registries, registry_name, resource_group_name): 409 if not source_location or source_location.lower() == ACR_NULL_CONTEXT: 410 source_location = None 411 elif os.path.exists(source_location): 412 if not os.path.isdir(source_location): 413 raise CLIError( 414 "Source location should be a local directory path or remote URL.") 415 416 tar_file_path = os.path.join(tempfile.gettempdir( 417 ), 'cli_source_archive_{}.tar.gz'.format(uuid.uuid4().hex)) 418 419 try: 420 source_location = upload_source_code( 421 cmd, client_registries, registry_name, resource_group_name, 422 source_location, tar_file_path, "", "") 423 except Exception as err: 424 raise CLIError(err) 425 finally: 426 try: 427 logger.debug( 428 "Deleting the archived source code from '%s'...", tar_file_path) 429 os.remove(tar_file_path) 430 except OSError: 431 pass 432 else: 433 source_location = check_remote_source_code(source_location) 434 logger.warning("Sending context to registry: %s...", registry_name) 435 436 return source_location 437 438 439class ResourceNotFound(CLIError): 440 """For exceptions that a resource couldn't be found in user's subscription 441 """ 442 443 444# Scope & Tokens help functions 445def parse_repositories_from_actions(actions): 446 if not actions: 447 return [] 448 from .scope_map import RepoScopeMapActions 449 valid_actions = {action.value for action in RepoScopeMapActions} 450 repositories = [] 451 REPOSITORY = 'repositories/' 452 for action in actions: 453 if action.startswith(REPOSITORY): 454 for rule in valid_actions: 455 if action.endswith(rule): 456 repo = action[len(REPOSITORY):-len(rule) - 1] 457 repositories.append(repo) 458 return list(set(repositories)) 459 460 461def parse_scope_map_actions(repository_actions_list=None, gateway_actions_list=None): 462 from .scope_map import RepoScopeMapActions, GatewayScopeMapActions 463 valid_actions = {action.value for action in RepoScopeMapActions} 464 actions = _parse_scope_map_actions(repository_actions_list, valid_actions, 'repositories') 465 valid_actions = {action.value for action in GatewayScopeMapActions} 466 actions.extend(_parse_scope_map_actions(gateway_actions_list, valid_actions, 'gateway')) 467 return actions 468 469 470def _parse_scope_map_actions(actions_list, valid_actions, action_prefix): 471 if not actions_list: 472 return [] 473 actions = [] 474 for rule in actions_list: 475 resource = rule[0].lower() 476 if len(rule) < 2: 477 raise CLIError('At least one action must be specified with "{}".'.format(resource)) 478 for action in rule[1:]: 479 action = action.lower() 480 if action not in valid_actions: 481 raise CLIError('Invalid action "{}" provided. \nValid actions are {}.'.format(action, valid_actions)) 482 actions.append('{}/{}/{}'.format(action_prefix, resource, action)) 483 return actions 484 485 486def create_default_scope_map(cmd, 487 resource_group_name, 488 registry_name, 489 scope_map_name, 490 repositories, 491 gateways, 492 scope_map_description="", 493 force=False): 494 from ._client_factory import cf_acr_scope_maps 495 scope_map_client = cf_acr_scope_maps(cmd.cli_ctx) 496 actions = parse_scope_map_actions(repositories, gateways) 497 try: 498 existing_scope_map = scope_map_client.get(resource_group_name, registry_name, scope_map_name) 499 # for command idempotency, if the actions are the same, we accept it 500 if sorted(existing_scope_map.actions) == sorted(actions): 501 return existing_scope_map 502 if force and existing_scope_map: 503 logger.warning("Overriding scope map '%s' properties", scope_map_name) 504 else: 505 raise CLIError('The default scope map was already configured with different repository permissions.' + 506 '\nPlease use "az acr scope-map update -r {} -n {} --add <REPO> --remove <REPO>" to update.' 507 .format(registry_name, scope_map_name)) 508 except ResourceNotFoundError: 509 pass 510 logger.info('Creating a scope map "%s" for provided permissions.', scope_map_name) 511 scope_map_request = { 512 'actions': actions, 513 'scope_map_description': scope_map_description 514 } 515 poller = scope_map_client.begin_create(resource_group_name, registry_name, scope_map_name, scope_map_request) 516 scope_map = LongRunningOperation(cmd.cli_ctx)(poller) 517 return scope_map 518 519 520def build_token_id(subscription_id, resource_group_name, registry_name, token_name): 521 return "/subscriptions/{}/resourceGroups/{}".format(subscription_id, resource_group_name) + \ 522 "/providers/Microsoft.ContainerRegistry/registries/{}/tokens/{}".format(registry_name, token_name) 523 524 525def get_token_from_id(cmd, token_id): 526 from ._client_factory import cf_acr_tokens 527 from .token import acr_token_show 528 token_client = cf_acr_tokens(cmd.cli_ctx) 529 # SCOPE MAP ID example 530 # /subscriptions/<1>/resourceGroups/<3>/providers/Microsoft.ContainerRegistry/registries/<7>/tokens/<9>' 531 token_info = token_id.lstrip('/').split('/') 532 if len(token_info) != 10: 533 raise CLIError("Not valid scope map id: {}".format(token_id)) 534 resource_group_name = token_info[3] 535 registry_name = token_info[7] 536 token_name = token_info[9] 537 return acr_token_show(cmd, token_client, registry_name, token_name, resource_group_name) 538 539 540def get_scope_map_from_id(cmd, scope_map_id): 541 from ._client_factory import cf_acr_scope_maps 542 from .scope_map import acr_scope_map_show 543 scope_map_client = cf_acr_scope_maps(cmd.cli_ctx) 544 # SCOPE MAP ID example 545 # /subscriptions/<1>/resourceGroups/<3>/providers/Microsoft.ContainerRegistry/registries/<7>/scopeMaps/<9>' 546 scope_info = scope_map_id.lstrip('/').split('/') 547 if len(scope_info) != 10: 548 raise CLIError("Not valid scope map id: {}".format(scope_map_id)) 549 resource_group_name = scope_info[3] 550 registry_name = scope_info[7] 551 scope_map_name = scope_info[9] 552 return acr_scope_map_show(cmd, scope_map_client, registry_name, scope_map_name, resource_group_name) 553# endregion 554 555 556def resolve_identity_client_id(cli_ctx, managed_identity_resource_id): 557 from azure.mgmt.msi import ManagedServiceIdentityClient 558 from azure.cli.core.commands.client_factory import get_mgmt_service_client 559 from msrestazure.tools import parse_resource_id 560 561 res = parse_resource_id(managed_identity_resource_id) 562 client = get_mgmt_service_client(cli_ctx, ManagedServiceIdentityClient, subscription_id=res['subscription']) 563 return client.user_assigned_identities.get(res['resource_group'], res['name']).client_id 564 565 566def get_task_details_by_name(cli_ctx, resource_group_name, registry_name, task_name): 567 """Returns the task details. 568 :param str resource_group_name: The name of resource group 569 :param str registry_name: The name of container registry 570 :param str task_name: The name of task 571 """ 572 from ._client_factory import cf_acr_tasks 573 client = cf_acr_tasks(cli_ctx) 574 return client.get_details(resource_group_name, registry_name, task_name) 575