14112aff7SAlex Bennée#!/usr/bin/env python3 24485b04bSFam Zheng# 34485b04bSFam Zheng# Docker controlling module 44485b04bSFam Zheng# 54485b04bSFam Zheng# Copyright (c) 2016 Red Hat Inc. 64485b04bSFam Zheng# 74485b04bSFam Zheng# Authors: 84485b04bSFam Zheng# Fam Zheng <famz@redhat.com> 94485b04bSFam Zheng# 104485b04bSFam Zheng# This work is licensed under the terms of the GNU GPL, version 2 114485b04bSFam Zheng# or (at your option) any later version. See the COPYING file in 124485b04bSFam Zheng# the top-level directory. 134485b04bSFam Zheng 144485b04bSFam Zhengimport os 154485b04bSFam Zhengimport sys 164485b04bSFam Zhengimport subprocess 174485b04bSFam Zhengimport json 184485b04bSFam Zhengimport hashlib 194485b04bSFam Zhengimport atexit 204485b04bSFam Zhengimport uuid 21ae68fdabSEduardo Habkostimport argparse 229459f754SMarc-André Lureauimport enum 234485b04bSFam Zhengimport tempfile 24504ca3c2SAlex Bennéeimport re 2597cba1a1SFam Zhengimport signal 266e733da6SAlex Bennéefrom tarfile import TarFile, TarInfo 277a5d936bSAlex Bennéefrom io import StringIO 28a9f8d038SAlex Bennéefrom shutil import copy, rmtree 29414a8ce5SAlex Bennéefrom pwd import getpwuid 307b882245SAlex Bennéefrom datetime import datetime, timedelta 314485b04bSFam Zheng 32c9772570SSascha Silbe 3306cc3551SPhilippe Mathieu-DaudéFILTERED_ENV_NAMES = ['ftp_proxy', 'http_proxy', 'https_proxy'] 3406cc3551SPhilippe Mathieu-Daudé 3506cc3551SPhilippe Mathieu-Daudé 36c9772570SSascha SilbeDEVNULL = open(os.devnull, 'wb') 37c9772570SSascha Silbe 389459f754SMarc-André Lureauclass EngineEnum(enum.IntEnum): 399459f754SMarc-André Lureau AUTO = 1 409459f754SMarc-André Lureau DOCKER = 2 419459f754SMarc-André Lureau PODMAN = 3 429459f754SMarc-André Lureau 439459f754SMarc-André Lureau def __str__(self): 449459f754SMarc-André Lureau return self.name.lower() 459459f754SMarc-André Lureau 469459f754SMarc-André Lureau def __repr__(self): 479459f754SMarc-André Lureau return str(self) 489459f754SMarc-André Lureau 499459f754SMarc-André Lureau @staticmethod 509459f754SMarc-André Lureau def argparse(s): 519459f754SMarc-André Lureau try: 529459f754SMarc-André Lureau return EngineEnum[s.upper()] 539459f754SMarc-André Lureau except KeyError: 549459f754SMarc-André Lureau return s 559459f754SMarc-André Lureau 569459f754SMarc-André Lureau 579459f754SMarc-André LureauUSE_ENGINE = EngineEnum.AUTO 58c9772570SSascha Silbe 594485b04bSFam Zhengdef _text_checksum(text): 604485b04bSFam Zheng """Calculate a digest string unique to the text content""" 614112aff7SAlex Bennée return hashlib.sha1(text.encode('utf-8')).hexdigest() 624485b04bSFam Zheng 634112aff7SAlex Bennéedef _read_dockerfile(path): 644112aff7SAlex Bennée return open(path, 'rt', encoding='utf-8').read() 65432d8ad5SAlex Bennée 66438d1168SPhilippe Mathieu-Daudédef _file_checksum(filename): 674112aff7SAlex Bennée return _text_checksum(_read_dockerfile(filename)) 68438d1168SPhilippe Mathieu-Daudé 69432d8ad5SAlex Bennée 709459f754SMarc-André Lureaudef _guess_engine_command(): 719459f754SMarc-André Lureau """ Guess a working engine command or raise exception if not found""" 729459f754SMarc-André Lureau commands = [] 739459f754SMarc-André Lureau 749459f754SMarc-André Lureau if USE_ENGINE in [EngineEnum.AUTO, EngineEnum.PODMAN]: 759459f754SMarc-André Lureau commands += [["podman"]] 769459f754SMarc-André Lureau if USE_ENGINE in [EngineEnum.AUTO, EngineEnum.DOCKER]: 779459f754SMarc-André Lureau commands += [["docker"], ["sudo", "-n", "docker"]] 784485b04bSFam Zheng for cmd in commands: 790679f98bSEduardo Habkost try: 8083405c45SAlex Bennée # docker version will return the client details in stdout 8183405c45SAlex Bennée # but still report a status of 1 if it can't contact the daemon 8283405c45SAlex Bennée if subprocess.call(cmd + ["version"], 83c9772570SSascha Silbe stdout=DEVNULL, stderr=DEVNULL) == 0: 844485b04bSFam Zheng return cmd 850679f98bSEduardo Habkost except OSError: 860679f98bSEduardo Habkost pass 874485b04bSFam Zheng commands_txt = "\n".join([" " + " ".join(x) for x in commands]) 889459f754SMarc-André Lureau raise Exception("Cannot find working engine command. Tried:\n%s" % 894485b04bSFam Zheng commands_txt) 904485b04bSFam Zheng 91432d8ad5SAlex Bennée 922499ee9fSPhilippe Mathieu-Daudédef _copy_with_mkdir(src, root_dir, sub_path='.'): 93504ca3c2SAlex Bennée """Copy src into root_dir, creating sub_path as needed.""" 94504ca3c2SAlex Bennée dest_dir = os.path.normpath("%s/%s" % (root_dir, sub_path)) 95504ca3c2SAlex Bennée try: 96504ca3c2SAlex Bennée os.makedirs(dest_dir) 97504ca3c2SAlex Bennée except OSError: 98504ca3c2SAlex Bennée # we can safely ignore already created directories 99504ca3c2SAlex Bennée pass 100504ca3c2SAlex Bennée 101504ca3c2SAlex Bennée dest_file = "%s/%s" % (dest_dir, os.path.basename(src)) 102504ca3c2SAlex Bennée copy(src, dest_file) 103504ca3c2SAlex Bennée 104504ca3c2SAlex Bennée 105504ca3c2SAlex Bennéedef _get_so_libs(executable): 106504ca3c2SAlex Bennée """Return a list of libraries associated with an executable. 107504ca3c2SAlex Bennée 108504ca3c2SAlex Bennée The paths may be symbolic links which would need to be resolved to 109504ca3c2SAlex Bennée ensure theright data is copied.""" 110504ca3c2SAlex Bennée 111504ca3c2SAlex Bennée libs = [] 112504ca3c2SAlex Bennée ldd_re = re.compile(r"(/.*/)(\S*)") 113504ca3c2SAlex Bennée try: 114eea2153eSAlex Bennée ldd_output = subprocess.check_output(["ldd", executable]).decode('utf-8') 115504ca3c2SAlex Bennée for line in ldd_output.split("\n"): 116504ca3c2SAlex Bennée search = ldd_re.search(line) 117504ca3c2SAlex Bennée if search and len(search.groups()) == 2: 118504ca3c2SAlex Bennée so_path = search.groups()[0] 119504ca3c2SAlex Bennée so_lib = search.groups()[1] 120504ca3c2SAlex Bennée libs.append("%s/%s" % (so_path, so_lib)) 121504ca3c2SAlex Bennée except subprocess.CalledProcessError: 122f03868bdSEduardo Habkost print("%s had no associated libraries (static build?)" % (executable)) 123504ca3c2SAlex Bennée 124504ca3c2SAlex Bennée return libs 125504ca3c2SAlex Bennée 126432d8ad5SAlex Bennée 127d10404b1SAlex Bennéedef _copy_binary_with_libs(src, bin_dest, dest_dir): 128d10404b1SAlex Bennée """Maybe copy a binary and all its dependent libraries. 129d10404b1SAlex Bennée 130d10404b1SAlex Bennée If bin_dest isn't set we only copy the support libraries because 131d10404b1SAlex Bennée we don't need qemu in the docker path to run (due to persistent 132d10404b1SAlex Bennée mapping). Indeed users may get confused if we aren't running what 133d10404b1SAlex Bennée is in the image. 134504ca3c2SAlex Bennée 135504ca3c2SAlex Bennée This does rely on the host file-system being fairly multi-arch 136d10404b1SAlex Bennée aware so the file don't clash with the guests layout. 137d10404b1SAlex Bennée """ 138504ca3c2SAlex Bennée 139d10404b1SAlex Bennée if bin_dest: 140d10404b1SAlex Bennée _copy_with_mkdir(src, dest_dir, os.path.dirname(bin_dest)) 141d10404b1SAlex Bennée else: 142d10404b1SAlex Bennée print("only copying support libraries for %s" % (src)) 143504ca3c2SAlex Bennée 144504ca3c2SAlex Bennée libs = _get_so_libs(src) 145504ca3c2SAlex Bennée if libs: 146504ca3c2SAlex Bennée for l in libs: 147504ca3c2SAlex Bennée so_path = os.path.dirname(l) 148504ca3c2SAlex Bennée _copy_with_mkdir(l, dest_dir, so_path) 149504ca3c2SAlex Bennée 15015352decSAlex Bennée 15115352decSAlex Bennéedef _check_binfmt_misc(executable): 15215352decSAlex Bennée """Check binfmt_misc has entry for executable in the right place. 15315352decSAlex Bennée 15415352decSAlex Bennée The details of setting up binfmt_misc are outside the scope of 15515352decSAlex Bennée this script but we should at least fail early with a useful 156d10404b1SAlex Bennée message if it won't work. 157d10404b1SAlex Bennée 158d10404b1SAlex Bennée Returns the configured binfmt path and a valid flag. For 159d10404b1SAlex Bennée persistent configurations we will still want to copy and dependent 160d10404b1SAlex Bennée libraries. 161d10404b1SAlex Bennée """ 16215352decSAlex Bennée 16315352decSAlex Bennée binary = os.path.basename(executable) 16415352decSAlex Bennée binfmt_entry = "/proc/sys/fs/binfmt_misc/%s" % (binary) 16515352decSAlex Bennée 16615352decSAlex Bennée if not os.path.exists(binfmt_entry): 16715352decSAlex Bennée print ("No binfmt_misc entry for %s" % (binary)) 168d10404b1SAlex Bennée return None, False 16915352decSAlex Bennée 17015352decSAlex Bennée with open(binfmt_entry) as x: entry = x.read() 17115352decSAlex Bennée 17243c898b7SAlex Bennée if re.search("flags:.*F.*\n", entry): 173432d8ad5SAlex Bennée print("binfmt_misc for %s uses persistent(F) mapping to host binary" % 17443c898b7SAlex Bennée (binary)) 175d10404b1SAlex Bennée return None, True 17643c898b7SAlex Bennée 1777e81d198SAlex Bennée m = re.search("interpreter (\S+)\n", entry) 1787e81d198SAlex Bennée interp = m.group(1) 1797e81d198SAlex Bennée if interp and interp != executable: 1807e81d198SAlex Bennée print("binfmt_misc for %s does not point to %s, using %s" % 1817e81d198SAlex Bennée (binary, executable, interp)) 18215352decSAlex Bennée 183d10404b1SAlex Bennée return interp, True 184d10404b1SAlex Bennée 18515352decSAlex Bennée 186c1958e9dSFam Zhengdef _read_qemu_dockerfile(img_name): 187547cb45eSAlex Bennée # special case for Debian linux-user images 188547cb45eSAlex Bennée if img_name.startswith("debian") and img_name.endswith("user"): 189547cb45eSAlex Bennée img_name = "debian-bootstrap" 190547cb45eSAlex Bennée 191c1958e9dSFam Zheng df = os.path.join(os.path.dirname(__file__), "dockerfiles", 192c1958e9dSFam Zheng img_name + ".docker") 1934112aff7SAlex Bennée return _read_dockerfile(df) 194c1958e9dSFam Zheng 195432d8ad5SAlex Bennée 196c1958e9dSFam Zhengdef _dockerfile_preprocess(df): 197c1958e9dSFam Zheng out = "" 198c1958e9dSFam Zheng for l in df.splitlines(): 199c1958e9dSFam Zheng if len(l.strip()) == 0 or l.startswith("#"): 200c1958e9dSFam Zheng continue 201c1958e9dSFam Zheng from_pref = "FROM qemu:" 202c1958e9dSFam Zheng if l.startswith(from_pref): 203c1958e9dSFam Zheng # TODO: Alternatively we could replace this line with "FROM $ID" 204c1958e9dSFam Zheng # where $ID is the image's hex id obtained with 205c1958e9dSFam Zheng # $ docker images $IMAGE --format="{{.Id}}" 206c1958e9dSFam Zheng # but unfortunately that's not supported by RHEL 7. 207c1958e9dSFam Zheng inlining = _read_qemu_dockerfile(l[len(from_pref):]) 208c1958e9dSFam Zheng out += _dockerfile_preprocess(inlining) 209c1958e9dSFam Zheng continue 210c1958e9dSFam Zheng out += l + "\n" 211c1958e9dSFam Zheng return out 212c1958e9dSFam Zheng 213432d8ad5SAlex Bennée 2144485b04bSFam Zhengclass Docker(object): 2154485b04bSFam Zheng """ Running Docker commands """ 2164485b04bSFam Zheng def __init__(self): 2179459f754SMarc-André Lureau self._command = _guess_engine_command() 218529994e2SAlex Bennée self._instance = None 2194485b04bSFam Zheng atexit.register(self._kill_instances) 22097cba1a1SFam Zheng signal.signal(signal.SIGTERM, self._kill_instances) 22197cba1a1SFam Zheng signal.signal(signal.SIGHUP, self._kill_instances) 2224485b04bSFam Zheng 22358bf7b6dSFam Zheng def _do(self, cmd, quiet=True, **kwargs): 2244485b04bSFam Zheng if quiet: 225c9772570SSascha Silbe kwargs["stdout"] = DEVNULL 2264485b04bSFam Zheng return subprocess.call(self._command + cmd, **kwargs) 2274485b04bSFam Zheng 2280b95ff72SFam Zheng def _do_check(self, cmd, quiet=True, **kwargs): 2290b95ff72SFam Zheng if quiet: 2300b95ff72SFam Zheng kwargs["stdout"] = DEVNULL 2310b95ff72SFam Zheng return subprocess.check_call(self._command + cmd, **kwargs) 2320b95ff72SFam Zheng 2334485b04bSFam Zheng def _do_kill_instances(self, only_known, only_active=True): 2344485b04bSFam Zheng cmd = ["ps", "-q"] 2354485b04bSFam Zheng if not only_active: 2364485b04bSFam Zheng cmd.append("-a") 237529994e2SAlex Bennée 238529994e2SAlex Bennée filter = "--filter=label=com.qemu.instance.uuid" 239529994e2SAlex Bennée if only_known: 240529994e2SAlex Bennée if self._instance: 241529994e2SAlex Bennée filter += "=%s" % (self._instance) 242529994e2SAlex Bennée else: 243529994e2SAlex Bennée # no point trying to kill, we finished 244529994e2SAlex Bennée return 245529994e2SAlex Bennée 246529994e2SAlex Bennée print("filter=%s" % (filter)) 247529994e2SAlex Bennée cmd.append(filter) 2484485b04bSFam Zheng for i in self._output(cmd).split(): 249529994e2SAlex Bennée self._do(["rm", "-f", i]) 2504485b04bSFam Zheng 2514485b04bSFam Zheng def clean(self): 2524485b04bSFam Zheng self._do_kill_instances(False, False) 2534485b04bSFam Zheng return 0 2544485b04bSFam Zheng 25597cba1a1SFam Zheng def _kill_instances(self, *args, **kwargs): 2564485b04bSFam Zheng return self._do_kill_instances(True) 2574485b04bSFam Zheng 2584485b04bSFam Zheng def _output(self, cmd, **kwargs): 259884fcafcSAlex Bennée if sys.version_info[1] >= 6: 2604485b04bSFam Zheng return subprocess.check_output(self._command + cmd, 2614485b04bSFam Zheng stderr=subprocess.STDOUT, 2624112aff7SAlex Bennée encoding='utf-8', 2634485b04bSFam Zheng **kwargs) 264884fcafcSAlex Bennée else: 265884fcafcSAlex Bennée return subprocess.check_output(self._command + cmd, 266884fcafcSAlex Bennée stderr=subprocess.STDOUT, 267884fcafcSAlex Bennée **kwargs).decode('utf-8') 268884fcafcSAlex Bennée 2694485b04bSFam Zheng 270f97da1f7SAlex Bennée def inspect_tag(self, tag): 271f97da1f7SAlex Bennée try: 272f97da1f7SAlex Bennée return self._output(["inspect", tag]) 273f97da1f7SAlex Bennée except subprocess.CalledProcessError: 274f97da1f7SAlex Bennée return None 275f97da1f7SAlex Bennée 2767b882245SAlex Bennée def get_image_creation_time(self, info): 2777b882245SAlex Bennée return json.loads(info)[0]["Created"] 2787b882245SAlex Bennée 2794485b04bSFam Zheng def get_image_dockerfile_checksum(self, tag): 280f97da1f7SAlex Bennée resp = self.inspect_tag(tag) 2814485b04bSFam Zheng labels = json.loads(resp)[0]["Config"].get("Labels", {}) 2824485b04bSFam Zheng return labels.get("com.qemu.dockerfile-checksum", "") 2834485b04bSFam Zheng 284414a8ce5SAlex Bennée def build_image(self, tag, docker_dir, dockerfile, 285438d1168SPhilippe Mathieu-Daudé quiet=True, user=False, argv=None, extra_files_cksum=[]): 286432d8ad5SAlex Bennée if argv is None: 2874485b04bSFam Zheng argv = [] 2884485b04bSFam Zheng 2894112aff7SAlex Bennée tmp_df = tempfile.NamedTemporaryFile(mode="w+t", 2904112aff7SAlex Bennée encoding='utf-8', 2914112aff7SAlex Bennée dir=docker_dir, suffix=".docker") 2924485b04bSFam Zheng tmp_df.write(dockerfile) 2934485b04bSFam Zheng 294414a8ce5SAlex Bennée if user: 295414a8ce5SAlex Bennée uid = os.getuid() 296414a8ce5SAlex Bennée uname = getpwuid(uid).pw_name 297414a8ce5SAlex Bennée tmp_df.write("\n") 298414a8ce5SAlex Bennée tmp_df.write("RUN id %s 2>/dev/null || useradd -u %d -U %s" % 299414a8ce5SAlex Bennée (uname, uid, uname)) 300414a8ce5SAlex Bennée 3014485b04bSFam Zheng tmp_df.write("\n") 3024485b04bSFam Zheng tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s" % 303f9172822SAlex Bennée _text_checksum(_dockerfile_preprocess(dockerfile))) 304f9172822SAlex Bennée for f, c in extra_files_cksum: 305f9172822SAlex Bennée tmp_df.write("LABEL com.qemu.%s-checksum=%s" % (f, c)) 306f9172822SAlex Bennée 3074485b04bSFam Zheng tmp_df.flush() 308a9f8d038SAlex Bennée 309432d8ad5SAlex Bennée self._do_check(["build", "-t", tag, "-f", tmp_df.name] + argv + 310a9f8d038SAlex Bennée [docker_dir], 3114485b04bSFam Zheng quiet=quiet) 3124485b04bSFam Zheng 3136e733da6SAlex Bennée def update_image(self, tag, tarball, quiet=True): 3146e733da6SAlex Bennée "Update a tagged image using " 3156e733da6SAlex Bennée 3160b95ff72SFam Zheng self._do_check(["build", "-t", tag, "-"], quiet=quiet, stdin=tarball) 3176e733da6SAlex Bennée 3184485b04bSFam Zheng def image_matches_dockerfile(self, tag, dockerfile): 3194485b04bSFam Zheng try: 3204485b04bSFam Zheng checksum = self.get_image_dockerfile_checksum(tag) 3214485b04bSFam Zheng except Exception: 3224485b04bSFam Zheng return False 323c1958e9dSFam Zheng return checksum == _text_checksum(_dockerfile_preprocess(dockerfile)) 3244485b04bSFam Zheng 32571ebbe09SAlex Bennée def run(self, cmd, keep, quiet, as_user=False): 326529994e2SAlex Bennée label = uuid.uuid4().hex 3274485b04bSFam Zheng if not keep: 328529994e2SAlex Bennée self._instance = label 32971ebbe09SAlex Bennée 33071ebbe09SAlex Bennée if as_user: 33171ebbe09SAlex Bennée uid = os.getuid() 33271ebbe09SAlex Bennée cmd = [ "-u", str(uid) ] + cmd 33371ebbe09SAlex Bennée # podman requires a bit more fiddling 33471ebbe09SAlex Bennée if self._command[0] == "podman": 335*b3a790beSJohn Snow cmd.insert(0, '--userns=keep-id') 33671ebbe09SAlex Bennée 3370b95ff72SFam Zheng ret = self._do_check(["run", "--label", 3384485b04bSFam Zheng "com.qemu.instance.uuid=" + label] + cmd, 3394485b04bSFam Zheng quiet=quiet) 3404485b04bSFam Zheng if not keep: 341529994e2SAlex Bennée self._instance = None 3424485b04bSFam Zheng return ret 3434485b04bSFam Zheng 3444b08af60SFam Zheng def command(self, cmd, argv, quiet): 3454b08af60SFam Zheng return self._do([cmd] + argv, quiet=quiet) 3464b08af60SFam Zheng 347432d8ad5SAlex Bennée 3484485b04bSFam Zhengclass SubCommand(object): 3494485b04bSFam Zheng """A SubCommand template base class""" 3504485b04bSFam Zheng name = None # Subcommand name 351432d8ad5SAlex Bennée 3524485b04bSFam Zheng def shared_args(self, parser): 3534485b04bSFam Zheng parser.add_argument("--quiet", action="store_true", 354e50a6121SStefan Weil help="Run quietly unless an error occurred") 3554485b04bSFam Zheng 3564485b04bSFam Zheng def args(self, parser): 3574485b04bSFam Zheng """Setup argument parser""" 3584485b04bSFam Zheng pass 359432d8ad5SAlex Bennée 3604485b04bSFam Zheng def run(self, args, argv): 3614485b04bSFam Zheng """Run command. 3624485b04bSFam Zheng args: parsed argument by argument parser. 3634485b04bSFam Zheng argv: remaining arguments from sys.argv. 3644485b04bSFam Zheng """ 3654485b04bSFam Zheng pass 3664485b04bSFam Zheng 367432d8ad5SAlex Bennée 3684485b04bSFam Zhengclass RunCommand(SubCommand): 3694485b04bSFam Zheng """Invoke docker run and take care of cleaning up""" 3704485b04bSFam Zheng name = "run" 371432d8ad5SAlex Bennée 3724485b04bSFam Zheng def args(self, parser): 3734485b04bSFam Zheng parser.add_argument("--keep", action="store_true", 3744485b04bSFam Zheng help="Don't remove image when command completes") 3752461d80eSMarc-André Lureau parser.add_argument("--run-as-current-user", action="store_true", 3762461d80eSMarc-André Lureau help="Run container using the current user's uid") 377432d8ad5SAlex Bennée 3784485b04bSFam Zheng def run(self, args, argv): 37971ebbe09SAlex Bennée return Docker().run(argv, args.keep, quiet=args.quiet, 38071ebbe09SAlex Bennée as_user=args.run_as_current_user) 3814485b04bSFam Zheng 382432d8ad5SAlex Bennée 3834485b04bSFam Zhengclass BuildCommand(SubCommand): 384432d8ad5SAlex Bennée """ Build docker image out of a dockerfile. Arg: <tag> <dockerfile>""" 3854485b04bSFam Zheng name = "build" 386432d8ad5SAlex Bennée 3874485b04bSFam Zheng def args(self, parser): 388504ca3c2SAlex Bennée parser.add_argument("--include-executable", "-e", 389504ca3c2SAlex Bennée help="""Specify a binary that will be copied to the 390504ca3c2SAlex Bennée container together with all its dependent 391504ca3c2SAlex Bennée libraries""") 3924c84f662SPhilippe Mathieu-Daudé parser.add_argument("--extra-files", "-f", nargs='*', 3934c84f662SPhilippe Mathieu-Daudé help="""Specify files that will be copied in the 3944c84f662SPhilippe Mathieu-Daudé Docker image, fulfilling the ADD directive from the 3954c84f662SPhilippe Mathieu-Daudé Dockerfile""") 396414a8ce5SAlex Bennée parser.add_argument("--add-current-user", "-u", dest="user", 397414a8ce5SAlex Bennée action="store_true", 398414a8ce5SAlex Bennée help="Add the current user to image's passwd") 3994485b04bSFam Zheng parser.add_argument("tag", 4004485b04bSFam Zheng help="Image Tag") 4014485b04bSFam Zheng parser.add_argument("dockerfile", 4024485b04bSFam Zheng help="Dockerfile name") 4034485b04bSFam Zheng 4044485b04bSFam Zheng def run(self, args, argv): 4054112aff7SAlex Bennée dockerfile = _read_dockerfile(args.dockerfile) 4064485b04bSFam Zheng tag = args.tag 4074485b04bSFam Zheng 4084485b04bSFam Zheng dkr = Docker() 4096fe3ae3fSAlex Bennée if "--no-cache" not in argv and \ 4106fe3ae3fSAlex Bennée dkr.image_matches_dockerfile(tag, dockerfile): 4114485b04bSFam Zheng if not args.quiet: 412f03868bdSEduardo Habkost print("Image is up to date.") 413a9f8d038SAlex Bennée else: 414a9f8d038SAlex Bennée # Create a docker context directory for the build 415a9f8d038SAlex Bennée docker_dir = tempfile.mkdtemp(prefix="docker_build") 4164485b04bSFam Zheng 41715352decSAlex Bennée # Validate binfmt_misc will work 41815352decSAlex Bennée if args.include_executable: 419d10404b1SAlex Bennée qpath, enabled = _check_binfmt_misc(args.include_executable) 420d10404b1SAlex Bennée if not enabled: 42115352decSAlex Bennée return 1 42215352decSAlex Bennée 423920776eaSAlex Bennée # Is there a .pre file to run in the build context? 424920776eaSAlex Bennée docker_pre = os.path.splitext(args.dockerfile)[0]+".pre" 425920776eaSAlex Bennée if os.path.exists(docker_pre): 426f8042deaSSascha Silbe stdout = DEVNULL if args.quiet else None 427920776eaSAlex Bennée rc = subprocess.call(os.path.realpath(docker_pre), 428f8042deaSSascha Silbe cwd=docker_dir, stdout=stdout) 429920776eaSAlex Bennée if rc == 3: 430f03868bdSEduardo Habkost print("Skip") 431920776eaSAlex Bennée return 0 432920776eaSAlex Bennée elif rc != 0: 433f03868bdSEduardo Habkost print("%s exited with code %d" % (docker_pre, rc)) 434920776eaSAlex Bennée return 1 435920776eaSAlex Bennée 4364c84f662SPhilippe Mathieu-Daudé # Copy any extra files into the Docker context. These can be 4374c84f662SPhilippe Mathieu-Daudé # included by the use of the ADD directive in the Dockerfile. 438438d1168SPhilippe Mathieu-Daudé cksum = [] 439504ca3c2SAlex Bennée if args.include_executable: 440438d1168SPhilippe Mathieu-Daudé # FIXME: there is no checksum of this executable and the linked 441438d1168SPhilippe Mathieu-Daudé # libraries, once the image built any change of this executable 442438d1168SPhilippe Mathieu-Daudé # or any library won't trigger another build. 443d10404b1SAlex Bennée _copy_binary_with_libs(args.include_executable, 444d10404b1SAlex Bennée qpath, docker_dir) 445d10404b1SAlex Bennée 4464c84f662SPhilippe Mathieu-Daudé for filename in args.extra_files or []: 4474c84f662SPhilippe Mathieu-Daudé _copy_with_mkdir(filename, docker_dir) 448f9172822SAlex Bennée cksum += [(filename, _file_checksum(filename))] 449504ca3c2SAlex Bennée 45006cc3551SPhilippe Mathieu-Daudé argv += ["--build-arg=" + k.lower() + "=" + v 4514112aff7SAlex Bennée for k, v in os.environ.items() 45206cc3551SPhilippe Mathieu-Daudé if k.lower() in FILTERED_ENV_NAMES] 453a9f8d038SAlex Bennée dkr.build_image(tag, docker_dir, dockerfile, 454438d1168SPhilippe Mathieu-Daudé quiet=args.quiet, user=args.user, argv=argv, 455438d1168SPhilippe Mathieu-Daudé extra_files_cksum=cksum) 456a9f8d038SAlex Bennée 457a9f8d038SAlex Bennée rmtree(docker_dir) 458a9f8d038SAlex Bennée 4594485b04bSFam Zheng return 0 4604485b04bSFam Zheng 461432d8ad5SAlex Bennée 4626e733da6SAlex Bennéeclass UpdateCommand(SubCommand): 463432d8ad5SAlex Bennée """ Update a docker image with new executables. Args: <tag> <executable>""" 4646e733da6SAlex Bennée name = "update" 465432d8ad5SAlex Bennée 4666e733da6SAlex Bennée def args(self, parser): 4676e733da6SAlex Bennée parser.add_argument("tag", 4686e733da6SAlex Bennée help="Image Tag") 4696e733da6SAlex Bennée parser.add_argument("executable", 4706e733da6SAlex Bennée help="Executable to copy") 4716e733da6SAlex Bennée 4726e733da6SAlex Bennée def run(self, args, argv): 4736e733da6SAlex Bennée # Create a temporary tarball with our whole build context and 4746e733da6SAlex Bennée # dockerfile for the update 4756e733da6SAlex Bennée tmp = tempfile.NamedTemporaryFile(suffix="dckr.tar.gz") 4766e733da6SAlex Bennée tmp_tar = TarFile(fileobj=tmp, mode='w') 4776e733da6SAlex Bennée 4787e81d198SAlex Bennée # Add the executable to the tarball, using the current 479d10404b1SAlex Bennée # configured binfmt_misc path. If we don't get a path then we 480d10404b1SAlex Bennée # only need the support libraries copied 481d10404b1SAlex Bennée ff, enabled = _check_binfmt_misc(args.executable) 4827e81d198SAlex Bennée 483d10404b1SAlex Bennée if not enabled: 484d10404b1SAlex Bennée print("binfmt_misc not enabled, update disabled") 485d10404b1SAlex Bennée return 1 486d10404b1SAlex Bennée 487d10404b1SAlex Bennée if ff: 4886e733da6SAlex Bennée tmp_tar.add(args.executable, arcname=ff) 4896e733da6SAlex Bennée 4906e733da6SAlex Bennée # Add any associated libraries 4916e733da6SAlex Bennée libs = _get_so_libs(args.executable) 4926e733da6SAlex Bennée if libs: 4936e733da6SAlex Bennée for l in libs: 4946e733da6SAlex Bennée tmp_tar.add(os.path.realpath(l), arcname=l) 4956e733da6SAlex Bennée 4966e733da6SAlex Bennée # Create a Docker buildfile 4976e733da6SAlex Bennée df = StringIO() 4986e733da6SAlex Bennée df.write("FROM %s\n" % args.tag) 4996e733da6SAlex Bennée df.write("ADD . /\n") 5006e733da6SAlex Bennée df.seek(0) 5016e733da6SAlex Bennée 5026e733da6SAlex Bennée df_tar = TarInfo(name="Dockerfile") 5036e733da6SAlex Bennée df_tar.size = len(df.buf) 5046e733da6SAlex Bennée tmp_tar.addfile(df_tar, fileobj=df) 5056e733da6SAlex Bennée 5066e733da6SAlex Bennée tmp_tar.close() 5076e733da6SAlex Bennée 5086e733da6SAlex Bennée # reset the file pointers 5096e733da6SAlex Bennée tmp.flush() 5106e733da6SAlex Bennée tmp.seek(0) 5116e733da6SAlex Bennée 5126e733da6SAlex Bennée # Run the build with our tarball context 5136e733da6SAlex Bennée dkr = Docker() 5146e733da6SAlex Bennée dkr.update_image(args.tag, tmp, quiet=args.quiet) 5156e733da6SAlex Bennée 5166e733da6SAlex Bennée return 0 5176e733da6SAlex Bennée 518432d8ad5SAlex Bennée 5194485b04bSFam Zhengclass CleanCommand(SubCommand): 5204485b04bSFam Zheng """Clean up docker instances""" 5214485b04bSFam Zheng name = "clean" 522432d8ad5SAlex Bennée 5234485b04bSFam Zheng def run(self, args, argv): 5244485b04bSFam Zheng Docker().clean() 5254485b04bSFam Zheng return 0 5264485b04bSFam Zheng 527432d8ad5SAlex Bennée 5284b08af60SFam Zhengclass ImagesCommand(SubCommand): 5294b08af60SFam Zheng """Run "docker images" command""" 5304b08af60SFam Zheng name = "images" 531432d8ad5SAlex Bennée 5324b08af60SFam Zheng def run(self, args, argv): 5334b08af60SFam Zheng return Docker().command("images", argv, args.quiet) 5344b08af60SFam Zheng 53515df9d37SAlex Bennée 53615df9d37SAlex Bennéeclass ProbeCommand(SubCommand): 53715df9d37SAlex Bennée """Probe if we can run docker automatically""" 53815df9d37SAlex Bennée name = "probe" 53915df9d37SAlex Bennée 54015df9d37SAlex Bennée def run(self, args, argv): 54115df9d37SAlex Bennée try: 54215df9d37SAlex Bennée docker = Docker() 54315df9d37SAlex Bennée if docker._command[0] == "docker": 5448480517dSAlex Bennée print("docker") 54515df9d37SAlex Bennée elif docker._command[0] == "sudo": 5468480517dSAlex Bennée print("sudo docker") 5479459f754SMarc-André Lureau elif docker._command[0] == "podman": 5489459f754SMarc-André Lureau print("podman") 54915df9d37SAlex Bennée except Exception: 550f03868bdSEduardo Habkost print("no") 55115df9d37SAlex Bennée 55215df9d37SAlex Bennée return 55315df9d37SAlex Bennée 55415df9d37SAlex Bennée 5555e03c2d8SAlex Bennéeclass CcCommand(SubCommand): 5565e03c2d8SAlex Bennée """Compile sources with cc in images""" 5575e03c2d8SAlex Bennée name = "cc" 5585e03c2d8SAlex Bennée 5595e03c2d8SAlex Bennée def args(self, parser): 5605e03c2d8SAlex Bennée parser.add_argument("--image", "-i", required=True, 5615e03c2d8SAlex Bennée help="The docker image in which to run cc") 56299cfdb86SAlex Bennée parser.add_argument("--cc", default="cc", 56399cfdb86SAlex Bennée help="The compiler executable to call") 5645e03c2d8SAlex Bennée parser.add_argument("--source-path", "-s", nargs="*", dest="paths", 5655e03c2d8SAlex Bennée help="""Extra paths to (ro) mount into container for 5665e03c2d8SAlex Bennée reading sources""") 5675e03c2d8SAlex Bennée 5685e03c2d8SAlex Bennée def run(self, args, argv): 5695e03c2d8SAlex Bennée if argv and argv[0] == "--": 5705e03c2d8SAlex Bennée argv = argv[1:] 5715e03c2d8SAlex Bennée cwd = os.getcwd() 5725e03c2d8SAlex Bennée cmd = ["--rm", "-w", cwd, 5735e03c2d8SAlex Bennée "-v", "%s:%s:rw" % (cwd, cwd)] 5745e03c2d8SAlex Bennée if args.paths: 5755e03c2d8SAlex Bennée for p in args.paths: 5765e03c2d8SAlex Bennée cmd += ["-v", "%s:%s:ro,z" % (p, p)] 57799cfdb86SAlex Bennée cmd += [args.image, args.cc] 5785e03c2d8SAlex Bennée cmd += argv 57971ebbe09SAlex Bennée return Docker().run(cmd, False, quiet=args.quiet, 58071ebbe09SAlex Bennée as_user=True) 5815e03c2d8SAlex Bennée 5825e03c2d8SAlex Bennée 583f97da1f7SAlex Bennéeclass CheckCommand(SubCommand): 584f97da1f7SAlex Bennée """Check if we need to re-build a docker image out of a dockerfile. 585f97da1f7SAlex Bennée Arguments: <tag> <dockerfile>""" 586f97da1f7SAlex Bennée name = "check" 587f97da1f7SAlex Bennée 588f97da1f7SAlex Bennée def args(self, parser): 589f97da1f7SAlex Bennée parser.add_argument("tag", 590f97da1f7SAlex Bennée help="Image Tag") 5917b882245SAlex Bennée parser.add_argument("dockerfile", default=None, 5927b882245SAlex Bennée help="Dockerfile name", nargs='?') 5937b882245SAlex Bennée parser.add_argument("--checktype", choices=["checksum", "age"], 5947b882245SAlex Bennée default="checksum", help="check type") 5957b882245SAlex Bennée parser.add_argument("--olderthan", default=60, type=int, 5967b882245SAlex Bennée help="number of minutes") 597f97da1f7SAlex Bennée 598f97da1f7SAlex Bennée def run(self, args, argv): 599f97da1f7SAlex Bennée tag = args.tag 600f97da1f7SAlex Bennée 60143e1b2ffSAlex Bennée try: 602f97da1f7SAlex Bennée dkr = Docker() 603432d8ad5SAlex Bennée except subprocess.CalledProcessError: 60443e1b2ffSAlex Bennée print("Docker not set up") 60543e1b2ffSAlex Bennée return 1 60643e1b2ffSAlex Bennée 607f97da1f7SAlex Bennée info = dkr.inspect_tag(tag) 608f97da1f7SAlex Bennée if info is None: 609f97da1f7SAlex Bennée print("Image does not exist") 610f97da1f7SAlex Bennée return 1 611f97da1f7SAlex Bennée 6127b882245SAlex Bennée if args.checktype == "checksum": 6137b882245SAlex Bennée if not args.dockerfile: 6147b882245SAlex Bennée print("Need a dockerfile for tag:%s" % (tag)) 6157b882245SAlex Bennée return 1 6167b882245SAlex Bennée 6174112aff7SAlex Bennée dockerfile = _read_dockerfile(args.dockerfile) 6187b882245SAlex Bennée 619f97da1f7SAlex Bennée if dkr.image_matches_dockerfile(tag, dockerfile): 620f97da1f7SAlex Bennée if not args.quiet: 621f97da1f7SAlex Bennée print("Image is up to date") 622f97da1f7SAlex Bennée return 0 623f97da1f7SAlex Bennée else: 624f97da1f7SAlex Bennée print("Image needs updating") 625f97da1f7SAlex Bennée return 1 6267b882245SAlex Bennée elif args.checktype == "age": 6277b882245SAlex Bennée timestr = dkr.get_image_creation_time(info).split(".")[0] 6287b882245SAlex Bennée created = datetime.strptime(timestr, "%Y-%m-%dT%H:%M:%S") 6297b882245SAlex Bennée past = datetime.now() - timedelta(minutes=args.olderthan) 6307b882245SAlex Bennée if created < past: 6317b882245SAlex Bennée print ("Image created @ %s more than %d minutes old" % 6327b882245SAlex Bennée (timestr, args.olderthan)) 6337b882245SAlex Bennée return 1 6347b882245SAlex Bennée else: 6357b882245SAlex Bennée if not args.quiet: 6367b882245SAlex Bennée print ("Image less than %d minutes old" % (args.olderthan)) 6377b882245SAlex Bennée return 0 638f97da1f7SAlex Bennée 639f97da1f7SAlex Bennée 6404485b04bSFam Zhengdef main(): 6419459f754SMarc-André Lureau global USE_ENGINE 6429459f754SMarc-André Lureau 6434485b04bSFam Zheng parser = argparse.ArgumentParser(description="A Docker helper", 644432d8ad5SAlex Bennée usage="%s <subcommand> ..." % 645432d8ad5SAlex Bennée os.path.basename(sys.argv[0])) 6469459f754SMarc-André Lureau parser.add_argument("--engine", type=EngineEnum.argparse, choices=list(EngineEnum), 6479459f754SMarc-André Lureau help="specify which container engine to use") 6484485b04bSFam Zheng subparsers = parser.add_subparsers(title="subcommands", help=None) 6494485b04bSFam Zheng for cls in SubCommand.__subclasses__(): 6504485b04bSFam Zheng cmd = cls() 6514485b04bSFam Zheng subp = subparsers.add_parser(cmd.name, help=cmd.__doc__) 6524485b04bSFam Zheng cmd.shared_args(subp) 6534485b04bSFam Zheng cmd.args(subp) 6544485b04bSFam Zheng subp.set_defaults(cmdobj=cmd) 6554485b04bSFam Zheng args, argv = parser.parse_known_args() 6568480517dSAlex Bennée if args.engine: 6579459f754SMarc-André Lureau USE_ENGINE = args.engine 6584485b04bSFam Zheng return args.cmdobj.run(args, argv) 6594485b04bSFam Zheng 660432d8ad5SAlex Bennée 6614485b04bSFam Zhengif __name__ == "__main__": 6624485b04bSFam Zheng sys.exit(main()) 663