diff --git a/repo2docker/docker.py b/repo2docker/docker.py index 830e5c78..e2eeac06 100644 --- a/repo2docker/docker.py +++ b/repo2docker/docker.py @@ -2,12 +2,17 @@ Docker container engine for repo2docker """ +import shutil +import tarfile +import tempfile + from iso8601 import parse_date -from traitlets import Dict +from traitlets import Dict, List, Unicode import docker from .engine import Container, ContainerEngine, ContainerEngineException, Image +from .utils import execute_cmd class DockerContainer(Container): @@ -53,7 +58,7 @@ class DockerEngine(ContainerEngine): https://docker-py.readthedocs.io/en/4.2.0/api.html#module-docker.api.build """ - string_output = False + string_output = True extra_init_args = Dict( {}, @@ -69,6 +74,14 @@ class DockerEngine(ContainerEngine): config=True, ) + extra_buildx_build_args = List( + [], + help=""" + Extra commandline arguments to pass to `docker buildx build` when building the image. + """, + config=True, + ) + def __init__(self, *, parent): super().__init__(parent=parent) try: @@ -94,22 +107,46 @@ def build( platform=None, **kwargs, ): - return self._apiclient.build( - buildargs=buildargs, - cache_from=cache_from, - container_limits=container_limits, - forcerm=True, - rm=True, - tag=tag, - custom_context=custom_context, - decode=True, - dockerfile=dockerfile, - fileobj=fileobj, - path=path, - labels=labels, - platform=platform, - **kwargs, - ) + if not shutil.which("docker"): + raise RuntimeError("The docker commandline client must be installed") + args = ["docker", "buildx", "build", "--progress", "plain", "--load"] + if buildargs: + for k, v in buildargs.items(): + args += ["--build-arg", f"{k}={v}"] + + if cache_from: + for cf in cache_from: + args += ["--cache-from", cf] + + if dockerfile: + args += ["--file", dockerfile] + + if tag: + args += ["--tag", tag] + + if labels: + for k, v in labels.items(): + args += ["--label", f"{k}={v}"] + + if platform: + args += ["--platform", platform] + + # place extra args right *before* the path + args += self.extra_buildx_build_args + + if fileobj: + with tempfile.TemporaryDirectory() as d: + tarf = tarfile.open(fileobj=fileobj) + tarf.extractall(d) + + args += [d] + + yield from execute_cmd(args, True) + else: + # Assume 'path' is passed in + args += [path] + + yield from execute_cmd(args, True) def images(self): images = self._apiclient.images() diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index d37103e6..7828c318 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -78,21 +78,6 @@ def test_local_dir_image_name(repo_with_content): ) -def test_build_kwargs(repo_with_content): - upstream, sha1 = repo_with_content - argv = [upstream] - app = make_r2d(argv) - app.extra_build_kwargs = {"somekey": "somevalue"} - - with patch.object(docker.APIClient, "build") as builds: - builds.return_value = [] - app.build() - builds.assert_called_once() - args, kwargs = builds.call_args - assert "somekey" in kwargs - assert kwargs["somekey"] == "somevalue" - - def test_run_kwargs(repo_with_content): upstream, sha1 = repo_with_content argv = [upstream] @@ -107,26 +92,6 @@ def test_run_kwargs(repo_with_content): assert kwargs["somekey"] == "somevalue" -def test_root_not_allowed(): - with TemporaryDirectory() as src, patch("os.geteuid") as geteuid: - geteuid.return_value = 0 - argv = [src] - with pytest.raises(SystemExit) as exc: - app = make_r2d(argv) - assert exc.code == 1 - - with pytest.raises(ValueError): - app = Repo2Docker(repo=src, run=False) - app.build() - - app = Repo2Docker(repo=src, user_id=1000, user_name="jovyan", run=False) - app.initialize() - with patch.object(docker.APIClient, "build") as builds: - builds.return_value = [] - app.build() - builds.assert_called_once() - - def test_dryrun_works_without_docker(tmpdir, capsys): with chdir(tmpdir): with patch.object(docker, "APIClient") as client: