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