From d0d87c8c7c91c63f8eafe6f21b4108e15f6c2ef0 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Tue, 28 Jan 2025 19:00:09 -0800 Subject: [PATCH 01/13] Shell out to `docker buildx build` to build images pydocker uses the HTTP API (equivalent to `docker build`) which has been dead for ages. `docker buildx` (the interface to BuildKit) is what we *should* be using, but alas pydocker does not currently support it and I suspect it never will (https://github.com/docker/docker-py/issues/2230). This PR simply shells out, as I think that's the way. Fixes https://github.com/jupyterhub/repo2docker/issues/875 --- repo2docker/docker.py | 52 +++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/repo2docker/docker.py b/repo2docker/docker.py index 830e5c78..8ebc571c 100644 --- a/repo2docker/docker.py +++ b/repo2docker/docker.py @@ -2,12 +2,19 @@ Docker container engine for repo2docker """ +import subprocess +import tarfile +import tempfile +from queue import Empty, Queue +from threading import Thread + from iso8601 import parse_date from traitlets import Dict import docker from .engine import Container, ContainerEngine, ContainerEngineException, Image +from .utils import execute_cmd class DockerContainer(Container): @@ -53,7 +60,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( {}, @@ -82,8 +89,8 @@ def __init__(self, *, parent): def build( self, *, - buildargs=None, - cache_from=None, + buildargs: dict | None = None, + cache_from: list[str] | None = None, container_limits=None, tag="", custom_context=False, @@ -94,22 +101,29 @@ 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, - ) + args = ["docker", "buildx", "build", "--progress", "plain"] + 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 fileobj: + with tempfile.TemporaryDirectory() as d: + tarf = tarfile.open(fileobj=fileobj) + tarf.extractall(d) + + args += [d] + + yield from execute_cmd(args, True) def images(self): images = self._apiclient.images() From 1ca89b828821bd1d5b9a5e715670784c47808dd6 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Tue, 28 Jan 2025 19:10:26 -0800 Subject: [PATCH 02/13] Remove type hints for now --- repo2docker/docker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo2docker/docker.py b/repo2docker/docker.py index 8ebc571c..29f2acfe 100644 --- a/repo2docker/docker.py +++ b/repo2docker/docker.py @@ -89,8 +89,8 @@ def __init__(self, *, parent): def build( self, *, - buildargs: dict | None = None, - cache_from: list[str] | None = None, + buildargs=None, + cache_from=None, container_limits=None, tag="", custom_context=False, From 258daa7c2089943b3460d479fba5397ee549b130 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Tue, 28 Jan 2025 19:12:09 -0800 Subject: [PATCH 03/13] Automatically load built image to mimic previous behavior --- repo2docker/docker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repo2docker/docker.py b/repo2docker/docker.py index 29f2acfe..81219982 100644 --- a/repo2docker/docker.py +++ b/repo2docker/docker.py @@ -101,7 +101,7 @@ def build( platform=None, **kwargs, ): - args = ["docker", "buildx", "build", "--progress", "plain"] + args = ["docker", "buildx", "build", "--progress", "plain", "--load"] if buildargs: for k, v in buildargs.items(): args += ["--build-arg", f"{k}={v}"] From 93cb0d757c544f0c2e6d392f9d0395ad88cf0c10 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Tue, 28 Jan 2025 19:14:08 -0800 Subject: [PATCH 04/13] Support `Dockerfile` too --- repo2docker/docker.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/repo2docker/docker.py b/repo2docker/docker.py index 81219982..dc36a0a6 100644 --- a/repo2docker/docker.py +++ b/repo2docker/docker.py @@ -124,6 +124,11 @@ def build( 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() From 77ed4996809213076f8a03c6c477723f01895314 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Tue, 28 Jan 2025 19:17:45 -0800 Subject: [PATCH 05/13] Support --platform and --labels --- repo2docker/docker.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/repo2docker/docker.py b/repo2docker/docker.py index dc36a0a6..8116ae8b 100644 --- a/repo2docker/docker.py +++ b/repo2docker/docker.py @@ -116,6 +116,13 @@ def build( if tag: args += ["--tag", tag] + if labels: + for k, v in labels: + args += ["--label", f"{k}={v}"] + + if platform: + args += ["--platform", platform] + if fileobj: with tempfile.TemporaryDirectory() as d: tarf = tarfile.open(fileobj=fileobj) From 743c9f964cc8ae4c3f01faa41f392d75bc14cfb0 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Tue, 28 Jan 2025 19:21:53 -0800 Subject: [PATCH 06/13] Iterate labels as dicts --- repo2docker/docker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repo2docker/docker.py b/repo2docker/docker.py index 8116ae8b..64f056ba 100644 --- a/repo2docker/docker.py +++ b/repo2docker/docker.py @@ -117,7 +117,7 @@ def build( args += ["--tag", tag] if labels: - for k, v in labels: + for k, v in labels.items(): args += ["--label", f"{k}={v}"] if platform: From b1cedc4a78a1307e99fd0a92ab5dda1b49687b3a Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Tue, 28 Jan 2025 20:29:42 -0800 Subject: [PATCH 07/13] Remove some patched out tests that don't apply anymore --- tests/unit/test_app.py | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index d37103e6..67cddc07 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,25 +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): From 8e20b60c69a598d9bd881d20a22a41440ac5eba6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 04:30:03 +0000 Subject: [PATCH 08/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/unit/test_app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index 67cddc07..7828c318 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -92,7 +92,6 @@ def test_run_kwargs(repo_with_content): assert kwargs["somekey"] == "somevalue" - def test_dryrun_works_without_docker(tmpdir, capsys): with chdir(tmpdir): with patch.object(docker, "APIClient") as client: From a03bf74d1f5e2ae66a8d054b798846724055869a Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Tue, 28 Jan 2025 20:31:26 -0800 Subject: [PATCH 09/13] Add a warning + check to make sure docker is installed --- repo2docker/docker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/repo2docker/docker.py b/repo2docker/docker.py index 64f056ba..6bd545a8 100644 --- a/repo2docker/docker.py +++ b/repo2docker/docker.py @@ -2,11 +2,9 @@ Docker container engine for repo2docker """ -import subprocess +import shutil import tarfile import tempfile -from queue import Empty, Queue -from threading import Thread from iso8601 import parse_date from traitlets import Dict @@ -101,6 +99,8 @@ def build( platform=None, **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(): From d9407061284bda4cdb32e67d47edbf37ae1b1762 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Wed, 29 Jan 2025 08:25:23 -0800 Subject: [PATCH 10/13] Add extra_buildx_build_args --- repo2docker/docker.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/repo2docker/docker.py b/repo2docker/docker.py index 6bd545a8..6d622bec 100644 --- a/repo2docker/docker.py +++ b/repo2docker/docker.py @@ -7,7 +7,7 @@ import tempfile from iso8601 import parse_date -from traitlets import Dict +from traitlets import Dict, List, Unicode import docker @@ -74,6 +74,15 @@ class DockerEngine(ContainerEngine): config=True, ) + extra_buildx_build_args = List( + Unicode, + [], + help=""" + Extra commandline arguments to pass to `docker buildx build` when building the image. + """, + help=True + ) + def __init__(self, *, parent): super().__init__(parent=parent) try: @@ -123,6 +132,9 @@ def build( 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) From 5b9615643bc612355f5afd9dd0b9cda90e0e7f41 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Wed, 29 Jan 2025 08:28:24 -0800 Subject: [PATCH 11/13] Fix trait definition --- repo2docker/docker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo2docker/docker.py b/repo2docker/docker.py index 6d622bec..4f51e5fc 100644 --- a/repo2docker/docker.py +++ b/repo2docker/docker.py @@ -75,12 +75,12 @@ class DockerEngine(ContainerEngine): ) extra_buildx_build_args = List( - Unicode, + [Unicode], [], help=""" Extra commandline arguments to pass to `docker buildx build` when building the image. """, - help=True + config=True ) def __init__(self, *, parent): From d7968bbf8eed7007002935493bce7a0aaa63fa0b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 18:34:40 +0000 Subject: [PATCH 12/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- repo2docker/docker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repo2docker/docker.py b/repo2docker/docker.py index 4f51e5fc..8fb35011 100644 --- a/repo2docker/docker.py +++ b/repo2docker/docker.py @@ -80,7 +80,7 @@ class DockerEngine(ContainerEngine): help=""" Extra commandline arguments to pass to `docker buildx build` when building the image. """, - config=True + config=True, ) def __init__(self, *, parent): From 6df3ec637757465c1f4d72097c4afb5588a32ed4 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Wed, 29 Jan 2025 21:34:54 -0800 Subject: [PATCH 13/13] Fix traitlet again? --- repo2docker/docker.py | 1 - 1 file changed, 1 deletion(-) diff --git a/repo2docker/docker.py b/repo2docker/docker.py index 8fb35011..e2eeac06 100644 --- a/repo2docker/docker.py +++ b/repo2docker/docker.py @@ -75,7 +75,6 @@ class DockerEngine(ContainerEngine): ) extra_buildx_build_args = List( - [Unicode], [], help=""" Extra commandline arguments to pass to `docker buildx build` when building the image.