| 1 | #!/usr/bin/env python3 |
| 2 | # |
| 3 | # Docker controlling module |
| 4 | # |
| 5 | # Copyright (c) 2016 Red Hat Inc. |
| 6 | # |
| 7 | # Authors: |
| 8 | # Fam Zheng <famz@redhat.com> |
| 9 | # |
| 10 | # This work is licensed under the terms of the GNU GPL, version 2 |
| 11 | # or (at your option) any later version. See the COPYING file in |
| 12 | # the top-level directory. |
| 13 | |
| 14 | import os |
| 15 | import sys |
| 16 | import subprocess |
| 17 | import json |
| 18 | import hashlib |
| 19 | import atexit |
| 20 | import uuid |
| 21 | import argparse |
| 22 | import enum |
| 23 | import tempfile |
| 24 | import re |
| 25 | import signal |
| 26 | import getpass |
| 27 | from tarfile import TarFile, TarInfo |
| 28 | from io import StringIO, BytesIO |
| 29 | from shutil import copy, rmtree |
| 30 | from datetime import datetime, timedelta |
| 31 | |
| 32 | |
| 33 | FILTERED_ENV_NAMES = ['ftp_proxy', 'http_proxy', 'https_proxy'] |
| 34 | |
| 35 | |
| 36 | DEVNULL = open(os.devnull, 'wb') |
| 37 | |
| 38 | def _bytes_checksum(bytes): |
| 39 | """Calculate a digest string unique to the text content""" |
| 40 | return hashlib.sha1(bytes).hexdigest() |
| 41 | |
| 42 | def _text_checksum(text): |
| 43 | """Calculate a digest string unique to the text content""" |
| 44 | return _bytes_checksum(text.encode('utf-8')) |
| 45 | |
| 46 | def _read_dockerfile(path): |
| 47 | return open(path, 'rt', encoding='utf-8').read() |
| 48 | |
| 49 | def _file_checksum(filename): |
| 50 | return _bytes_checksum(open(filename, 'rb').read()) |
| 51 | |
| 52 | |
| 53 | def _guess_engine_command(): |
| 54 | """ Guess a working engine command or raise exception if not found""" |
| 55 | commands = [["podman"], |
| 56 | ["podman-remote"], |
| 57 | ["podman", "--remote"], |
| 58 | ["docker"], |
| 59 | ["sudo", "-n", "docker"]] |
| 60 | for cmd in commands: |
| 61 | try: |
| 62 | # 'version' is not sufficient to prove a working binary |
| 63 | # for podman. 'info' is a stronger check that is more |
| 64 | # likely to correlate with ability to create containers, |
| 65 | # and required to detect the need for podman remote |
| 66 | if subprocess.call(cmd + ["info"], |
| 67 | stdout=DEVNULL, stderr=DEVNULL) == 0: |
| 68 | return cmd |
| 69 | except OSError: |
| 70 | pass |
| 71 | commands_txt = "\n".join([" " + " ".join(x) for x in commands]) |
| 72 | raise Exception("Cannot find working engine command. Tried:\n%s" % |
| 73 | commands_txt) |
| 74 | |
| 75 | |
| 76 | def _copy_with_mkdir(src, root_dir, sub_path='.', name=None): |
| 77 | """Copy src into root_dir, creating sub_path as needed.""" |
| 78 | dest_dir = os.path.normpath("%s/%s" % (root_dir, sub_path)) |
| 79 | try: |
| 80 | os.makedirs(dest_dir) |
| 81 | except OSError: |
| 82 | # we can safely ignore already created directories |
| 83 | pass |
| 84 | |
| 85 | dest_file = "%s/%s" % (dest_dir, name if name else os.path.basename(src)) |
| 86 | |
| 87 | try: |
| 88 | copy(src, dest_file) |
| 89 | except FileNotFoundError: |
| 90 | print("Couldn't copy %s to %s" % (src, dest_file)) |
| 91 | pass |
| 92 | |
| 93 | |
| 94 | def _get_so_libs(executable): |
| 95 | """Return a list of libraries associated with an executable. |
| 96 | |
| 97 | The paths may be symbolic links which would need to be resolved to |
| 98 | ensure the right data is copied.""" |
| 99 | |
| 100 | libs = [] |
| 101 | ldd_re = re.compile(r"(?:\S+ => )?(\S*) \(:?0x[0-9a-f]+\)") |
| 102 | try: |
| 103 | ldd_output = subprocess.check_output(["ldd", executable]).decode('utf-8') |
| 104 | for line in ldd_output.split("\n"): |
| 105 | search = ldd_re.search(line) |
| 106 | if search: |
| 107 | try: |
| 108 | libs.append(search.group(1)) |
| 109 | except IndexError: |
| 110 | pass |
| 111 | except subprocess.CalledProcessError: |
| 112 | print("%s had no associated libraries (static build?)" % (executable)) |
| 113 | |
| 114 | return libs |
| 115 | |
| 116 | |
| 117 | def _copy_binary_with_libs(src, bin_dest, dest_dir): |
| 118 | """Maybe copy a binary and all its dependent libraries. |
| 119 | |
| 120 | If bin_dest isn't set we only copy the support libraries because |
| 121 | we don't need qemu in the docker path to run (due to persistent |
| 122 | mapping). Indeed users may get confused if we aren't running what |
| 123 | is in the image. |
| 124 | |
| 125 | This does rely on the host file-system being fairly multi-arch |
| 126 | aware so the file don't clash with the guests layout. |
| 127 | """ |
| 128 | |
| 129 | if bin_dest: |
| 130 | _copy_with_mkdir(src, dest_dir, os.path.dirname(bin_dest)) |
| 131 | else: |
| 132 | print("only copying support libraries for %s" % (src)) |
| 133 | |
| 134 | libs = _get_so_libs(src) |
| 135 | if libs: |
| 136 | for l in libs: |
| 137 | so_path = os.path.dirname(l) |
| 138 | name = os.path.basename(l) |
| 139 | real_l = os.path.realpath(l) |
| 140 | _copy_with_mkdir(real_l, dest_dir, so_path, name) |
| 141 | |
| 142 | |
| 143 | def _check_binfmt_misc(executable): |
| 144 | """Check binfmt_misc has entry for executable in the right place. |
| 145 | |
| 146 | The details of setting up binfmt_misc are outside the scope of |
| 147 | this script but we should at least fail early with a useful |
| 148 | message if it won't work. |
| 149 | |
| 150 | Returns the configured binfmt path and a valid flag. For |
| 151 | persistent configurations we will still want to copy and dependent |
| 152 | libraries. |
| 153 | """ |
| 154 | |
| 155 | binary = os.path.basename(executable) |
| 156 | binfmt_entry = "/proc/sys/fs/binfmt_misc/%s" % (binary) |
| 157 | |
| 158 | if not os.path.exists(binfmt_entry): |
| 159 | print ("No binfmt_misc entry for %s" % (binary)) |
| 160 | return None, False |
| 161 | |
| 162 | with open(binfmt_entry) as x: entry = x.read() |
| 163 | |
| 164 | if re.search("flags:.*F.*\n", entry): |
| 165 | print("binfmt_misc for %s uses persistent(F) mapping to host binary" % |
| 166 | (binary)) |
| 167 | return None, True |
| 168 | |
| 169 | m = re.search(r"interpreter (\S+)\n", entry) |
| 170 | interp = m.group(1) |
| 171 | if interp and interp != executable: |
| 172 | print("binfmt_misc for %s does not point to %s, using %s" % |
| 173 | (binary, executable, interp)) |
| 174 | |
| 175 | return interp, True |
| 176 | |
| 177 | |
| 178 | def _read_qemu_dockerfile(img_name): |
| 179 | # special case for Debian linux-user images |
| 180 | if img_name.startswith("debian") and img_name.endswith("user"): |
| 181 | img_name = "debian-bootstrap" |
| 182 | |
| 183 | df = os.path.join(os.path.dirname(__file__), "dockerfiles", |
| 184 | img_name + ".docker") |
| 185 | return _read_dockerfile(df) |
| 186 | |
| 187 | |
| 188 | def _dockerfile_verify_flat(df): |
| 189 | "Verify we do not include other qemu/ layers" |
| 190 | for l in df.splitlines(): |
| 191 | if len(l.strip()) == 0 or l.startswith("#"): |
| 192 | continue |
| 193 | from_pref = "FROM qemu/" |
| 194 | if l.startswith(from_pref): |
| 195 | print("We no longer support multiple QEMU layers.") |
| 196 | print("Dockerfiles should be flat, ideally created by lcitool") |
| 197 | return False |
| 198 | return True |
| 199 | |
| 200 | |
| 201 | class Docker(object): |
| 202 | """ Running Docker commands """ |
| 203 | def __init__(self, commandstr=None): |
| 204 | if commandstr is None: |
| 205 | self._command = _guess_engine_command() |
| 206 | else: |
| 207 | self._command = commandstr.split(" ") |
| 208 | |
| 209 | if ("docker" in self._command and |
| 210 | "TRAVIS" not in os.environ and |
| 211 | "GITLAB_CI" not in os.environ): |
| 212 | os.environ["DOCKER_BUILDKIT"] = "1" |
| 213 | self._buildkit = True |
| 214 | else: |
| 215 | self._buildkit = False |
| 216 | |
| 217 | self._instance = None |
| 218 | atexit.register(self._kill_instances) |
| 219 | signal.signal(signal.SIGTERM, self._kill_instances) |
| 220 | signal.signal(signal.SIGHUP, self._kill_instances) |
| 221 | |
| 222 | def _do(self, cmd, quiet=True, **kwargs): |
| 223 | if quiet: |
| 224 | kwargs["stdout"] = DEVNULL |
| 225 | return subprocess.call(self._command + cmd, **kwargs) |
| 226 | |
| 227 | def _do_check(self, cmd, quiet=True, **kwargs): |
| 228 | if quiet: |
| 229 | kwargs["stdout"] = DEVNULL |
| 230 | return subprocess.check_call(self._command + cmd, **kwargs) |
| 231 | |
| 232 | def _do_kill_instances(self, only_known, only_active=True): |
| 233 | cmd = ["ps", "-q"] |
| 234 | if not only_active: |
| 235 | cmd.append("-a") |
| 236 | |
| 237 | filter = "--filter=label=com.qemu.instance.uuid" |
| 238 | if only_known: |
| 239 | if self._instance: |
| 240 | filter += "=%s" % (self._instance) |
| 241 | else: |
| 242 | # no point trying to kill, we finished |
| 243 | return |
| 244 | |
| 245 | print("filter=%s" % (filter)) |
| 246 | cmd.append(filter) |
| 247 | for i in self._output(cmd).split(): |
| 248 | self._do(["rm", "-f", i]) |
| 249 | |
| 250 | def clean(self): |
| 251 | self._do_kill_instances(False, False) |
| 252 | return 0 |
| 253 | |
| 254 | def _kill_instances(self, *args, **kwargs): |
| 255 | return self._do_kill_instances(True) |
| 256 | |
| 257 | def _output(self, cmd, **kwargs): |
| 258 | try: |
| 259 | return subprocess.check_output(self._command + cmd, |
| 260 | stderr=subprocess.STDOUT, |
| 261 | encoding='utf-8', |
| 262 | **kwargs) |
| 263 | except TypeError: |
| 264 | # 'encoding' argument was added in 3.6+ |
| 265 | return subprocess.check_output(self._command + cmd, |
| 266 | stderr=subprocess.STDOUT, |
| 267 | **kwargs).decode('utf-8') |
| 268 | |
| 269 | |
| 270 | def inspect_tag(self, tag): |
| 271 | try: |
| 272 | return self._output(["inspect", tag]) |
| 273 | except subprocess.CalledProcessError: |
| 274 | return None |
| 275 | |
| 276 | def get_image_creation_time(self, info): |
| 277 | return json.loads(info)[0]["Created"] |
| 278 | |
| 279 | def get_image_dockerfile_checksum(self, tag): |
| 280 | resp = self.inspect_tag(tag) |
| 281 | labels = json.loads(resp)[0]["Config"].get("Labels", {}) |
| 282 | return labels.get("com.qemu.dockerfile-checksum", "") |
| 283 | |
| 284 | def build_image(self, tag, docker_dir, dockerfile, |
| 285 | quiet=True, user=False, argv=None, registry=None, |
| 286 | extra_files_cksum=[]): |
| 287 | if argv is None: |
| 288 | argv = [] |
| 289 | |
| 290 | if not _dockerfile_verify_flat(dockerfile): |
| 291 | return -1 |
| 292 | |
| 293 | checksum = _text_checksum(dockerfile) |
| 294 | |
| 295 | tmp_df = tempfile.NamedTemporaryFile(mode="w+t", |
| 296 | encoding='utf-8', |
| 297 | dir=docker_dir, suffix=".docker") |
| 298 | tmp_df.write(dockerfile) |
| 299 | |
| 300 | if user: |
| 301 | uid = os.getuid() |
| 302 | uname = getpass.getuser() |
| 303 | tmp_df.write("\n") |
| 304 | tmp_df.write("RUN id %s 2>/dev/null || useradd -u %d -U %s" % |
| 305 | (uname, uid, uname)) |
| 306 | |
| 307 | tmp_df.write("\n") |
| 308 | tmp_df.write("LABEL com.qemu.dockerfile-checksum=%s\n" % (checksum)) |
| 309 | for f, c in extra_files_cksum: |
| 310 | tmp_df.write("LABEL com.qemu.%s-checksum=%s\n" % (f, c)) |
| 311 | |
| 312 | tmp_df.flush() |
| 313 | |
| 314 | build_args = ["build", "-t", tag, "-f", tmp_df.name] |
| 315 | if self._buildkit: |
| 316 | build_args += ["--build-arg", "BUILDKIT_INLINE_CACHE=1"] |
| 317 | |
| 318 | if registry is not None: |
| 319 | pull_args = ["pull", "%s/%s" % (registry, tag)] |
| 320 | self._do(pull_args, quiet=quiet) |
| 321 | cache = "%s/%s" % (registry, tag) |
| 322 | build_args += ["--cache-from", cache] |
| 323 | build_args += argv |
| 324 | build_args += [docker_dir] |
| 325 | |
| 326 | self._do_check(build_args, |
| 327 | quiet=quiet) |
| 328 | |
| 329 | def update_image(self, tag, tarball, quiet=True): |
| 330 | "Update a tagged image using " |
| 331 | |
| 332 | self._do_check(["build", "-t", tag, "-"], quiet=quiet, stdin=tarball) |
| 333 | |
| 334 | def image_matches_dockerfile(self, tag, dockerfile): |
| 335 | try: |
| 336 | checksum = self.get_image_dockerfile_checksum(tag) |
| 337 | except Exception: |
| 338 | return False |
| 339 | return checksum == _text_checksum(dockerfile) |
| 340 | |
| 341 | def run(self, cmd, keep, quiet, as_user=False): |
| 342 | label = uuid.uuid4().hex |
| 343 | if not keep: |
| 344 | self._instance = label |
| 345 | |
| 346 | if as_user: |
| 347 | uid = os.getuid() |
| 348 | cmd = [ "-u", str(uid) ] + cmd |
| 349 | # podman requires a bit more fiddling |
| 350 | if self._command[0] == "podman": |
| 351 | cmd.insert(0, '--userns=keep-id') |
| 352 | |
| 353 | ret = self._do_check(["run", "--rm", "--label", |
| 354 | "com.qemu.instance.uuid=" + label] + cmd, |
| 355 | quiet=quiet) |
| 356 | if not keep: |
| 357 | self._instance = None |
| 358 | return ret |
| 359 | |
| 360 | def command(self, cmd, argv, quiet): |
| 361 | return self._do([cmd] + argv, quiet=quiet) |
| 362 | |
| 363 | |
| 364 | class SubCommand(object): |
| 365 | """A SubCommand template base class""" |
| 366 | name = None # Subcommand name |
| 367 | |
| 368 | def shared_args(self, parser): |
| 369 | parser.add_argument("--quiet", action="store_true", |
| 370 | help="Run quietly unless an error occurred") |
| 371 | |
| 372 | def args(self, parser): |
| 373 | """Setup argument parser""" |
| 374 | pass |
| 375 | |
| 376 | def run(self, args, argv): |
| 377 | """Run command. |
| 378 | args: parsed argument by argument parser. |
| 379 | argv: remaining arguments from sys.argv. |
| 380 | """ |
| 381 | pass |
| 382 | |
| 383 | |
| 384 | class RunCommand(SubCommand): |
| 385 | """Invoke docker run and take care of cleaning up""" |
| 386 | name = "run" |
| 387 | |
| 388 | def args(self, parser): |
| 389 | parser.add_argument("--keep", action="store_true", |
| 390 | help="Don't remove image when command completes") |
| 391 | parser.add_argument("--run-as-current-user", action="store_true", |
| 392 | help="Run container using the current user's uid") |
| 393 | |
| 394 | def run(self, args, argv): |
| 395 | return Docker(args.command).run(argv, args.keep, quiet=args.quiet, |
| 396 | as_user=args.run_as_current_user) |
| 397 | |
| 398 | |
| 399 | class BuildCommand(SubCommand): |
| 400 | """ Build docker image out of a dockerfile. Arg: <tag> <dockerfile>""" |
| 401 | name = "build" |
| 402 | |
| 403 | def args(self, parser): |
| 404 | parser.add_argument("--include-executable", "-e", |
| 405 | help="""Specify a binary that will be copied to the |
| 406 | container together with all its dependent |
| 407 | libraries""") |
| 408 | parser.add_argument("--skip-binfmt", |
| 409 | action="store_true", |
| 410 | help="""Skip binfmt entry check (used for testing)""") |
| 411 | parser.add_argument("--extra-files", nargs='*', |
| 412 | help="""Specify files that will be copied in the |
| 413 | Docker image, fulfilling the ADD directive from the |
| 414 | Dockerfile""") |
| 415 | parser.add_argument("--add-current-user", "-u", dest="user", |
| 416 | action="store_true", |
| 417 | help="Add the current user to image's passwd") |
| 418 | parser.add_argument("--registry", "-r", |
| 419 | help="cache from docker registry") |
| 420 | parser.add_argument("-t", dest="tag", |
| 421 | help="Image Tag") |
| 422 | parser.add_argument("-f", dest="dockerfile", |
| 423 | help="Dockerfile name") |
| 424 | |
| 425 | def run(self, args, argv): |
| 426 | dockerfile = _read_dockerfile(args.dockerfile) |
| 427 | tag = args.tag |
| 428 | |
| 429 | dkr = Docker(args.command) |
| 430 | if "--no-cache" not in argv and \ |
| 431 | dkr.image_matches_dockerfile(tag, dockerfile): |
| 432 | if not args.quiet: |
| 433 | print("Image is up to date.") |
| 434 | else: |
| 435 | # Create a docker context directory for the build |
| 436 | docker_dir = tempfile.mkdtemp(prefix="docker_build") |
| 437 | |
| 438 | # Validate binfmt_misc will work |
| 439 | if args.skip_binfmt: |
| 440 | qpath = args.include_executable |
| 441 | elif args.include_executable: |
| 442 | qpath, enabled = _check_binfmt_misc(args.include_executable) |
| 443 | if not enabled: |
| 444 | return 1 |
| 445 | |
| 446 | # Is there a .pre file to run in the build context? |
| 447 | docker_pre = os.path.splitext(args.dockerfile)[0]+".pre" |
| 448 | if os.path.exists(docker_pre): |
| 449 | stdout = DEVNULL if args.quiet else None |
| 450 | rc = subprocess.call(os.path.realpath(docker_pre), |
| 451 | cwd=docker_dir, stdout=stdout) |
| 452 | if rc == 3: |
| 453 | print("Skip") |
| 454 | return 0 |
| 455 | elif rc != 0: |
| 456 | print("%s exited with code %d" % (docker_pre, rc)) |
| 457 | return 1 |
| 458 | |
| 459 | # Copy any extra files into the Docker context. These can be |
| 460 | # included by the use of the ADD directive in the Dockerfile. |
| 461 | cksum = [] |
| 462 | if args.include_executable: |
| 463 | # FIXME: there is no checksum of this executable and the linked |
| 464 | # libraries, once the image built any change of this executable |
| 465 | # or any library won't trigger another build. |
| 466 | _copy_binary_with_libs(args.include_executable, |
| 467 | qpath, docker_dir) |
| 468 | |
| 469 | for filename in args.extra_files or []: |
| 470 | _copy_with_mkdir(filename, docker_dir) |
| 471 | cksum += [(filename, _file_checksum(filename))] |
| 472 | |
| 473 | argv += ["--build-arg=" + k.lower() + "=" + v |
| 474 | for k, v in os.environ.items() |
| 475 | if k.lower() in FILTERED_ENV_NAMES] |
| 476 | dkr.build_image(tag, docker_dir, dockerfile, |
| 477 | quiet=args.quiet, user=args.user, |
| 478 | argv=argv, registry=args.registry, |
| 479 | extra_files_cksum=cksum) |
| 480 | |
| 481 | rmtree(docker_dir) |
| 482 | |
| 483 | return 0 |
| 484 | |
| 485 | class FetchCommand(SubCommand): |
| 486 | """ Fetch a docker image from the registry. Args: <tag> <registry>""" |
| 487 | name = "fetch" |
| 488 | |
| 489 | def args(self, parser): |
| 490 | parser.add_argument("tag", |
| 491 | help="Local tag for image") |
| 492 | parser.add_argument("registry", |
| 493 | help="Docker registry") |
| 494 | |
| 495 | def run(self, args, argv): |
| 496 | dkr = Docker(args.command) |
| 497 | dkr.command(cmd="pull", quiet=args.quiet, |
| 498 | argv=["%s/%s" % (args.registry, args.tag)]) |
| 499 | dkr.command(cmd="tag", quiet=args.quiet, |
| 500 | argv=["%s/%s" % (args.registry, args.tag), args.tag]) |
| 501 | |
| 502 | |
| 503 | class UpdateCommand(SubCommand): |
| 504 | """ Update a docker image. Args: <tag> <actions>""" |
| 505 | name = "update" |
| 506 | |
| 507 | def args(self, parser): |
| 508 | parser.add_argument("tag", |
| 509 | help="Image Tag") |
| 510 | parser.add_argument("--executable", |
| 511 | help="Executable to copy") |
| 512 | parser.add_argument("--add-current-user", "-u", dest="user", |
| 513 | action="store_true", |
| 514 | help="Add the current user to image's passwd") |
| 515 | |
| 516 | def run(self, args, argv): |
| 517 | # Create a temporary tarball with our whole build context and |
| 518 | # dockerfile for the update |
| 519 | tmp = tempfile.NamedTemporaryFile(suffix="dckr.tar.gz") |
| 520 | tmp_tar = TarFile(fileobj=tmp, mode='w') |
| 521 | |
| 522 | # Create a Docker buildfile |
| 523 | df = StringIO() |
| 524 | df.write(u"FROM %s\n" % args.tag) |
| 525 | |
| 526 | if args.executable: |
| 527 | # Add the executable to the tarball, using the current |
| 528 | # configured binfmt_misc path. If we don't get a path then we |
| 529 | # only need the support libraries copied |
| 530 | ff, enabled = _check_binfmt_misc(args.executable) |
| 531 | |
| 532 | if not enabled: |
| 533 | print("binfmt_misc not enabled, update disabled") |
| 534 | return 1 |
| 535 | |
| 536 | if ff: |
| 537 | tmp_tar.add(args.executable, arcname=ff) |
| 538 | |
| 539 | # Add any associated libraries |
| 540 | libs = _get_so_libs(args.executable) |
| 541 | if libs: |
| 542 | for l in libs: |
| 543 | so_path = os.path.dirname(l) |
| 544 | name = os.path.basename(l) |
| 545 | real_l = os.path.realpath(l) |
| 546 | try: |
| 547 | tmp_tar.add(real_l, arcname="%s/%s" % (so_path, name)) |
| 548 | except FileNotFoundError: |
| 549 | print("Couldn't add %s/%s to archive" % (so_path, name)) |
| 550 | pass |
| 551 | |
| 552 | df.write(u"ADD . /\n") |
| 553 | |
| 554 | if args.user: |
| 555 | uid = os.getuid() |
| 556 | uname = getpass.getuser() |
| 557 | df.write("\n") |
| 558 | df.write("RUN id %s 2>/dev/null || useradd -u %d -U %s" % |
| 559 | (uname, uid, uname)) |
| 560 | |
| 561 | df_bytes = BytesIO(bytes(df.getvalue(), "UTF-8")) |
| 562 | |
| 563 | df_tar = TarInfo(name="Dockerfile") |
| 564 | df_tar.size = df_bytes.getbuffer().nbytes |
| 565 | tmp_tar.addfile(df_tar, fileobj=df_bytes) |
| 566 | |
| 567 | tmp_tar.close() |
| 568 | |
| 569 | # reset the file pointers |
| 570 | tmp.flush() |
| 571 | tmp.seek(0) |
| 572 | |
| 573 | # Run the build with our tarball context |
| 574 | dkr = Docker(args.command) |
| 575 | dkr.update_image(args.tag, tmp, quiet=args.quiet) |
| 576 | |
| 577 | return 0 |
| 578 | |
| 579 | |
| 580 | class CleanCommand(SubCommand): |
| 581 | """Clean up docker instances""" |
| 582 | name = "clean" |
| 583 | |
| 584 | def run(self, args, argv): |
| 585 | Docker(args.command).clean() |
| 586 | return 0 |
| 587 | |
| 588 | |
| 589 | class ImagesCommand(SubCommand): |
| 590 | """Run "docker images" command""" |
| 591 | name = "images" |
| 592 | |
| 593 | def run(self, args, argv): |
| 594 | return Docker(args.command).command("images", argv, args.quiet) |
| 595 | |
| 596 | |
| 597 | class ProbeCommand(SubCommand): |
| 598 | """Probe if we can run docker automatically""" |
| 599 | name = "probe" |
| 600 | |
| 601 | def run(self, args, argv): |
| 602 | try: |
| 603 | docker = Docker(args.command) |
| 604 | print(" ".join(docker._command)) |
| 605 | except Exception: |
| 606 | print("no") |
| 607 | |
| 608 | return |
| 609 | |
| 610 | |
| 611 | class CcCommand(SubCommand): |
| 612 | """Compile sources with cc in images""" |
| 613 | name = "cc" |
| 614 | |
| 615 | def args(self, parser): |
| 616 | parser.add_argument("--image", "-i", required=True, |
| 617 | help="The docker image in which to run cc") |
| 618 | parser.add_argument("--cc", default="cc", |
| 619 | help="The compiler executable to call") |
| 620 | parser.add_argument("--source-path", "-s", nargs="*", dest="paths", |
| 621 | help="""Extra paths to (ro) mount into container for |
| 622 | reading sources""") |
| 623 | |
| 624 | def run(self, args, argv): |
| 625 | if argv and argv[0] == "--": |
| 626 | argv = argv[1:] |
| 627 | cwd = os.getcwd() |
| 628 | cmd = ["-w", cwd, |
| 629 | "-v", "%s:%s:rw" % (cwd, cwd)] |
| 630 | if args.paths: |
| 631 | for p in args.paths: |
| 632 | cmd += ["-v", "%s:%s:ro,z" % (p, p)] |
| 633 | cmd += [args.image, args.cc] |
| 634 | cmd += argv |
| 635 | return Docker(args.command).run(cmd, False, quiet=args.quiet, |
| 636 | as_user=True) |
| 637 | |
| 638 | |
| 639 | def main(): |
| 640 | parser = argparse.ArgumentParser(description="A Docker helper", |
| 641 | usage="%s <subcommand> ..." % |
| 642 | os.path.basename(sys.argv[0])) |
| 643 | parser.add_argument("--command", |
| 644 | help="specify which container engine command to use") |
| 645 | subparsers = parser.add_subparsers(title="subcommands", help=None) |
| 646 | for cls in SubCommand.__subclasses__(): |
| 647 | cmd = cls() |
| 648 | subp = subparsers.add_parser(cmd.name, help=cmd.__doc__) |
| 649 | cmd.shared_args(subp) |
| 650 | cmd.args(subp) |
| 651 | subp.set_defaults(cmdobj=cmd) |
| 652 | args, argv = parser.parse_known_args() |
| 653 | return args.cmdobj.run(args, argv) |
| 654 | |
| 655 | |
| 656 | if __name__ == "__main__": |
| 657 | sys.exit(main()) |