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 6from knack.util import CLIError 7from knack.log import get_logger 8from knack.prompting import prompt_y_n, NoTTYException 9from azure.cli.core.commands.parameters import get_resources_in_subscription 10 11from ._constants import ( 12 REGISTRY_RESOURCE_TYPE, 13 ACR_RESOURCE_PROVIDER, 14 STORAGE_RESOURCE_TYPE, 15 ACR_TASK_YAML_DEFAULT_NAME, 16 get_classic_sku, 17 get_managed_sku, 18 get_premium_sku, 19 get_valid_os, 20 get_valid_architecture, 21 get_valid_variant 22) 23from ._client_factory import ( 24 get_arm_service_client, 25 get_storage_service_client, 26 get_acr_service_client 27) 28 29logger = get_logger(__name__) 30 31 32def _arm_get_resource_by_name(cli_ctx, resource_name, resource_type): 33 """Returns the ARM resource in the current subscription with resource_name. 34 :param str resource_name: The name of resource 35 :param str resource_type: The type of resource 36 """ 37 result = get_resources_in_subscription(cli_ctx, resource_type) 38 elements = [item for item in result if item.name.lower() == 39 resource_name.lower()] 40 41 if not elements: 42 from azure.cli.core._profile import Profile 43 profile = Profile(cli_ctx=cli_ctx) 44 message = "The resource with name '{}' and type '{}' could not be found".format( 45 resource_name, resource_type) 46 try: 47 subscription = profile.get_subscription( 48 cli_ctx.data['subscription_id']) 49 raise ResourceNotFound( 50 "{} in subscription '{} ({})'.".format(message, subscription['name'], subscription['id'])) 51 except (KeyError, TypeError) as e: 52 logger.debug( 53 "Could not get the current subscription. Exception: %s", str(e)) 54 raise ResourceNotFound( 55 "{} in the current subscription.".format(message)) 56 57 elif len(elements) == 1: 58 return elements[0] 59 else: 60 raise CLIError( 61 "More than one resources with type '{}' are found with name '{}'.".format( 62 resource_type, resource_name)) 63 64 65def _get_resource_group_name_by_resource_id(resource_id): 66 """Returns the resource group name from parsing the resource id. 67 :param str resource_id: The resource id 68 """ 69 resource_id = resource_id.lower() 70 resource_group_keyword = '/resourcegroups/' 71 return resource_id[resource_id.index(resource_group_keyword) + len( 72 resource_group_keyword): resource_id.index('/providers/')] 73 74 75def get_resource_group_name_by_registry_name(cli_ctx, registry_name, 76 resource_group_name=None): 77 """Returns the resource group name for the container registry. 78 :param str registry_name: The name of container registry 79 :param str resource_group_name: The name of resource group 80 """ 81 if not resource_group_name: 82 arm_resource = _arm_get_resource_by_name( 83 cli_ctx, registry_name, REGISTRY_RESOURCE_TYPE) 84 resource_group_name = _get_resource_group_name_by_resource_id( 85 arm_resource.id) 86 return resource_group_name 87 88 89def get_resource_id_by_storage_account_name(cli_ctx, storage_account_name): 90 """Returns the resource id for the storage account. 91 :param str storage_account_name: The name of storage account 92 """ 93 arm_resource = _arm_get_resource_by_name( 94 cli_ctx, storage_account_name, STORAGE_RESOURCE_TYPE) 95 return arm_resource.id 96 97 98def get_registry_by_name(cli_ctx, registry_name, resource_group_name=None): 99 """Returns a tuple of Registry object and resource group name. 100 :param str registry_name: The name of container registry 101 :param str resource_group_name: The name of resource group 102 """ 103 resource_group_name = get_resource_group_name_by_registry_name( 104 cli_ctx, registry_name, resource_group_name) 105 client = get_acr_service_client(cli_ctx).registries 106 107 return client.get(resource_group_name, registry_name), resource_group_name 108 109 110def get_registry_from_name_or_login_server(cli_ctx, login_server, registry_name=None): 111 """Returns a Registry object for the specified name. 112 :param str name: either the registry name or the login server of the registry. 113 """ 114 client = get_acr_service_client(cli_ctx).registries 115 registry_list = client.list() 116 117 if registry_name: 118 elements = [item for item in registry_list if 119 item.login_server.lower() == login_server.lower() or item.name.lower() == registry_name.lower()] 120 else: 121 elements = [item for item in registry_list if 122 item.login_server.lower() == login_server.lower()] 123 124 if len(elements) == 1: 125 return elements[0] 126 elif len(elements) > 1: 127 logger.warning( 128 "More than one registries were found by %s.", login_server) 129 return None 130 131 132def arm_deploy_template_new_storage(cli_ctx, 133 resource_group_name, 134 registry_name, 135 location, 136 sku, 137 storage_account_name, 138 admin_user_enabled, 139 deployment_name=None): 140 """Deploys ARM template to create a container registry with a new storage account. 141 :param str resource_group_name: The name of resource group 142 :param str registry_name: The name of container registry 143 :param str location: The name of location 144 :param str sku: The SKU of the container registry 145 :param str storage_account_name: The name of storage account 146 :param bool admin_user_enabled: Enable admin user 147 :param str deployment_name: The name of the deployment 148 """ 149 from azure.mgmt.resource.resources.models import DeploymentProperties 150 from azure.cli.core.util import get_file_json 151 import os 152 153 parameters = _parameters( 154 registry_name=registry_name, 155 location=location, 156 sku=sku, 157 admin_user_enabled=admin_user_enabled, 158 storage_account_name=storage_account_name) 159 160 file_path = os.path.join(os.path.dirname( 161 __file__), 'template_new_storage.json') 162 template = get_file_json(file_path) 163 properties = DeploymentProperties( 164 template=template, parameters=parameters, mode='incremental') 165 166 return _arm_deploy_template( 167 get_arm_service_client(cli_ctx).deployments, resource_group_name, deployment_name, properties) 168 169 170def arm_deploy_template_existing_storage(cli_ctx, 171 resource_group_name, 172 registry_name, 173 location, 174 sku, 175 storage_account_name, 176 admin_user_enabled, 177 deployment_name=None): 178 """Deploys ARM template to create a container registry with an existing storage account. 179 :param str resource_group_name: The name of resource group 180 :param str registry_name: The name of container registry 181 :param str location: The name of location 182 :param str sku: The SKU of the container registry 183 :param str storage_account_name: The name of storage account 184 :param bool admin_user_enabled: Enable admin user 185 :param str deployment_name: The name of the deployment 186 """ 187 from azure.mgmt.resource.resources.models import DeploymentProperties 188 from azure.cli.core.util import get_file_json 189 import os 190 191 storage_account_id = get_resource_id_by_storage_account_name( 192 cli_ctx, storage_account_name) 193 194 parameters = _parameters( 195 registry_name=registry_name, 196 location=location, 197 sku=sku, 198 admin_user_enabled=admin_user_enabled, 199 storage_account_id=storage_account_id) 200 201 file_path = os.path.join(os.path.dirname( 202 __file__), 'template_existing_storage.json') 203 template = get_file_json(file_path) 204 properties = DeploymentProperties( 205 template=template, parameters=parameters, mode='incremental') 206 207 return _arm_deploy_template( 208 get_arm_service_client(cli_ctx).deployments, resource_group_name, deployment_name, properties) 209 210 211def _arm_deploy_template(deployments_client, 212 resource_group_name, 213 deployment_name, 214 properties): 215 """Deploys ARM template to create a container registry. 216 :param obj deployments_client: ARM deployments service client 217 :param str resource_group_name: The name of resource group 218 :param str deployment_name: The name of the deployment 219 :param DeploymentProperties properties: The properties of a deployment 220 """ 221 if deployment_name is None: 222 import random 223 deployment_name = '{0}_{1}'.format( 224 ACR_RESOURCE_PROVIDER, random.randint(100, 800)) 225 226 return deployments_client.create_or_update(resource_group_name, deployment_name, properties) 227 228 229def _parameters(registry_name, 230 location, 231 sku, 232 admin_user_enabled, 233 storage_account_name=None, 234 storage_account_id=None, 235 registry_api_version=None): 236 """Returns a dict of deployment parameters. 237 :param str registry_name: The name of container registry 238 :param str location: The name of location 239 :param str sku: The SKU of the container registry 240 :param bool admin_user_enabled: Enable admin user 241 :param str storage_account_name: The name of storage account 242 :param str storage_account_id: The resource ID of storage account 243 :param str registry_api_version: The API version of the container registry 244 """ 245 parameters = { 246 'registryName': {'value': registry_name}, 247 'registryLocation': {'value': location}, 248 'registrySku': {'value': sku}, 249 'adminUserEnabled': {'value': admin_user_enabled} 250 } 251 if registry_api_version: 252 parameters['registryApiVersion'] = {'value': registry_api_version} 253 if storage_account_name: 254 parameters['storageAccountName'] = {'value': storage_account_name} 255 if storage_account_id: 256 parameters['storageAccountId'] = {'value': storage_account_id} 257 258 return parameters 259 260 261def random_storage_account_name(cli_ctx, registry_name): 262 from datetime import datetime 263 264 client = get_storage_service_client(cli_ctx).storage_accounts 265 prefix = registry_name[:18].lower() 266 267 for x in range(10): 268 time_stamp_suffix = datetime.utcnow().strftime('%H%M%S') 269 storage_account_name = ''.join([prefix, time_stamp_suffix])[:24] 270 logger.debug("Checking storage account %s with name '%s'.", 271 x, storage_account_name) 272 if client.check_name_availability(storage_account_name).name_available: # pylint: disable=no-member 273 return storage_account_name 274 275 raise CLIError( 276 "Could not find an available storage account name. Please try again later.") 277 278 279def validate_managed_registry(cmd, registry_name, resource_group_name=None, message=None): 280 """Raise CLIError if the registry in not in Managed SKU. 281 :param str registry_name: The name of container registry 282 :param str resource_group_name: The name of resource group 283 """ 284 registry, resource_group_name = get_registry_by_name( 285 cmd.cli_ctx, registry_name, resource_group_name) 286 287 if not registry.sku or registry.sku.name not in get_managed_sku(cmd): 288 raise CLIError( 289 message or "This operation is only supported for managed registries.") 290 291 return registry, resource_group_name 292 293 294def validate_premium_registry(cmd, registry_name, resource_group_name=None, message=None): 295 """Raise CLIError if the registry in not in Premium SKU. 296 :param str registry_name: The name of container registry 297 :param str resource_group_name: The name of resource group 298 """ 299 registry, resource_group_name = get_registry_by_name( 300 cmd.cli_ctx, registry_name, resource_group_name) 301 302 if not registry.sku or registry.sku.name not in get_premium_sku(cmd): 303 raise CLIError( 304 message or "This operation is only supported for managed registries in Premium SKU.") 305 306 return registry, resource_group_name 307 308 309def validate_sku_update(cmd, current_sku, sku_parameter): 310 """Validates a registry SKU update parameter. 311 :param object sku_parameter: The registry SKU update parameter 312 """ 313 if sku_parameter is None: 314 return 315 316 Sku = cmd.get_models('Sku') 317 if isinstance(sku_parameter, dict): 318 if 'name' not in sku_parameter: 319 _invalid_sku_update(cmd) 320 if sku_parameter['name'] not in get_classic_sku(cmd) and sku_parameter['name'] not in get_managed_sku(cmd): 321 _invalid_sku_update(cmd) 322 if current_sku in get_managed_sku(cmd) and sku_parameter['name'] in get_classic_sku(cmd): 323 _invalid_sku_downgrade() 324 elif isinstance(sku_parameter, Sku): 325 if current_sku in get_managed_sku(cmd) and sku_parameter.name in get_classic_sku(cmd): 326 _invalid_sku_downgrade() 327 else: 328 _invalid_sku_update(cmd) 329 330 331def _invalid_sku_update(cmd): 332 raise CLIError("Please specify SKU by '--sku SKU' or '--set sku.name=SKU'. Allowed SKUs: {0}".format( 333 get_managed_sku(cmd))) 334 335 336def _invalid_sku_downgrade(): 337 raise CLIError( 338 "Managed registries could not be downgraded to Classic SKU.") 339 340 341def user_confirmation(message, yes=False): 342 if yes: 343 return 344 try: 345 if not prompt_y_n(message): 346 raise CLIError('Operation cancelled.') 347 except NoTTYException: 348 raise CLIError( 349 'Unable to prompt for confirmation as no tty available. Use --yes.') 350 351 352def get_validate_platform(cmd, platform): 353 """Gets and validates the Platform from both flags 354 :param str platform: The name of Platform passed by user in --platform flag 355 """ 356 OS, Architecture = cmd.get_models('OS', 'Architecture') 357 # Defaults 358 platform_os = OS.linux.value 359 platform_arch = Architecture.amd64.value 360 platform_variant = None 361 362 if platform: 363 platform_split = platform.split('/') 364 platform_os = platform_split[0] 365 platform_arch = platform_split[1] if len( 366 platform_split) > 1 else Architecture.amd64.value 367 platform_variant = platform_split[2] if len( 368 platform_split) > 2 else None 369 370 platform_os = platform_os.lower() 371 platform_arch = platform_arch.lower() 372 373 valid_os = get_valid_os(cmd) 374 valid_arch = get_valid_architecture(cmd) 375 valid_variant = get_valid_variant(cmd) 376 377 if platform_os not in valid_os: 378 raise CLIError( 379 "'{0}' is not a valid value for OS specified in --os or --platform. " 380 "Valid options are {1}.".format(platform_os, ','.join(valid_os)) 381 ) 382 if platform_arch not in valid_arch: 383 raise CLIError( 384 "'{0}' is not a valid value for Architecture specified in --platform. " 385 "Valid options are {1}.".format( 386 platform_arch, ','.join(valid_arch)) 387 ) 388 if platform_variant and (platform_variant not in valid_variant): 389 raise CLIError( 390 "'{0}' is not a valid value for Variant specified in --platform. " 391 "Valid options are {1}.".format( 392 platform_variant, ','.join(valid_variant)) 393 ) 394 395 return platform_os, platform_arch, platform_variant 396 397 398def get_yaml_and_values(cmd_value, timeout, file): 399 """Generates yaml template and its value content if applicable 400 :param str cmd_value: The command to execute in each step 401 :param str timeout: The timeout for each step 402 :param str file: The task definition 403 """ 404 yaml_template = "" 405 values_content = "" 406 if cmd_value: 407 yaml_template = "steps: \n - cmd: {{ .Values.command }}\n" 408 values_content = "command: {0}\n".format(cmd_value) 409 if timeout: 410 yaml_template += " timeout: {{ .Values.timeout }}\n" 411 values_content += "timeout: {0}\n".format(timeout) 412 else: 413 if not file: 414 file = ACR_TASK_YAML_DEFAULT_NAME 415 416 if file == "-": 417 import sys 418 for s in sys.stdin.readlines(): 419 yaml_template += s 420 else: 421 import os 422 if os.path.exists(file): 423 f = open(file, 'r') 424 for line in f: 425 yaml_template += line 426 else: 427 raise CLIError("{0} does not exist.".format(file)) 428 429 if not yaml_template: 430 raise CLIError("Failed to initialize yaml template.") 431 432 return yaml_template, values_content 433 434 435def get_custom_registry_credentials(cmd, 436 auth_mode=None, 437 login_server=None, 438 username=None, 439 password=None, 440 identity=None, 441 is_remove=False): 442 """Get the credential object from the input 443 :param str auth_mode: The login mode for the source registry 444 :param str login_server: The login server of custom registry 445 :param str username: The username for custom registry (plain text or a key vault secret URI) 446 :param str password: The password for custom registry (plain text or a key vault secret URI) 447 :param str identity: The task managed identity used for the credential 448 """ 449 450 source_registry_credentials = None 451 if auth_mode: 452 SourceRegistryCredentials = cmd.get_models('SourceRegistryCredentials') 453 source_registry_credentials = SourceRegistryCredentials( 454 login_mode=auth_mode) 455 456 custom_registries = None 457 if login_server: 458 # if null username and password (or identity), then remove the credential 459 custom_reg_credential = None 460 461 is_identity_credential = False 462 if not username and not password: 463 is_identity_credential = identity is not None 464 465 CustomRegistryCredentials, SecretObject, SecretObjectType = cmd.get_models( 466 'CustomRegistryCredentials', 467 'SecretObject', 468 'SecretObjectType' 469 ) 470 471 if not is_remove: 472 if is_identity_credential: 473 custom_reg_credential = CustomRegistryCredentials( 474 identity=identity 475 ) 476 else: 477 custom_reg_credential = CustomRegistryCredentials( 478 user_name=SecretObject( 479 type=SecretObjectType.vaultsecret if is_vault_secret( 480 cmd, username)else SecretObjectType.opaque, 481 value=username 482 ), 483 password=SecretObject( 484 type=SecretObjectType.vaultsecret if is_vault_secret( 485 cmd, password) else SecretObjectType.opaque, 486 value=password 487 ), 488 identity=identity 489 ) 490 491 custom_registries = {login_server: custom_reg_credential} 492 493 Credentials = cmd.get_models('Credentials') 494 return Credentials( 495 source_registry=source_registry_credentials, 496 custom_registries=custom_registries 497 ) 498 499 500def is_vault_secret(cmd, credential): 501 keyvault_dns = None 502 try: 503 keyvault_dns = cmd.cli_ctx.cloud.suffixes.keyvault_dns 504 except ResourceNotFound: 505 return False 506 return keyvault_dns.upper() in credential.upper() 507 508 509class ResourceNotFound(CLIError): 510 """For exceptions that a resource couldn't be found in user's subscription 511 """ 512 pass 513