Skip to content

Commit 520c7ad

Browse files
authored
Merge pull request #848 from manics/abstractengine
[MRG] Define an interface for Container engines
2 parents ea90ae2 + d47b744 commit 520c7ad

File tree

11 files changed

+506
-44
lines changed

11 files changed

+506
-44
lines changed

repo2docker/__main__.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import sys
33
import os
44
import logging
5-
import docker
65
from .app import Repo2Docker
6+
from .engine import BuildError, ImageLoadError
77
from . import __version__
88
from .utils import validate_and_generate_port_mapping, is_valid_docker_image_name
99

@@ -217,6 +217,8 @@ def get_argparser():
217217
"--cache-from", action="append", default=[], help=Repo2Docker.cache_from.help
218218
)
219219

220+
argparser.add_argument("--engine", help="Name of the container engine")
221+
220222
return argparser
221223

222224

@@ -351,6 +353,9 @@ def make_r2d(argv=None):
351353
if args.cache_from:
352354
r2d.cache_from = args.cache_from
353355

356+
if args.engine:
357+
r2d.engine = args.engine
358+
354359
r2d.environment = args.environment
355360

356361
# if the source exists locally we don't want to delete it at the end
@@ -371,12 +376,12 @@ def main():
371376
r2d.initialize()
372377
try:
373378
r2d.start()
374-
except docker.errors.BuildError as e:
379+
except BuildError as e:
375380
# This is only raised by us
376381
if r2d.log_level == logging.DEBUG:
377382
r2d.log.exception(e)
378383
sys.exit(1)
379-
except docker.errors.ImageLoadError as e:
384+
except ImageLoadError as e:
380385
# This is only raised by us
381386
if r2d.log_level == logging.DEBUG:
382387
r2d.log.exception(e)

repo2docker/app.py

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,14 @@
1111
import sys
1212
import logging
1313
import os
14+
import entrypoints
1415
import getpass
1516
import shutil
1617
import tempfile
1718
import time
1819

19-
import docker
20+
from .engine import BuildError, ContainerEngineException, ImageLoadError
2021
from urllib.parse import urlparse
21-
from docker.utils import kwargs_from_env
22-
from docker.errors import DockerException
2322
import escapism
2423
from pythonjsonlogger import jsonlogger
2524

@@ -382,6 +381,33 @@ def _user_name_default(self):
382381
config=True,
383382
)
384383

384+
engine = Unicode(
385+
"docker",
386+
config=True,
387+
help="""
388+
Name of the container engine.
389+
390+
Defaults to 'docker'.
391+
""",
392+
)
393+
394+
def get_engine(self):
395+
"""Return an instance of the container engine.
396+
397+
Currently no arguments are passed to the engine constructor.
398+
"""
399+
engines = entrypoints.get_group_named("repo2docker.engines")
400+
try:
401+
entry = engines[self.engine]
402+
except KeyError:
403+
raise ContainerEngineException(
404+
"Container engine '{}' not found. Available engines: {}".format(
405+
self.engine, ",".join(engines.keys())
406+
)
407+
)
408+
engine_class = entry.load()
409+
return engine_class(parent=self)
410+
385411
def fetch(self, url, ref, checkout_path):
386412
"""Fetch the contents of `url` and place it in `checkout_path`.
387413
@@ -474,13 +500,18 @@ def initialize(self):
474500

475501
def push_image(self):
476502
"""Push docker image to registry"""
477-
client = docker.APIClient(version="auto", **kwargs_from_env())
503+
client = self.get_engine()
478504
# Build a progress setup for each layer, and only emit per-layer
479505
# info every 1.5s
480506
progress_layers = {}
481507
layers = {}
482508
last_emit_time = time.time()
483-
for chunk in client.push(self.output_image_spec, stream=True):
509+
for chunk in client.push(self.output_image_spec):
510+
if client.string_output:
511+
self.log.info(chunk, extra=dict(phase="pushing"))
512+
continue
513+
# else this is Docker output
514+
484515
# each chunk can be one or more lines of json events
485516
# split lines here in case multiple are delivered at once
486517
for line in chunk.splitlines():
@@ -492,7 +523,7 @@ def push_image(self):
492523
continue
493524
if "error" in progress:
494525
self.log.error(progress["error"], extra=dict(phase="failed"))
495-
raise docker.errors.ImageLoadError(progress["error"])
526+
raise ImageLoadError(progress["error"])
496527
if "id" not in progress:
497528
continue
498529
# deprecated truncated-progress data
@@ -528,7 +559,7 @@ def start_container(self):
528559
529560
Returns running container
530561
"""
531-
client = docker.from_env(version="auto")
562+
client = self.get_engine()
532563

