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
8
9from azure.cli.core.commands import LongRunningOperation
10
11from ._constants import get_classic_sku, get_managed_sku, get_premium_sku
12from ._utils import (
13    arm_deploy_template_new_storage,
14    arm_deploy_template_existing_storage,
15    random_storage_account_name,
16    get_registry_by_name,
17    validate_managed_registry,
18    validate_sku_update,
19    get_resource_group_name_by_registry_name,
20    get_resource_id_by_storage_account_name
21)
22from ._docker_utils import get_login_credentials
23from .network_rule import NETWORK_RULE_NOT_SUPPORTED
24
25logger = get_logger(__name__)
26
27
28def acr_check_name(client, registry_name):
29    return client.check_name_availability(registry_name)
30
31
32def acr_list(client, resource_group_name=None):
33    if resource_group_name:
34        return client.list_by_resource_group(resource_group_name)
35    return client.list()
36
37
38def acr_create(cmd,
39               client,
40               registry_name,
41               resource_group_name,
42               sku,
43               location=None,
44               storage_account_name=None,
45               admin_enabled=False,
46               default_action=None,
47               deployment_name=None):
48    if default_action and sku not in get_premium_sku(cmd):
49        raise CLIError(NETWORK_RULE_NOT_SUPPORTED)
50
51    if sku in get_managed_sku(cmd) and storage_account_name:
52        raise CLIError("Please specify '--sku {}' without providing an existing storage account "
53                       "to create a managed registry, or specify '--sku Classic --storage-account-name {}' "
54                       "to create a Classic registry using storage account `{}`."
55                       .format(sku, storage_account_name, storage_account_name))
56
57    if sku in get_classic_sku(cmd):
58        result = client.check_name_availability(registry_name)
59        if not result.name_available:
60            raise CLIError(result.message)
61
62        logger.warning(
63            "Due to the planned deprecation of the Classic registry SKU, we recommend using "
64            "Basic, Standard, or Premium for all new registries. See https://aka.ms/acr/skus for details.")
65        if storage_account_name is None:
66            storage_account_name = random_storage_account_name(cmd.cli_ctx, registry_name)
67            logger.warning(
68                "A new storage account '%s' will be created in resource group '%s'.",
69                storage_account_name,
70                resource_group_name)
71            LongRunningOperation(cmd.cli_ctx)(
72                arm_deploy_template_new_storage(
73                    cmd.cli_ctx,
74                    resource_group_name,
75                    registry_name,
76                    location,
77                    sku,
78                    storage_account_name,
79                    admin_enabled,
80                    deployment_name)
81            )
82        else:
83            LongRunningOperation(cmd.cli_ctx)(
84                arm_deploy_template_existing_storage(
85                    cmd.cli_ctx,
86                    resource_group_name,
87                    registry_name,
88                    location,
89                    sku,
90                    storage_account_name,
91                    admin_enabled,
92                    deployment_name)
93            )
94        return client.get(resource_group_name, registry_name)
95    else:
96        if storage_account_name:
97            logger.warning(
98                "The registry '%s' in '%s' SKU is a managed registry. The specified storage account will be ignored.",
99                registry_name, sku)
100        Registry, Sku, NetworkRuleSet = cmd.get_models('Registry', 'Sku', 'NetworkRuleSet')
101        registry = Registry(location=location, sku=Sku(name=sku), admin_user_enabled=admin_enabled)
102        if default_action:
103            registry.network_rule_set = NetworkRuleSet(default_action=default_action)
104        return client.create(resource_group_name, registry_name, registry)
105
106
107def acr_delete(cmd, client, registry_name, resource_group_name=None):
108    resource_group_name = get_resource_group_name_by_registry_name(cmd.cli_ctx, registry_name, resource_group_name)
109    return client.delete(resource_group_name, registry_name)
110
111
112def acr_show(cmd, client, registry_name, resource_group_name=None):
113    resource_group_name = get_resource_group_name_by_registry_name(cmd.cli_ctx, registry_name, resource_group_name)
114    return client.get(resource_group_name, registry_name)
115
116
117def acr_update_custom(cmd,
118                      instance,
119                      sku=None,
120                      storage_account_name=None,
121                      admin_enabled=None,
122                      default_action=None,
123                      tags=None):
124    if sku is not None:
125        Sku = cmd.get_models('Sku')
126        instance.sku = Sku(name=sku)
127
128    if storage_account_name is not None:
129        StorageAccountProperties = cmd.get_models('StorageAccountProperties')
130        instance.storage_account = StorageAccountProperties(
131            id=get_resource_id_by_storage_account_name(cmd.cli_ctx, storage_account_name))
132
133    if admin_enabled is not None:
134        instance.admin_user_enabled = admin_enabled
135
136    if tags is not None:
137        instance.tags = tags
138
139    if default_action is not None:
140        NetworkRuleSet = cmd.get_models('NetworkRuleSet')
141        instance.network_rule_set = NetworkRuleSet(default_action=default_action)
142
143    return instance
144
145
146def acr_update_get(cmd):
147    """Returns an empty RegistryUpdateParameters object.
148    """
149    RegistryUpdateParameters = cmd.get_models('RegistryUpdateParameters')
150    return RegistryUpdateParameters()
151
152
153def acr_update_set(cmd,
154                   client,
155                   registry_name,
156                   resource_group_name=None,
157                   parameters=None):
158    registry, resource_group_name = get_registry_by_name(cmd.cli_ctx, registry_name, resource_group_name)
159
160    if parameters.network_rule_set and registry.sku.name not in get_premium_sku(cmd):
161        raise CLIError(NETWORK_RULE_NOT_SUPPORTED)
162
163    validate_sku_update(cmd, registry.sku.name, parameters.sku)
164
165    if registry.sku.name in get_managed_sku(cmd) and parameters.storage_account is not None:
166        parameters.storage_account = None
167        logger.warning(
168            "The registry '%s' in '%s' SKU is a managed registry. The specified storage account will be ignored.",
169            registry_name, registry.sku.name)
170
171    return client.update(resource_group_name, registry_name, parameters)
172
173
174def acr_login(cmd,
175              registry_name,
176              resource_group_name=None,  # pylint: disable=unused-argument
177              tenant_suffix=None,
178              username=None,
179              password=None):
180    from azure.cli.core.util import in_cloud_console
181    if in_cloud_console():
182        raise CLIError('This command requires running the docker daemon, which is not supported in Azure Cloud Shell.')
183
184    docker_command, _ = get_docker_command()
185
186    login_server, username, password = get_login_credentials(
187        cmd=cmd,
188        registry_name=registry_name,
189        tenant_suffix=tenant_suffix,
190        username=username,
191        password=password)
192
193    from subprocess import PIPE, Popen
194    p = Popen([docker_command, "login",
195               "--username", username,
196               "--password", password,
197               login_server], stderr=PIPE)
198    _, stderr = p.communicate()
199
200    if stderr:
201        if b'error storing credentials' in stderr and b'stub received bad data' in stderr \
202           and _check_wincred(login_server):
203            # Retry once after disabling wincred
204            p = Popen([docker_command, "login",
205                       "--username", username,
206                       "--password", password,
207                       login_server])
208            p.wait()
209        else:
210            if b'--password-stdin' in stderr:
211                errors = [err for err in stderr.decode().split('\n') if '--password-stdin' not in err]
212                stderr = '\n'.join(errors).encode()
213
214            import sys
215            output = getattr(sys.stderr, 'buffer', sys.stderr)
216            output.write(stderr)
217
218
219def acr_show_usage(cmd, client, registry_name, resource_group_name=None):
220    _, resource_group_name = validate_managed_registry(cmd,
221                                                       registry_name,
222                                                       resource_group_name,
223                                                       "Usage is only supported for managed registries.")
224    return client.list_usages(resource_group_name, registry_name)
225
226
227def get_docker_command(is_diagnostics_context=False):
228    from ._errors import DOCKER_COMMAND_ERROR, DOCKER_DAEMON_ERROR
229    docker_command = 'docker'
230
231    from subprocess import PIPE, Popen, CalledProcessError
232    try:
233        p = Popen([docker_command, "ps"], stdout=PIPE, stderr=PIPE)
234        _, stderr = p.communicate()
235    except OSError as e:
236        logger.debug("Could not run '%s' command. Exception: %s", docker_command, str(e))
237        # The executable may not be discoverable in WSL so retry *.exe once
238        try:
239            docker_command = 'docker.exe'
240            p = Popen([docker_command, "ps"], stdout=PIPE, stderr=PIPE)
241            _, stderr = p.communicate()
242        except OSError as inner:
243            logger.debug("Could not run '%s' command. Exception: %s", docker_command, str(inner))
244            if is_diagnostics_context:
245                return None, DOCKER_COMMAND_ERROR
246            raise CLIError(DOCKER_COMMAND_ERROR.get_error_message())
247        except CalledProcessError as inner:
248            logger.debug("Could not run '%s' command. Exception: %s", docker_command, str(inner))
249            if is_diagnostics_context:
250                return docker_command, DOCKER_DAEMON_ERROR
251            raise CLIError(DOCKER_DAEMON_ERROR.get_error_message())
252    except CalledProcessError as e:
253        logger.debug("Could not run '%s' command. Exception: %s", docker_command, str(e))
254        if is_diagnostics_context:
255            return docker_command, DOCKER_DAEMON_ERROR
256        raise CLIError(DOCKER_DAEMON_ERROR.get_error_message())
257
258    if stderr:
259        if is_diagnostics_context:
260            return None, DOCKER_COMMAND_ERROR.set_error_message(stderr.decode())
261        raise CLIError(DOCKER_COMMAND_ERROR.set_error_message(stderr.decode()).get_error_message())
262
263    return docker_command, None
264
265
266def _check_wincred(login_server):
267    import platform
268    if platform.system() == 'Windows':
269        import json
270        from os.path import expanduser, isfile, join
271        docker_directory = join(expanduser('~'), '.docker')
272        config_path = join(docker_directory, 'config.json')
273        logger.debug("Docker config file path %s", config_path)
274        if isfile(config_path):
275            with open(config_path) as input_file:
276                content = json.load(input_file)
277                input_file.close()
278            wincred = content.pop('credsStore', None)
279            if wincred and wincred.lower() == 'wincred':
280                # Ask for confirmation
281                from knack.prompting import prompt_y_n, NoTTYException
282                message = "This operation will disable wincred and use file system to store docker credentials." \
283                          " All registries that are currently logged in will be logged out." \
284                          "\nAre you sure you want to continue?"
285                try:
286                    if prompt_y_n(message):
287                        with open(config_path, 'w') as output_file:
288                            json.dump(content, output_file, indent=4)
289                            output_file.close()
290                        return True
291                    return False
292                except NoTTYException:
293                    return False
294            # Don't update config file or retry as this doesn't seem to be a wincred issue
295            return False
296        else:
297            import os
298            content = {
299                "auths": {
300                    login_server: {}
301                }
302            }
303            try:
304                os.makedirs(docker_directory)
305            except OSError as e:
306                logger.debug("Could not create docker directory '%s'. Exception: %s", docker_directory, str(e))
307            with open(config_path, 'w') as output_file:
308                json.dump(content, output_file, indent=4)
309                output_file.close()
310            return True
311
312    return False
313