docker-devtools 0.0.1__py3-none-win_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docker_devtools/__init__.py +198 -0
- docker_devtools/__main__.py +33 -0
- docker_devtools/_find.py +116 -0
- docker_devtools/_version.py +24 -0
- docker_devtools/py.typed +0 -0
- docker_devtools-0.0.1.data/scripts/docker-devtools.exe +0 -0
- docker_devtools-0.0.1.dist-info/METADATA +227 -0
- docker_devtools-0.0.1.dist-info/RECORD +10 -0
- docker_devtools-0.0.1.dist-info/WHEEL +4 -0
- docker_devtools-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Work on the Dockerfiles, Compose files and build context in a repository.
|
|
2
|
+
|
|
3
|
+
This package bundles the ``docker-devtools`` binary and wraps its JSON output in
|
|
4
|
+
typed dataclasses. The work happens in Go, against the same libraries BuildKit
|
|
5
|
+
uses, so the results match what ``docker build`` would do.
|
|
6
|
+
|
|
7
|
+
>>> from docker_devtools import image_ls
|
|
8
|
+
>>> for ref in image_ls("testdata").refs:
|
|
9
|
+
... print(ref.path, ref.line, ref.raw)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import subprocess
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from dataclasses import field
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
from typing import Any
|
|
20
|
+
from typing import Literal
|
|
21
|
+
|
|
22
|
+
from docker_devtools._find import BINARY_ENV_VAR
|
|
23
|
+
from docker_devtools._find import BINARY_NAME
|
|
24
|
+
from docker_devtools._find import BinaryNotFoundError
|
|
25
|
+
from docker_devtools._find import find_binary
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from collections.abc import Sequence
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"BINARY_ENV_VAR",
|
|
32
|
+
"BINARY_NAME",
|
|
33
|
+
"IMAGE_SCHEMA_VERSION",
|
|
34
|
+
"BinaryNotFoundError",
|
|
35
|
+
"Change",
|
|
36
|
+
"ImageRef",
|
|
37
|
+
"ImageResult",
|
|
38
|
+
"UpdateReport",
|
|
39
|
+
"find_binary",
|
|
40
|
+
"image_ls",
|
|
41
|
+
"image_update",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
#: The image-scan schema this wrapper understands. A mismatch means the binary
|
|
45
|
+
#: and the Python half are out of step.
|
|
46
|
+
IMAGE_SCHEMA_VERSION = 1
|
|
47
|
+
|
|
48
|
+
TagPolicy = Literal["same-pattern", "minor", "patch", "latest"]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class ImageRef:
|
|
53
|
+
"""One image reference and where it sits."""
|
|
54
|
+
|
|
55
|
+
path: str
|
|
56
|
+
line: int
|
|
57
|
+
kind: str
|
|
58
|
+
raw: str
|
|
59
|
+
resolved: bool
|
|
60
|
+
registry: str | None = None
|
|
61
|
+
repository: str | None = None
|
|
62
|
+
tag: str | None = None
|
|
63
|
+
digest: str | None = None
|
|
64
|
+
stage: str | None = None
|
|
65
|
+
note: str | None = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class ImageResult:
|
|
70
|
+
"""Every reference found by a scan."""
|
|
71
|
+
|
|
72
|
+
schema: int
|
|
73
|
+
refs: tuple[ImageRef, ...]
|
|
74
|
+
warnings: tuple[str, ...] = ()
|
|
75
|
+
|
|
76
|
+
def resolved(self) -> tuple[ImageRef, ...]:
|
|
77
|
+
"""Return only the references that name a real image."""
|
|
78
|
+
return tuple(r for r in self.refs if r.resolved)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class Change:
|
|
83
|
+
"""One reference an update would rewrite."""
|
|
84
|
+
|
|
85
|
+
path: str
|
|
86
|
+
line: int
|
|
87
|
+
old: str
|
|
88
|
+
new: str
|
|
89
|
+
reason: str
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True)
|
|
93
|
+
class UpdateReport:
|
|
94
|
+
"""The plan an update produced."""
|
|
95
|
+
|
|
96
|
+
schema: int
|
|
97
|
+
changes: tuple[Change, ...]
|
|
98
|
+
skipped: int = 0
|
|
99
|
+
warnings: tuple[str, ...] = field(default=())
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def image_ls(*paths: str) -> ImageResult:
|
|
103
|
+
"""List every image reference under ``paths``.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
paths: files or directories. Defaults to the working directory.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The parsed scan.
|
|
110
|
+
"""
|
|
111
|
+
data = _run(["image", "ls", "--json", *paths])
|
|
112
|
+
_check_schema(data.get("schema"))
|
|
113
|
+
return ImageResult(
|
|
114
|
+
schema=data["schema"],
|
|
115
|
+
refs=tuple(
|
|
116
|
+
ImageRef(
|
|
117
|
+
path=r["path"],
|
|
118
|
+
line=r["line"],
|
|
119
|
+
kind=r["kind"],
|
|
120
|
+
raw=r["raw"],
|
|
121
|
+
resolved=r["resolved"],
|
|
122
|
+
registry=r.get("registry"),
|
|
123
|
+
repository=r.get("repository"),
|
|
124
|
+
tag=r.get("tag"),
|
|
125
|
+
digest=r.get("digest"),
|
|
126
|
+
stage=r.get("stage"),
|
|
127
|
+
note=r.get("note"),
|
|
128
|
+
)
|
|
129
|
+
for r in data["refs"]
|
|
130
|
+
),
|
|
131
|
+
warnings=tuple(data.get("warnings") or ()),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def image_update(
|
|
136
|
+
*paths: str,
|
|
137
|
+
pin_digest: bool = False,
|
|
138
|
+
tag_policy: TagPolicy | None = None,
|
|
139
|
+
dry_run: bool = True,
|
|
140
|
+
) -> UpdateReport:
|
|
141
|
+
"""Plan, and optionally apply, changes to image references.
|
|
142
|
+
|
|
143
|
+
``dry_run`` defaults to True so that calling this by accident cannot
|
|
144
|
+
rewrite a repository.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
paths: files or directories. Defaults to the working directory.
|
|
148
|
+
pin_digest: append or refresh the ``@sha256`` digest.
|
|
149
|
+
tag_policy: how far a tag may move. None leaves tags alone.
|
|
150
|
+
dry_run: report without writing.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
The plan, whether or not it was applied.
|
|
154
|
+
"""
|
|
155
|
+
args = ["image", "update", "--json"]
|
|
156
|
+
if pin_digest:
|
|
157
|
+
args.append("--pin-digest")
|
|
158
|
+
if tag_policy is not None:
|
|
159
|
+
args += ["--tag-policy", tag_policy]
|
|
160
|
+
if dry_run:
|
|
161
|
+
args.append("--dry-run")
|
|
162
|
+
args += list(paths)
|
|
163
|
+
|
|
164
|
+
data = _run(args)
|
|
165
|
+
_check_schema(data.get("schema"))
|
|
166
|
+
return UpdateReport(
|
|
167
|
+
schema=data["schema"],
|
|
168
|
+
changes=tuple(
|
|
169
|
+
Change(path=c["path"], line=c["line"], old=c["old"], new=c["new"], reason=c["reason"])
|
|
170
|
+
for c in data["changes"]
|
|
171
|
+
),
|
|
172
|
+
skipped=data.get("skipped", 0),
|
|
173
|
+
warnings=tuple(data.get("warnings") or ()),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _run(args: Sequence[str]) -> dict[str, Any]:
|
|
178
|
+
binary = find_binary()
|
|
179
|
+
proc = subprocess.run( # noqa: S603
|
|
180
|
+
[binary, *args],
|
|
181
|
+
capture_output=True,
|
|
182
|
+
text=True,
|
|
183
|
+
check=False,
|
|
184
|
+
)
|
|
185
|
+
if proc.returncode != 0:
|
|
186
|
+
msg = f"{BINARY_NAME} {' '.join(args)} failed: {proc.stderr.strip()}"
|
|
187
|
+
raise RuntimeError(msg)
|
|
188
|
+
result: dict[str, Any] = json.loads(proc.stdout)
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _check_schema(schema: object) -> None:
|
|
193
|
+
if schema != IMAGE_SCHEMA_VERSION:
|
|
194
|
+
msg = (
|
|
195
|
+
f"{BINARY_NAME} emitted schema {schema!r}, but this package understands "
|
|
196
|
+
f"{IMAGE_SCHEMA_VERSION}. The binary and the Python wrapper are out of step."
|
|
197
|
+
)
|
|
198
|
+
raise RuntimeError(msg)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Run the bundled binary via ``python -m docker_devtools``.
|
|
2
|
+
|
|
3
|
+
The binary is normally on PATH already, since the wheel installs it into the
|
|
4
|
+
environment's scripts directory. This module exists for the cases where it is
|
|
5
|
+
not, such as an environment whose scripts directory is not on PATH.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from docker_devtools._find import find_binary
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _run() -> None:
|
|
18
|
+
binary = find_binary()
|
|
19
|
+
if sys.platform == "win32":
|
|
20
|
+
# Windows has no exec that replaces the process cleanly, and a
|
|
21
|
+
# KeyboardInterrupt here would print a traceback over the child's own
|
|
22
|
+
# output.
|
|
23
|
+
try:
|
|
24
|
+
completed = subprocess.run([binary, *sys.argv[1:]], check=False) # noqa: S603
|
|
25
|
+
except KeyboardInterrupt:
|
|
26
|
+
sys.exit(130)
|
|
27
|
+
sys.exit(completed.returncode)
|
|
28
|
+
else:
|
|
29
|
+
os.execv(binary, [binary, *sys.argv[1:]]) # noqa: S606
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == "__main__":
|
|
33
|
+
_run()
|
docker_devtools/_find.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Locate the bundled docker-devtools binary.
|
|
2
|
+
|
|
3
|
+
The search order is adapted from Astral's ``uv`` package (``python/uv/_find_uv.py``,
|
|
4
|
+
MIT OR Apache-2.0). The binary ships in the wheel's ``.data/scripts`` directory,
|
|
5
|
+
which installs into the environment's scripts directory rather than next to this
|
|
6
|
+
module, and that directory moves depending on how the wheel was installed --
|
|
7
|
+
``pip install --target``, ``--prefix``, the user scheme, or ``uv run --with``.
|
|
8
|
+
A naive ``os.path.dirname(__file__)`` lookup gets all of those wrong.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import sysconfig
|
|
16
|
+
from fnmatch import fnmatch
|
|
17
|
+
|
|
18
|
+
BINARY_NAME = "docker-devtools"
|
|
19
|
+
|
|
20
|
+
#: Set this to an absolute path to use a specific binary instead of searching.
|
|
21
|
+
#: Useful when running against a locally built binary, and as an escape hatch
|
|
22
|
+
#: for install layouts the search below does not anticipate.
|
|
23
|
+
BINARY_ENV_VAR = "DOCKER_DEVTOOLS_BINARY"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BinaryNotFoundError(FileNotFoundError):
|
|
27
|
+
"""The bundled binary could not be located."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def find_binary() -> str:
|
|
31
|
+
"""Return the path to the bundled docker-devtools binary.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
BinaryNotFoundError: if no candidate directory holds the binary.
|
|
35
|
+
"""
|
|
36
|
+
override = os.environ.get(BINARY_ENV_VAR)
|
|
37
|
+
if override:
|
|
38
|
+
if not os.path.isfile(override):
|
|
39
|
+
msg = f"{BINARY_ENV_VAR} points at {override!r}, which is not a file"
|
|
40
|
+
raise BinaryNotFoundError(msg)
|
|
41
|
+
return override
|
|
42
|
+
|
|
43
|
+
exe = BINARY_NAME + (sysconfig.get_config_var("EXE") or "")
|
|
44
|
+
|
|
45
|
+
targets = [
|
|
46
|
+
# The scripts directory for the current interpreter.
|
|
47
|
+
sysconfig.get_path("scripts"),
|
|
48
|
+
# The scripts directory for the base prefix, for virtualenvs.
|
|
49
|
+
sysconfig.get_path("scripts", vars={"base": sys.base_prefix}),
|
|
50
|
+
# Above the package root, e.g. `pip install --prefix` or `uv run --with`.
|
|
51
|
+
(
|
|
52
|
+
_join(
|
|
53
|
+
_matching_parents(_module_path(), "Lib/site-packages/docker_devtools"),
|
|
54
|
+
"Scripts",
|
|
55
|
+
)
|
|
56
|
+
if sys.platform == "win32"
|
|
57
|
+
else _join(
|
|
58
|
+
_matching_parents(_module_path(), "lib/python*/site-packages/docker_devtools"),
|
|
59
|
+
"bin",
|
|
60
|
+
)
|
|
61
|
+
),
|
|
62
|
+
# Adjacent to the package root, e.g. `pip install --target`.
|
|
63
|
+
_join(_matching_parents(_module_path(), "docker_devtools"), "bin"),
|
|
64
|
+
_matching_parents(_module_path(), "docker_devtools"),
|
|
65
|
+
# The user scheme's scripts directory, e.g. `~/.local/bin`.
|
|
66
|
+
sysconfig.get_path("scripts", scheme=_user_scheme()),
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
seen: list[str] = []
|
|
70
|
+
for target in targets:
|
|
71
|
+
if not target or target in seen:
|
|
72
|
+
continue
|
|
73
|
+
seen.append(target)
|
|
74
|
+
path = os.path.join(target, exe)
|
|
75
|
+
if os.path.isfile(path):
|
|
76
|
+
return path
|
|
77
|
+
|
|
78
|
+
locations = "\n".join(f" - {target}" for target in seen)
|
|
79
|
+
msg = f"Could not find the {BINARY_NAME} binary in any of:\n{locations}\n"
|
|
80
|
+
raise BinaryNotFoundError(msg)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _module_path() -> str:
|
|
84
|
+
return os.path.dirname(__file__)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _matching_parents(path: str | None, match: str) -> str | None:
|
|
88
|
+
"""Trim ``match`` off the end of ``path`` and return what is left.
|
|
89
|
+
|
|
90
|
+
``match`` uses ``/`` separators and may contain ``*`` wildcards; ``path``
|
|
91
|
+
uses the platform separator. Components compare case-insensitively.
|
|
92
|
+
"""
|
|
93
|
+
if not path:
|
|
94
|
+
return None
|
|
95
|
+
parts = path.split(os.sep)
|
|
96
|
+
match_parts = match.split("/")
|
|
97
|
+
if len(parts) < len(match_parts):
|
|
98
|
+
return None
|
|
99
|
+
if not all(
|
|
100
|
+
fnmatch(part, match_part)
|
|
101
|
+
for part, match_part in zip(reversed(parts), reversed(match_parts), strict=False)
|
|
102
|
+
):
|
|
103
|
+
return None
|
|
104
|
+
return os.sep.join(parts[: -len(match_parts)])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _join(path: str | None, *parts: str) -> str | None:
|
|
108
|
+
if not path:
|
|
109
|
+
return None
|
|
110
|
+
return os.path.join(path, *parts)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _user_scheme() -> str:
|
|
114
|
+
# uv's original branches on sys.version_info here because it supports 3.8.
|
|
115
|
+
# This package requires 3.10, where get_preferred_scheme always exists.
|
|
116
|
+
return sysconfig.get_preferred_scheme("user")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.0.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 0, 1)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
docker_devtools/py.typed
ADDED
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: docker-devtools
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Work on the Dockerfiles, Compose files and build context in a repository.
|
|
5
|
+
Project-URL: Documentation, https://github.com/FlavioAmurrioCS/docker-devtools#readme
|
|
6
|
+
Project-URL: Issues, https://github.com/FlavioAmurrioCS/docker-devtools/issues
|
|
7
|
+
Project-URL: Source, https://github.com/FlavioAmurrioCS/docker-devtools
|
|
8
|
+
Author-email: Flavio Amurrio <25621374+FlavioAmurrioCS@users.noreply.github.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: build-context,buildkit,docker,docker-compose,dockerfile,dockerignore,pre-commit
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Go
|
|
16
|
+
Classifier: Programming Language :: Python
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
23
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
24
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
25
|
+
Classifier: Topic :: Utilities
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# docker-devtools
|
|
30
|
+
|
|
31
|
+
Work on the Docker files in a repository: the build context a Dockerfile would
|
|
32
|
+
send, and the image references it and your Compose files point at.
|
|
33
|
+
|
|
34
|
+
```console
|
|
35
|
+
$ docker-devtools image ls
|
|
36
|
+
Dockerfile:1 python:3.11-slim
|
|
37
|
+
compose.yaml:3 nginx:1.25-alpine
|
|
38
|
+
|
|
39
|
+
$ docker-devtools image update --tag-policy same-pattern --dry-run
|
|
40
|
+
Dockerfile:1 python:3.11-slim -> python:3.14-slim (tag 3.11-slim -> 3.14-slim)
|
|
41
|
+
compose.yaml:3 nginx:1.25-alpine -> nginx:1.31-alpine (tag 1.25-alpine -> 1.31-alpine)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Why another one
|
|
45
|
+
|
|
46
|
+
Renovate and Dependabot already update image references, and they do it well.
|
|
47
|
+
They run as bots against a repository and open pull requests. This one runs on
|
|
48
|
+
your machine and edits the files in place. It is fast enough for a pre-commit
|
|
49
|
+
hook, so a stale base image gets caught before it is ever committed.
|
|
50
|
+
|
|
51
|
+
Where the semantics are Docker's, this defers to Docker's own code:
|
|
52
|
+
|
|
53
|
+
| Step | Package |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| Parse Dockerfiles | `moby/buildkit/frontend/dockerfile/parser` and `instructions` |
|
|
56
|
+
| Parse image references | `google/go-containerregistry/pkg/name` |
|
|
57
|
+
| Talk to registries | `google/go-containerregistry/pkg/v1/remote` |
|
|
58
|
+
| Match .dockerignore rules | `moby/patternmatcher` |
|
|
59
|
+
| Walk a build context | `tonistiigi/fsutil`, the package BuildKit sends contexts with |
|
|
60
|
+
|
|
61
|
+
None of the `.dockerignore` semantics are reimplemented here, and CI checks
|
|
62
|
+
that rather than asserting it: for every fixture, `scripts/conformance.sh`
|
|
63
|
+
builds `FROM scratch` with `COPY . /`, exports the image as a tarball, and
|
|
64
|
+
diffs the tar members against what `context ls` reports.
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
```console
|
|
69
|
+
$ uvx docker-devtools image ls # no install
|
|
70
|
+
$ pipx run docker-devtools image ls # no install
|
|
71
|
+
$ uv tool install docker-devtools
|
|
72
|
+
$ pip install docker-devtools
|
|
73
|
+
$ mise use ubi:FlavioAmurrioCS/docker-devtools
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Prebuilt binaries are attached to each
|
|
77
|
+
[release](https://github.com/FlavioAmurrioCS/docker-devtools/releases). With a
|
|
78
|
+
Go toolchain:
|
|
79
|
+
|
|
80
|
+
```console
|
|
81
|
+
$ go install github.com/FlavioAmurrioCS/docker-devtools/cmd/docker-devtools@latest
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Reading and updating files doesn't require a Docker installation or a running
|
|
85
|
+
daemon.
|
|
86
|
+
Registry lookups authenticate with the same `~/.docker/config.json` the docker
|
|
87
|
+
CLI uses.
|
|
88
|
+
|
|
89
|
+
## Usage
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
docker-devtools context ls [PATH] list the files Docker would send
|
|
93
|
+
docker-devtools context explain PATH show which .dockerignore rule decided a path
|
|
94
|
+
docker-devtools image ls [PATH...] list every image reference, with file and line
|
|
95
|
+
docker-devtools image update [PATH...] rewrite references in place
|
|
96
|
+
docker-devtools install-docker-plugin register as "docker devtools"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Updating image references
|
|
100
|
+
|
|
101
|
+
What changes is split by how much judgement it needs.
|
|
102
|
+
|
|
103
|
+
`--pin-digest` resolves the current tag to a digest and appends it, turning
|
|
104
|
+
`nginx:1.29` into `nginx:1.29@sha256:…`. It doesn't decide anything about versions, so it is
|
|
105
|
+
reversible and safe to run anywhere.
|
|
106
|
+
|
|
107
|
+
`--tag-policy` moves the tag. The default, `same-pattern`, moves only the last
|
|
108
|
+
component and keeps the suffix, so how specific your tag is decides how far it
|
|
109
|
+
may move:
|
|
110
|
+
|
|
111
|
+
| Current tag | same-pattern | minor | patch | latest |
|
|
112
|
+
| --- | --- | --- | --- | --- |
|
|
113
|
+
| `3.12-slim` | `3.13-slim` | `3.13-slim` | no change | `4.0-slim` |
|
|
114
|
+
| `3.12.1-slim` | `3.12.7-slim` | `3.13.0-slim` | `3.12.7-slim` | `4.0-slim` |
|
|
115
|
+
| `latest` | no change | no change | no change | no change |
|
|
116
|
+
|
|
117
|
+
No policy ever changes the suffix: `-alpine` and `-slim` are different images,
|
|
118
|
+
and swapping them would change your base distribution without saying so. Tags
|
|
119
|
+
with no version, such as `latest` or `bookworm`, are never moved, because there
|
|
120
|
+
is no ordering to move along.
|
|
121
|
+
|
|
122
|
+
Add `--dry-run` to see the plan without writing, and `--fail-on-diff` to exit
|
|
123
|
+
non-zero when anything would change, which is what makes it useful in CI.
|
|
124
|
+
|
|
125
|
+
### What it will not touch
|
|
126
|
+
|
|
127
|
+
Some references cannot be resolved to an image, and those are reported rather
|
|
128
|
+
than guessed at. Pass `--unresolved` to `image ls` to see them:
|
|
129
|
+
|
|
130
|
+
- `FROM builder`, where `builder` is an earlier stage
|
|
131
|
+
- `COPY --from=0`, which indexes a stage
|
|
132
|
+
- `FROM $BASE`, which depends on a build argument
|
|
133
|
+
- `FROM scratch`, which is the empty base rather than a registry image
|
|
134
|
+
- Compose values built from variables, such as `${REGISTRY}/app:latest`
|
|
135
|
+
|
|
136
|
+
### Editing in place
|
|
137
|
+
|
|
138
|
+
An update splices the new reference into the exact byte range the parser
|
|
139
|
+
reported. It never re-encodes the file, so comments, quoting style, anchors and
|
|
140
|
+
whitespace all survive:
|
|
141
|
+
|
|
142
|
+
```yaml
|
|
143
|
+
image: "nginx:1.29-alpine" # keep this comment and the quotes
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
becomes
|
|
147
|
+
|
|
148
|
+
```yaml
|
|
149
|
+
image: "nginx:1.31-alpine" # keep this comment and the quotes
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
If a byte range no longer holds the text the parse said it held, the update
|
|
153
|
+
fails instead of writing. A rewrite that has drifted from the parse is a bug,
|
|
154
|
+
and corrupting the file would hide it.
|
|
155
|
+
|
|
156
|
+
## Shell completion
|
|
157
|
+
|
|
158
|
+
The binary emits a [usage](https://usage.jdx.dev) spec describing its own
|
|
159
|
+
command tree, and the `usage` CLI turns that into completions for bash, zsh,
|
|
160
|
+
fish, powershell and nushell:
|
|
161
|
+
|
|
162
|
+
```console
|
|
163
|
+
$ mise use usage
|
|
164
|
+
$ usage g completion zsh docker-devtools --usage-cmd 'docker-devtools --usage-spec' --install
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The generated scripts call back to `usage` at completion time, so it has to stay
|
|
168
|
+
on your PATH. `mise run completions` regenerates all five, plus a markdown
|
|
169
|
+
reference, into `build/`.
|
|
170
|
+
|
|
171
|
+
## As a Docker CLI plugin
|
|
172
|
+
|
|
173
|
+
```console
|
|
174
|
+
$ docker-devtools install-docker-plugin
|
|
175
|
+
$ docker devtools image ls
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
This symlinks the binary into `~/.docker/cli-plugins/`. Use `--system` to
|
|
179
|
+
install it for every user.
|
|
180
|
+
|
|
181
|
+
The subcommand is `devtools` because Docker validates plugin names against
|
|
182
|
+
`^[a-z][a-z0-9]*$` and refuses to load anything else. Python wheels cannot do
|
|
183
|
+
this step at install time: they have no post-install hook, and
|
|
184
|
+
`~/.docker/cli-plugins/` sits outside every Python install path.
|
|
185
|
+
|
|
186
|
+
## Python API
|
|
187
|
+
|
|
188
|
+
The wheel bundles the binary and a typed wrapper.
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from docker_devtools import image_ls
|
|
192
|
+
from docker_devtools import image_update
|
|
193
|
+
|
|
194
|
+
for ref in image_ls(".").resolved():
|
|
195
|
+
print(f"{ref.path}:{ref.line}", ref.repository, ref.tag)
|
|
196
|
+
|
|
197
|
+
report = image_update(".", pin_digest=True, dry_run=True)
|
|
198
|
+
for change in report.changes:
|
|
199
|
+
print(change.old, "->", change.new, f"({change.reason})")
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`image_update` defaults to `dry_run=True`, so calling it by accident cannot
|
|
203
|
+
rewrite a repository.
|
|
204
|
+
|
|
205
|
+
## Development
|
|
206
|
+
|
|
207
|
+
`mise.toml` defines the tools and the tasks.
|
|
208
|
+
|
|
209
|
+
```console
|
|
210
|
+
$ mise run build # compile into ./build
|
|
211
|
+
$ mise run test # go test + pytest
|
|
212
|
+
$ mise run lint # pre-commit across the repo
|
|
213
|
+
$ mise run conformance # diff context listing against real docker build
|
|
214
|
+
$ mise run completions # regenerate completions and docs
|
|
215
|
+
$ mise run wheels # every platform wheel into ./dist
|
|
216
|
+
$ mise run test-clone # verify a fresh clone in a container
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Registry behaviour is tested against `go-containerregistry`'s in-process
|
|
220
|
+
registry, so the suite doesn't touch the network or carry recorded fixtures.
|
|
221
|
+
|
|
222
|
+
## License
|
|
223
|
+
|
|
224
|
+
MIT. See [LICENSE](LICENSE).
|
|
225
|
+
|
|
226
|
+
`src/docker_devtools/_find.py` adapts the binary-discovery search order from
|
|
227
|
+
[uv](https://github.com/astral-sh/uv), which is MIT OR Apache-2.0.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
docker_devtools/__init__.py,sha256=9aT8SmaV-5c6agMYBr2V5Q484FnHJ4c_xOyHaXlgZ5I,5361
|
|
2
|
+
docker_devtools/__main__.py,sha256=Z69rv0HFIRO51fDaDbkJGcVjKBU5xLJy5JwBVLlHicY,977
|
|
3
|
+
docker_devtools/_find.py,sha256=05nOAAWz_wfte_oC2xgLAdbGibxK8Lh8x_HaXGlPP34,4098
|
|
4
|
+
docker_devtools/_version.py,sha256=8OsTLsIVB9D0HdPTmt5rVwyVUBe9xTVGkRslXicxzkM,520
|
|
5
|
+
docker_devtools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
docker_devtools-0.0.1.data/scripts/docker-devtools.exe,sha256=GEq2HylYVqYkhKvNmG78JLPlqDa_jWLhHfTFmDEwego,11654144
|
|
7
|
+
docker_devtools-0.0.1.dist-info/METADATA,sha256=uuK1ZLszlCU6EPa0RCSPy931e-vFSS_mTK7FW5wshmw,8563
|
|
8
|
+
docker_devtools-0.0.1.dist-info/WHEEL,sha256=dSSxJmsrrjLMXPiCJqqR6Yi1PcmyV1gHC9udYo4AMyI,94
|
|
9
|
+
docker_devtools-0.0.1.dist-info/licenses/LICENSE,sha256=fbYeK7h6gKfDaN_ycO18fIdTlYGsYJZypz-JAR_-ybA,1071
|
|
10
|
+
docker_devtools-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Flavio Amurrio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|