533564
docker_host = os.environ.get("DOCKER_HOST")
534565
if docker_host:
@@ -565,11 +596,8 @@ def start_container(self):
565596

566597
container_volumes = {}
567598
if self.volumes:
568-
api_client = docker.APIClient(
569-
version="auto", **docker.utils.kwargs_from_env()
570-
)
571-
image = api_client.inspect_image(self.output_image_spec)
572-
image_workdir = image["ContainerConfig"]["WorkingDir"]
599+
image = client.inspect_image(self.output_image_spec)
600+
image_workdir = image.config["WorkingDir"]
573601

574602
for k, v in self.volumes.items():
575603
container_volumes[os.path.abspath(k)] = {
@@ -580,15 +608,14 @@ def start_container(self):
580608
run_kwargs = dict(
581609
publish_all_ports=self.all_ports,
582610
ports=ports,
583-
detach=True,
584611
command=run_cmd,
585612
volumes=container_volumes,
586613
environment=self.environment,
587614
)
588615

589616
run_kwargs.update(self.extra_run_kwargs)
590617

591-
container = client.containers.run(self.output_image_spec, **run_kwargs)
618+
container = client.run(self.output_image_spec, **run_kwargs)
592619

593620
while container.status == "created":
594621
time.sleep(0.5)
@@ -611,7 +638,7 @@ def wait_for_container(self, container):
611638
if container.status == "running":
612639
self.log.info("Stopping container...\n", extra=dict(phase="running"))
613640
container.kill()
614-
exit_code = container.attrs["State"]["ExitCode"]
641+
exit_code = container.exitcode
615642

616643
container.wait()
617644

@@ -645,12 +672,11 @@ def find_image(self):
645672
if self.dry_run:
646673
return False
647674
# check if we already have an image for this content
648-
client = docker.APIClient(version="auto", **kwargs_from_env())
675+
client = self.get_engine()
649676
for image in client.images():
650-
if image["RepoTags"] is not None:
651-
for tag in image["RepoTags"]:
652-
if tag == self.output_image_spec + ":latest":
653-
return True
677+
for tag in image.tags:
678+
if tag == self.output_image_spec + ":latest":
679+
return True
654680
return False
655681

656682
def build(self):
@@ -660,12 +686,9 @@ def build(self):
660686
# Check if r2d can connect to docker daemon
661687
if not self.dry_run:
662688
try:
663-
docker_client = docker.APIClient(version="auto", **kwargs_from_env())
664-
except DockerException as e:
665-
self.log.error(
666-
"\nDocker client initialization error: %s.\nCheck if docker is running on the host.\n",
667-
e,
668-
)
689+
docker_client = self.get_engine()
690+
except ContainerEngineException as e:
691+
self.log.error("\nContainer engine initialization error: %s\n", e)
669692
self.exit(1)
670693

671694
# If the source to be executed is a directory, continue using the
@@ -751,11 +774,14 @@ def build(self):
751774
self.cache_from,
752775
self.extra_build_kwargs,
753776
):
754-
if "stream" in l:
777+
if docker_client.string_output:
778+
self.log.info(l, extra=dict(phase="building"))
779+
# else this is Docker output
780+
elif "stream" in l:
755781
self.log.info(l["stream"], extra=dict(phase="building"))
756782
elif "error" in l:
757783
self.log.info(l["error"], extra=dict(phase="failure"))
758-
raise docker.errors.BuildError(l["error"], build_log="")
784+
raise BuildError(l["error"])
759785
elif "status" in l:
760786
self.log.info(
761787
"Fetching base image...\r", extra=dict(phase="building")

repo2docker/buildpacks/base.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -607,9 +607,6 @@ def _filter_tar(tar):
607607
tag=image_spec,
608608
custom_context=True,
609609
buildargs=build_args,
610-
decode=True,
611-
forcerm=True,
612-
rm=True,
613610
container_limits=limits,
614611
cache_from=cache_from,
615612
)

