xref: /aosp_15_r20/external/google-cloud-java/owl-bot-postprocessor/synthtool/gcp/artman.py (revision 55e87721aa1bc457b326496a7ca40f3ea1a63287)
1# Copyright 2018 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import functools
16import json
17import os
18import platform
19import tempfile
20
21from synthtool import metadata, shell
22from synthtool.log import logger
23
24ARTMAN_VERSION = os.environ.get("SYNTHTOOL_ARTMAN_VERSION", "latest")
25
26
27class Artman:
28    def __init__(self):
29        # Docker on mac by default cannot use the default temp file location
30        # # instead use the more standard *nix /tmp location\
31        if platform.system() == "Darwin":
32            tempfile.tempdir = "/tmp"
33        self._ensure_dependencies_installed()
34        self._install_artman()
35        self._report_metadata()
36
37    @functools.lru_cache()
38    def _docker_image_info(self):
39        result = shell.run(
40            ["docker", "inspect", f"googleapis/artman:{ARTMAN_VERSION}"],
41            hide_output=True,
42        )
43        return json.loads(result.stdout)[0]
44
45    @property
46    def version(self) -> str:
47        # The artman version is hidden in the container's environment variables.
48        # We could just docker run `artman --version`, but we already have the
49        # container info so why not? This is faster as it saves us an exec().
50        env_vars = dict(
51            value.split("=", 1) for value in self._docker_image_info()["Config"]["Env"]
52        )
53
54        return env_vars.get("ARTMAN_VERSION", "unknown")
55
56    @property
57    def docker_image(self) -> str:
58        return self._docker_image_info()["RepoDigests"][0]
59
60    def run(
61        self, image, root_dir, config, *args, generator_dir=None, generator_args=None
62    ):
63        """Executes artman command in the artman container.
64
65        Args:
66            image:
67                The Docker image for artman.
68            root_dir:
69                The input directory that will be mounted to artman docker
70                container as local googleapis directory.
71            config:
72                Path to artman configuration YAML file.
73            *args:
74                Arguments to artman that follow ``generate``. Defines which
75                artifacts to generate.
76            generator_dir (Optional[str]):
77                Path to local gapic-generator directory to use for generation.
78                By default, the latest version of gapic-generator will be used.
79            generator_args (Optional[List[str]]):
80                Additional arguments to pass to the gapic generator, such as
81                ``--dev_samples``.
82        Returns:
83            The output directory with artman-generated files.
84        """
85        container_name = "artman-docker"
86        output_dir = root_dir / "artman-genfiles"
87
88        additional_flags = []
89
90        if generator_args:
91            additional_flags.append(
92                "--generator-args='{}'".format(" ".join(generator_args))
93            )
94
95        docker_cmd = ["docker", "run", "--name", container_name, "--rm", "-i"]
96
97        # Environment variables
98        docker_cmd.extend(
99            [
100                "-e",
101                f"HOST_USER_ID={os.getuid()}",
102                "-e",
103                f"HOST_GROUP_ID={os.getgid()}",
104                "-e",
105                "RUNNING_IN_ARTMAN_DOCKER=True",
106            ]
107        )
108
109        # Local directories to mount as volumes (and set working directory -w)
110        docker_cmd.extend(
111            [
112                "-v",
113                f"{root_dir}:{root_dir}",
114                "-v",
115                f"{output_dir}:{output_dir}",
116                "-w",
117                root_dir,
118            ]
119        )
120
121        # Use local copy of GAPIC generator to generate, if path provided
122        if generator_dir:
123            docker_cmd.extend(["-v", f"{generator_dir}:/toolkit"])
124
125        # Run /bin/bash in the image and then provide the shell command to run
126        docker_cmd.extend([image, "/bin/bash", "-c"])
127
128        artman_command = " ".join(
129            map(
130                str,
131                ["artman", "--local", "--config", config]
132                + additional_flags
133                + ["generate"]
134                + list(args),
135            )
136        )
137
138        cmd = docker_cmd + [artman_command]
139
140        shell.run(cmd, cwd=root_dir)
141
142        return output_dir
143
144    def _ensure_dependencies_installed(self):
145        logger.debug("Ensuring dependencies.")
146
147        dependencies = ["docker", "git"]
148        failed_dependencies = []
149        for dependency in dependencies:
150            return_code = shell.run(["which", dependency], check=False).returncode
151            if return_code:
152                failed_dependencies.append(dependency)
153
154        if failed_dependencies:
155            raise EnvironmentError(
156                f"Dependencies missing: {', '.join(failed_dependencies)}"
157            )
158
159    def _install_artman(self):
160        logger.debug("Pulling artman image.")
161        shell.run(
162            ["docker", "pull", f"googleapis/artman:{ARTMAN_VERSION}"], hide_output=False
163        )
164
165    def _report_metadata(self):
166        metadata.add_generator_source(
167            name="artman", version=self.version, docker_image=self.docker_image
168        )
169