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