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