repo2docker/buildpacks/docker.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,6 @@ def build(
5050
dockerfile=self.binder_path(self.dockerfile),
5151
tag=image_spec,
5252
buildargs=build_args,
53-
decode=True,
54-
forcerm=True,
55-
rm=True,
5653
container_limits=limits,
5754
cache_from=cache_from,
5855
)

repo2docker/docker.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""
2+
Docker container engine for repo2docker
3+
"""
4+
5+
import docker
6+
from .engine import Container, ContainerEngine, ContainerEngineException, Image
7+
8+
9+
class DockerContainer(Container):
10+
def __init__(self, container):
11+
self._c = container
12+
13+
def reload(self):
14+
return self._c.reload()
15+
16+
def logs(self, *, stream=False):
17+
return self._c.logs(stream=stream)
18+
19+
def kill(self, *, signal="KILL"):
20+
return self._c.kill(signal=signal)
21+
22+
def remove(self):
23+
return self._c.remove()
24+
25+
def stop(self, *, timeout=10):
26+
return self._c.stop(timeout=timeout)
27+
28+
def wait(self):
29+
return self._c.wait()
30+
31+
@property
32+
def exitcode(self):
33+
return self._c.attrs["State"]["ExitCode"]
34+
35+
@property
36+
def status(self):
37+
return self._c.status
38+
39+
40+
class DockerEngine(ContainerEngine):
41+
"""
42+
https://docker-py.readthedocs.io/en/4.2.0/api.html#module-docker.api.build
43+
"""
44+
45+
string_output = False
46+
47+
def __init__(self, *, parent):
48+
super().__init__(parent=parent)
49+
try:
50+
self._apiclient = docker.APIClient(
51+
version="auto", **docker.utils.kwargs_from_env()
52+
)
53+
except docker.errors.DockerException as e:
54+
raise ContainerEngineException("Check if docker is running on the host.", e)
55+
56+
def build(
57+
self,
58+
*,
59+
buildargs=None,
60+
cache_from=None,
61+
container_limits=None,
62+
tag="",
63+
custom_context=False,
64+
dockerfile="",
65+
fileobj=None,
66+
path="",
67+
**kwargs,
68+
):
69+
return self._apiclient.build(
70+
buildargs=buildargs,
71+
cache_from=cache_from,
72+
container_limits=container_limits,
73+
forcerm=True,
74+
rm=True,
75+
tag=tag,
76+
custom_context=custom_context,
77+
decode=True,
78+
dockerfile=dockerfile,
79+
fileobj=fileobj,
80+
path=path,
81+
**kwargs,
82+
)
83+
84+
def images(self):
85+
images = self._apiclient.images()
86+
return [Image(tags=image["RepoTags"]) for image in images]
87+
88+
def inspect_image(self, image):
89+
image = self._apiclient.inspect_image(image)
90+
return Image(tags=image["RepoTags"], config=image["ContainerConfig"])
91+
92+
def push(self, image_spec):
93+
return self._apiclient.push(image_spec, stream=True)
94+
95+
def run(
96+
self,
97+
image_spec,
98+
*,
99+
command=None,
100+
environment=None,
101+
ports=None,
102+
publish_all_ports=False,
103+
remove=False,
104+
volumes=None,
105+
**kwargs,
106+
):
107+
client = docker.from_env(version="auto")
108+
container = client.containers.run(
109+
image_spec,
110+
command=command,
111+
environment=(environment or []),
112+
detach=True,
113+
ports=(ports or {}),
114+
publish_all_ports=publish_all_ports,
115+
remove=remove,
116+
volumes=(volumes or {}),
117+
**kwargs,
118+
)
119+
return DockerContainer(container)

0 commit comments

Comments
 (0)