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
7import uuid
8import tempfile
9
10import os
11
12from knack.log import get_logger
13from knack.util import CLIError
14from azure.cli.core.commands import LongRunningOperation
15
16from ._utils import validate_managed_registry, get_validate_platform, get_custom_registry_credentials
17from ._stream_utils import stream_logs
18from ._archive_utils import upload_source_code, check_remote_source_code
19
20logger = get_logger(__name__)
21
22
23BUILD_NOT_SUPPORTED = 'Builds are only supported for managed registries.'
24
25
26def acr_build(cmd,  # pylint: disable=too-many-locals
27              client,
28              registry_name,
29              source_location,
30              image_names=None,
31              resource_group_name=None,
32              timeout=None,
33              arg=None,
34              secret_arg=None,
35              docker_file_path='',
36              no_format=False,
37              no_push=False,
38              no_logs=False,
39              no_wait=False,
40              platform=None,
41              target=None,
42              auth_mode=None):
43    _, resource_group_name = validate_managed_registry(
44        cmd, registry_name, resource_group_name, BUILD_NOT_SUPPORTED)
45
46    from ._client_factory import cf_acr_registries
47    client_registries = cf_acr_registries(cmd.cli_ctx)
48
49    if os.path.exists(source_location):
50        if not os.path.isdir(source_location):
51            raise CLIError("Source location should be a local directory path or remote URL.")
52
53        # NOTE: If docker_file_path is not specified, the default is Dockerfile in source_location.
54        # Otherwise, it's based on current working directory.
55        if not docker_file_path:
56            docker_file_path = os.path.join(source_location, "Dockerfile")
57            logger.info("'--file or -f' is not provided. '%s' is used.", docker_file_path)
58
59        _check_local_docker_file(docker_file_path)
60
61        tar_file_path = os.path.join(tempfile.gettempdir(
62        ), 'build_archive_{}.tar.gz'.format(uuid.uuid4().hex))
63
64        try:
65            # NOTE: os.path.basename is unable to parse "\" in the file path
66            original_docker_file_name = os.path.basename(
67                docker_file_path.replace("\\", "/"))
68            docker_file_in_tar = '{}_{}'.format(
69                uuid.uuid4().hex, original_docker_file_name)
70
71            source_location = upload_source_code(
72                client_registries, registry_name, resource_group_name,
73                source_location, tar_file_path,
74                docker_file_path, docker_file_in_tar)
75            # For local source, the docker file is added separately into tar as the new file name (docker_file_in_tar)
76            # So we need to update the docker_file_path
77            docker_file_path = docker_file_in_tar
78        except Exception as err:
79            raise CLIError(err)
80        finally:
81            try:
82                logger.debug("Deleting the archived source code from '%s'...", tar_file_path)
83                os.remove(tar_file_path)
84            except OSError:
85                pass
86    else:
87        # NOTE: If docker_file_path is not specified, the default is Dockerfile. It's the same as docker build command.
88        if not docker_file_path:
89            docker_file_path = "Dockerfile"
90            logger.info("'--file or -f' is not provided. '%s' is used.", docker_file_path)
91
92        source_location = check_remote_source_code(source_location)
93        logger.warning("Sending context to registry: %s...", registry_name)
94
95    is_push_enabled = _get_push_enabled_status(no_push, image_names)
96
97    platform_os, platform_arch, platform_variant = get_validate_platform(cmd, platform)
98
99    DockerBuildRequest, PlatformProperties = cmd.get_models('DockerBuildRequest', 'PlatformProperties')
100    docker_build_request = DockerBuildRequest(
101        image_names=image_names,
102        is_push_enabled=is_push_enabled,
103        source_location=source_location,
104        platform=PlatformProperties(
105            os=platform_os,
106            architecture=platform_arch,
107            variant=platform_variant
108        ),
109        docker_file_path=docker_file_path,
110        timeout=timeout,
111        arguments=(arg if arg else []) + (secret_arg if secret_arg else []),
112        target=target,
113        credentials=get_custom_registry_credentials(
114            cmd=cmd,
115            auth_mode=auth_mode
116        )
117    )
118
119    queued = LongRunningOperation(cmd.cli_ctx)(client_registries.schedule_run(
120        resource_group_name=resource_group_name,
121        registry_name=registry_name,
122        run_request=docker_build_request))
123
124    run_id = queued.run_id
125    logger.warning("Queued a build with ID: %s", run_id)
126
127    if no_wait:
128        return queued
129
130    logger.warning("Waiting for an agent...")
131
132    if no_logs:
133        from ._run_polling import get_run_with_polling
134        return get_run_with_polling(cmd, client, run_id, registry_name, resource_group_name)
135
136    return stream_logs(client, run_id, registry_name, resource_group_name, no_format, True)
137
138
139def _warn_unsupported_image_name(image_names):
140    for img in image_names:
141        if ".Build.ID" in img:
142            logger.warning(".Build.ID is no longer supported as a valid substitution, use .Run.ID instead.")
143            break
144
145
146def _get_push_enabled_status(no_push, image_names):
147    if no_push:
148        is_push_enabled = False
149    else:
150        if image_names:
151            is_push_enabled = True
152            _warn_unsupported_image_name(image_names)
153        else:
154            is_push_enabled = False
155            logger.warning("'--image or -t' is not provided. Skipping image push after build.")
156    return is_push_enabled
157
158
159def _check_local_docker_file(docker_file_path):
160    if not os.path.isfile(docker_file_path):
161        raise CLIError("Unable to find '{}'.".format(docker_file_path))
162