docker-devtools 0.0.1__tar.gz
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-0.0.1/.gitignore +40 -0
- docker_devtools-0.0.1/LICENSE +21 -0
- docker_devtools-0.0.1/PKG-INFO +227 -0
- docker_devtools-0.0.1/README.md +199 -0
- docker_devtools-0.0.1/cmd/docker-devtools/context.go +166 -0
- docker_devtools-0.0.1/cmd/docker-devtools/image.go +105 -0
- docker_devtools-0.0.1/cmd/docker-devtools/main.go +119 -0
- docker_devtools-0.0.1/go.mod +37 -0
- docker_devtools-0.0.1/go.sum +76 -0
- docker_devtools-0.0.1/hatch_build.py +145 -0
- docker_devtools-0.0.1/internal/imgref/compose.go +89 -0
- docker_devtools-0.0.1/internal/imgref/dockerfile.go +119 -0
- docker_devtools-0.0.1/internal/imgref/imgref.go +121 -0
- docker_devtools-0.0.1/internal/imgref/imgref_test.go +146 -0
- docker_devtools-0.0.1/internal/imgref/scan.go +116 -0
- docker_devtools-0.0.1/internal/imgupdate/tagpolicy.go +153 -0
- docker_devtools-0.0.1/internal/imgupdate/tagpolicy_test.go +103 -0
- docker_devtools-0.0.1/internal/imgupdate/update.go +218 -0
- docker_devtools-0.0.1/internal/imgupdate/update_test.go +192 -0
- docker_devtools-0.0.1/internal/plugin/plugin.go +204 -0
- docker_devtools-0.0.1/internal/plugin/plugin_test.go +126 -0
- docker_devtools-0.0.1/internal/registry/registry.go +68 -0
- docker_devtools-0.0.1/internal/rewrite/rewrite.go +60 -0
- docker_devtools-0.0.1/internal/rewrite/rewrite_test.go +98 -0
- docker_devtools-0.0.1/pyproject.toml +249 -0
- docker_devtools-0.0.1/scripts/build-wheels.sh +40 -0
- docker_devtools-0.0.1/scripts/commit-msg-lint.sh +24 -0
- docker_devtools-0.0.1/scripts/completions.sh +43 -0
- docker_devtools-0.0.1/scripts/conformance.sh +66 -0
- docker_devtools-0.0.1/scripts/prose-lint.sh +33 -0
- docker_devtools-0.0.1/scripts/test-fresh-clone.sh +95 -0
- docker_devtools-0.0.1/src/docker_devtools/__init__.py +198 -0
- docker_devtools-0.0.1/src/docker_devtools/__main__.py +33 -0
- docker_devtools-0.0.1/src/docker_devtools/_find.py +116 -0
- docker_devtools-0.0.1/src/docker_devtools/_version.py +24 -0
- docker_devtools-0.0.1/src/docker_devtools/py.typed +0 -0
- docker_devtools-0.0.1/testdata/dctx/lowercase/all.golden +9 -0
- docker_devtools-0.0.1/testdata/dctx/lowercase/context/dockerfile +2 -0
- docker_devtools-0.0.1/testdata/dctx/lowercase/context/dockerfile.dockerignore +1 -0
- docker_devtools-0.0.1/testdata/dctx/lowercase/context/keep +1 -0
- docker_devtools-0.0.1/testdata/dctx/lowercase/context/secret +1 -0
- docker_devtools-0.0.1/testdata/dctx/lowercase/ignored.golden +6 -0
- docker_devtools-0.0.1/testdata/dctx/lowercase/included.golden +8 -0
- docker_devtools-0.0.1/testdata/dctx/none/all.golden +8 -0
- docker_devtools-0.0.1/testdata/dctx/none/context/Dockerfile +2 -0
- docker_devtools-0.0.1/testdata/dctx/none/context/a.txt +1 -0
- docker_devtools-0.0.1/testdata/dctx/none/context/b.txt +1 -0
- docker_devtools-0.0.1/testdata/dctx/none/ignored.golden +5 -0
- docker_devtools-0.0.1/testdata/dctx/none/included.golden +8 -0
- docker_devtools-0.0.1/testdata/dctx/order/all.golden +12 -0
- docker_devtools-0.0.1/testdata/dctx/order/context/.dockerignore +2 -0
- docker_devtools-0.0.1/testdata/dctx/order/context/CHANGELOG.md +1 -0
- docker_devtools-0.0.1/testdata/dctx/order/context/Dockerfile +2 -0
- docker_devtools-0.0.1/testdata/dctx/order/context/README.md +1 -0
- docker_devtools-0.0.1/testdata/dctx/order/context/docs/guide.md +1 -0
- docker_devtools-0.0.1/testdata/dctx/order/context/main.go +1 -0
- docker_devtools-0.0.1/testdata/dctx/order/ignored.golden +6 -0
- docker_devtools-0.0.1/testdata/dctx/order/included.golden +11 -0
- docker_devtools-0.0.1/testdata/dctx/percent/all.golden +9 -0
- docker_devtools-0.0.1/testdata/dctx/percent/case.json +1 -0
- docker_devtools-0.0.1/testdata/dctx/percent/context/nope +1 -0
- docker_devtools-0.0.1/testdata/dctx/percent/context/we%ird +2 -0
- docker_devtools-0.0.1/testdata/dctx/percent/context/we%ird.dockerignore +1 -0
- docker_devtools-0.0.1/testdata/dctx/percent/context/yes +1 -0
- docker_devtools-0.0.1/testdata/dctx/percent/ignored.golden +6 -0
- docker_devtools-0.0.1/testdata/dctx/percent/included.golden +8 -0
- docker_devtools-0.0.1/testdata/dctx/perdockerfile/all.golden +11 -0
- docker_devtools-0.0.1/testdata/dctx/perdockerfile/case.json +1 -0
- docker_devtools-0.0.1/testdata/dctx/perdockerfile/context/.dockerignore +1 -0
- docker_devtools-0.0.1/testdata/dctx/perdockerfile/context/Prj1 +2 -0
- docker_devtools-0.0.1/testdata/dctx/perdockerfile/context/Prj1.dockerignore +1 -0
- docker_devtools-0.0.1/testdata/dctx/perdockerfile/context/only-default +1 -0
- docker_devtools-0.0.1/testdata/dctx/perdockerfile/context/only-prj1 +1 -0
- docker_devtools-0.0.1/testdata/dctx/perdockerfile/context/other +1 -0
- docker_devtools-0.0.1/testdata/dctx/perdockerfile/ignored.golden +6 -0
- docker_devtools-0.0.1/testdata/dctx/perdockerfile/included.golden +10 -0
- docker_devtools-0.0.1/testdata/dctx/pycache/all.golden +18 -0
- docker_devtools-0.0.1/testdata/dctx/pycache/context/.dockerignore +1 -0
- docker_devtools-0.0.1/testdata/dctx/pycache/context/Dockerfile +2 -0
- docker_devtools-0.0.1/testdata/dctx/pycache/context/a/b/keep.py +1 -0
- docker_devtools-0.0.1/testdata/dctx/pycache/context/main.py +1 -0
- docker_devtools-0.0.1/testdata/dctx/pycache/context/pkg/mod.py +1 -0
- docker_devtools-0.0.1/testdata/dctx/pycache/ignored.golden +10 -0
- docker_devtools-0.0.1/testdata/dctx/pycache/included.golden +13 -0
- docker_devtools-0.0.1/testdata/dctx/reinclude/all.golden +13 -0
- docker_devtools-0.0.1/testdata/dctx/reinclude/context/.dockerignore +2 -0
- docker_devtools-0.0.1/testdata/dctx/reinclude/context/Dockerfile +2 -0
- docker_devtools-0.0.1/testdata/dctx/reinclude/context/app.js +1 -0
- docker_devtools-0.0.1/testdata/dctx/reinclude/context/node_modules/drop/index.js +1 -0
- docker_devtools-0.0.1/testdata/dctx/reinclude/context/node_modules/keep/index.js +1 -0
- docker_devtools-0.0.1/testdata/dctx/reinclude/ignored.golden +7 -0
- docker_devtools-0.0.1/testdata/dctx/reinclude/included.golden +11 -0
- docker_devtools-0.0.1/testdata/dctx/trailing/all.golden +12 -0
- docker_devtools-0.0.1/testdata/dctx/trailing/context/.dockerignore +1 -0
- docker_devtools-0.0.1/testdata/dctx/trailing/context/Dockerfile +2 -0
- docker_devtools-0.0.1/testdata/dctx/trailing/context/build/out.bin +1 -0
- docker_devtools-0.0.1/testdata/dctx/trailing/context/build/sub/deep.bin +1 -0
- docker_devtools-0.0.1/testdata/dctx/trailing/context/src.go +1 -0
- docker_devtools-0.0.1/testdata/dctx/trailing/ignored.golden +9 -0
- docker_devtools-0.0.1/testdata/dctx/trailing/included.golden +8 -0
- docker_devtools-0.0.1/testdata/imageref/compose/compose.yaml +25 -0
- docker_devtools-0.0.1/testdata/imageref/compose/expected.txt +4 -0
- docker_devtools-0.0.1/testdata/imageref/edge/Dockerfile +2 -0
- docker_devtools-0.0.1/testdata/imageref/edge/Dockerfile.pinned +2 -0
- docker_devtools-0.0.1/testdata/imageref/edge/expected.txt +2 -0
- docker_devtools-0.0.1/testdata/imageref/multistage/Dockerfile +17 -0
- docker_devtools-0.0.1/testdata/imageref/multistage/expected.txt +7 -0
- docker_devtools-0.0.1/tests/__init__.py +0 -0
- docker_devtools-0.0.1/tests/api_test.py +63 -0
- docker_devtools-0.0.1/tests/binary_test.py +54 -0
- docker_devtools-0.0.1/tests/conftest.py +67 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Go
|
|
2
|
+
/build/
|
|
3
|
+
*.test
|
|
4
|
+
|
|
5
|
+
# Python
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
dist/
|
|
11
|
+
build/
|
|
12
|
+
*.egg-info/
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.coverage
|
|
17
|
+
.coverage.*
|
|
18
|
+
coverage.xml
|
|
19
|
+
htmlcov/
|
|
20
|
+
|
|
21
|
+
# hatch-vcs writes this at build time
|
|
22
|
+
_version.py
|
|
23
|
+
|
|
24
|
+
# uv
|
|
25
|
+
uv.lock
|
|
26
|
+
requirements.txt
|
|
27
|
+
|
|
28
|
+
# vale style packs are fetched by `vale sync`
|
|
29
|
+
.vale-styles/
|
|
30
|
+
.vale-commits/styles/
|
|
31
|
+
.vale-commits/msg.md
|
|
32
|
+
|
|
33
|
+
.DS_Store
|
|
34
|
+
|
|
35
|
+
# The fixture contexts are the data under test. They deliberately contain
|
|
36
|
+
# directories the rules above ignore (__pycache__, build/), because ignoring
|
|
37
|
+
# those is exactly the behaviour being checked. Re-include everything under
|
|
38
|
+
# testdata; the negation has to name the directories too, since git will not
|
|
39
|
+
# descend into an excluded one.
|
|
40
|
+
!/testdata/**
|
|
@@ -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.
|
|
@@ -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,199 @@
|
|
|
1
|
+
# docker-devtools
|
|
2
|
+
|
|
3
|
+
Work on the Docker files in a repository: the build context a Dockerfile would
|
|
4
|
+
send, and the image references it and your Compose files point at.
|
|
5
|
+
|
|
6
|
+
```console
|
|
7
|
+
$ docker-devtools image ls
|
|
8
|
+
Dockerfile:1 python:3.11-slim
|
|
9
|
+
compose.yaml:3 nginx:1.25-alpine
|
|
10
|
+
|
|
11
|
+
$ docker-devtools image update --tag-policy same-pattern --dry-run
|
|
12
|
+
Dockerfile:1 python:3.11-slim -> python:3.14-slim (tag 3.11-slim -> 3.14-slim)
|
|
13
|
+
compose.yaml:3 nginx:1.25-alpine -> nginx:1.31-alpine (tag 1.25-alpine -> 1.31-alpine)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Why another one
|
|
17
|
+
|
|
18
|
+
Renovate and Dependabot already update image references, and they do it well.
|
|
19
|
+
They run as bots against a repository and open pull requests. This one runs on
|
|
20
|
+
your machine and edits the files in place. It is fast enough for a pre-commit
|
|
21
|
+
hook, so a stale base image gets caught before it is ever committed.
|
|
22
|
+
|
|
23
|
+
Where the semantics are Docker's, this defers to Docker's own code:
|
|
24
|
+
|
|
25
|
+
| Step | Package |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| Parse Dockerfiles | `moby/buildkit/frontend/dockerfile/parser` and `instructions` |
|
|
28
|
+
| Parse image references | `google/go-containerregistry/pkg/name` |
|
|
29
|
+
| Talk to registries | `google/go-containerregistry/pkg/v1/remote` |
|
|
30
|
+
| Match .dockerignore rules | `moby/patternmatcher` |
|
|
31
|
+
| Walk a build context | `tonistiigi/fsutil`, the package BuildKit sends contexts with |
|
|
32
|
+
|
|
33
|
+
None of the `.dockerignore` semantics are reimplemented here, and CI checks
|
|
34
|
+
that rather than asserting it: for every fixture, `scripts/conformance.sh`
|
|
35
|
+
builds `FROM scratch` with `COPY . /`, exports the image as a tarball, and
|
|
36
|
+
diffs the tar members against what `context ls` reports.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```console
|
|
41
|
+
$ uvx docker-devtools image ls # no install
|
|
42
|
+
$ pipx run docker-devtools image ls # no install
|
|
43
|
+
$ uv tool install docker-devtools
|
|
44
|
+
$ pip install docker-devtools
|
|
45
|
+
$ mise use ubi:FlavioAmurrioCS/docker-devtools
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Prebuilt binaries are attached to each
|
|
49
|
+
[release](https://github.com/FlavioAmurrioCS/docker-devtools/releases). With a
|
|
50
|
+
Go toolchain:
|
|
51
|
+
|
|
52
|
+
```console
|
|
53
|
+
$ go install github.com/FlavioAmurrioCS/docker-devtools/cmd/docker-devtools@latest
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Reading and updating files doesn't require a Docker installation or a running
|
|
57
|
+
daemon.
|
|
58
|
+
Registry lookups authenticate with the same `~/.docker/config.json` the docker
|
|
59
|
+
CLI uses.
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
docker-devtools context ls [PATH] list the files Docker would send
|
|
65
|
+
docker-devtools context explain PATH show which .dockerignore rule decided a path
|
|
66
|
+
docker-devtools image ls [PATH...] list every image reference, with file and line
|
|
67
|
+
docker-devtools image update [PATH...] rewrite references in place
|
|
68
|
+
docker-devtools install-docker-plugin register as "docker devtools"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Updating image references
|
|
72
|
+
|
|
73
|
+
What changes is split by how much judgement it needs.
|
|
74
|
+
|
|
75
|
+
`--pin-digest` resolves the current tag to a digest and appends it, turning
|
|
76
|
+
`nginx:1.29` into `nginx:1.29@sha256:…`. It doesn't decide anything about versions, so it is
|
|
77
|
+
reversible and safe to run anywhere.
|
|
78
|
+
|
|
79
|
+
`--tag-policy` moves the tag. The default, `same-pattern`, moves only the last
|
|
80
|
+
component and keeps the suffix, so how specific your tag is decides how far it
|
|
81
|
+
may move:
|
|
82
|
+
|
|
83
|
+
| Current tag | same-pattern | minor | patch | latest |
|
|
84
|
+
| --- | --- | --- | --- | --- |
|
|
85
|
+
| `3.12-slim` | `3.13-slim` | `3.13-slim` | no change | `4.0-slim` |
|
|
86
|
+
| `3.12.1-slim` | `3.12.7-slim` | `3.13.0-slim` | `3.12.7-slim` | `4.0-slim` |
|
|
87
|
+
| `latest` | no change | no change | no change | no change |
|
|
88
|
+
|
|
89
|
+
No policy ever changes the suffix: `-alpine` and `-slim` are different images,
|
|
90
|
+
and swapping them would change your base distribution without saying so. Tags
|
|
91
|
+
with no version, such as `latest` or `bookworm`, are never moved, because there
|
|
92
|
+
is no ordering to move along.
|
|
93
|
+
|
|
94
|
+
Add `--dry-run` to see the plan without writing, and `--fail-on-diff` to exit
|
|
95
|
+
non-zero when anything would change, which is what makes it useful in CI.
|
|
96
|
+
|
|
97
|
+
### What it will not touch
|
|
98
|
+
|
|
99
|
+
Some references cannot be resolved to an image, and those are reported rather
|
|
100
|
+
than guessed at. Pass `--unresolved` to `image ls` to see them:
|
|
101
|
+
|
|
102
|
+
- `FROM builder`, where `builder` is an earlier stage
|
|
103
|
+
- `COPY --from=0`, which indexes a stage
|
|
104
|
+
- `FROM $BASE`, which depends on a build argument
|
|
105
|
+
- `FROM scratch`, which is the empty base rather than a registry image
|
|
106
|
+
- Compose values built from variables, such as `${REGISTRY}/app:latest`
|
|
107
|
+
|
|
108
|
+
### Editing in place
|
|
109
|
+
|
|
110
|
+
An update splices the new reference into the exact byte range the parser
|
|
111
|
+
reported. It never re-encodes the file, so comments, quoting style, anchors and
|
|
112
|
+
whitespace all survive:
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
image: "nginx:1.29-alpine" # keep this comment and the quotes
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
becomes
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
image: "nginx:1.31-alpine" # keep this comment and the quotes
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
If a byte range no longer holds the text the parse said it held, the update
|
|
125
|
+
fails instead of writing. A rewrite that has drifted from the parse is a bug,
|
|
126
|
+
and corrupting the file would hide it.
|
|
127
|
+
|
|
128
|
+
## Shell completion
|
|
129
|
+
|
|
130
|
+
The binary emits a [usage](https://usage.jdx.dev) spec describing its own
|
|
131
|
+
command tree, and the `usage` CLI turns that into completions for bash, zsh,
|
|
132
|
+
fish, powershell and nushell:
|
|
133
|
+
|
|
134
|
+
```console
|
|
135
|
+
$ mise use usage
|
|
136
|
+
$ usage g completion zsh docker-devtools --usage-cmd 'docker-devtools --usage-spec' --install
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The generated scripts call back to `usage` at completion time, so it has to stay
|
|
140
|
+
on your PATH. `mise run completions` regenerates all five, plus a markdown
|
|
141
|
+
reference, into `build/`.
|
|
142
|
+
|
|
143
|
+
## As a Docker CLI plugin
|
|
144
|
+
|
|
145
|
+
```console
|
|
146
|
+
$ docker-devtools install-docker-plugin
|
|
147
|
+
$ docker devtools image ls
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
This symlinks the binary into `~/.docker/cli-plugins/`. Use `--system` to
|
|
151
|
+
install it for every user.
|
|
152
|
+
|
|
153
|
+
The subcommand is `devtools` because Docker validates plugin names against
|
|
154
|
+
`^[a-z][a-z0-9]*$` and refuses to load anything else. Python wheels cannot do
|
|
155
|
+
this step at install time: they have no post-install hook, and
|
|
156
|
+
`~/.docker/cli-plugins/` sits outside every Python install path.
|
|
157
|
+
|
|
158
|
+
## Python API
|
|
159
|
+
|
|
160
|
+
The wheel bundles the binary and a typed wrapper.
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from docker_devtools import image_ls
|
|
164
|
+
from docker_devtools import image_update
|
|
165
|
+
|
|
166
|
+
for ref in image_ls(".").resolved():
|
|
167
|
+
print(f"{ref.path}:{ref.line}", ref.repository, ref.tag)
|
|
168
|
+
|
|
169
|
+
report = image_update(".", pin_digest=True, dry_run=True)
|
|
170
|
+
for change in report.changes:
|
|
171
|
+
print(change.old, "->", change.new, f"({change.reason})")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`image_update` defaults to `dry_run=True`, so calling it by accident cannot
|
|
175
|
+
rewrite a repository.
|
|
176
|
+
|
|
177
|
+
## Development
|
|
178
|
+
|
|
179
|
+
`mise.toml` defines the tools and the tasks.
|
|
180
|
+
|
|
181
|
+
```console
|
|
182
|
+
$ mise run build # compile into ./build
|
|
183
|
+
$ mise run test # go test + pytest
|
|
184
|
+
$ mise run lint # pre-commit across the repo
|
|
185
|
+
$ mise run conformance # diff context listing against real docker build
|
|
186
|
+
$ mise run completions # regenerate completions and docs
|
|
187
|
+
$ mise run wheels # every platform wheel into ./dist
|
|
188
|
+
$ mise run test-clone # verify a fresh clone in a container
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Registry behaviour is tested against `go-containerregistry`'s in-process
|
|
192
|
+
registry, so the suite doesn't touch the network or carry recorded fixtures.
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT. See [LICENSE](LICENSE).
|
|
197
|
+
|
|
198
|
+
`src/docker_devtools/_find.py` adapts the binary-discovery search order from
|
|
199
|
+
[uv](https://github.com/astral-sh/uv), which is MIT OR Apache-2.0.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"io"
|
|
8
|
+
"strings"
|
|
9
|
+
|
|
10
|
+
"github.com/FlavioAmurrioCS/docker-devtools/dctx"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// ContextCmd groups the build-context commands. The listing itself is in the
|
|
14
|
+
// dctx package, whose output scripts/conformance.sh diffs against a real
|
|
15
|
+
// docker build for every fixture.
|
|
16
|
+
type ContextCmd struct {
|
|
17
|
+
Ls ContextLsCmd `cmd:"" help:"List the files Docker sends as the build context."`
|
|
18
|
+
Explain ContextExplainCmd `cmd:"" help:"Show which .dockerignore rule decided a path."`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type ContextLsCmd struct {
|
|
22
|
+
Path string `arg:"" optional:"" default:"." help:"Build context directory." type:"path"`
|
|
23
|
+
|
|
24
|
+
File string `short:"f" placeholder:"NAME" help:"Dockerfile name, which also selects <name>.dockerignore."`
|
|
25
|
+
Ignored bool `xor:"mode" help:"List the excluded files instead of the included ones."`
|
|
26
|
+
All bool `xor:"mode" help:"List every file, prefixed + for sent and - for excluded."`
|
|
27
|
+
Size bool `help:"Prefix each path with its size in bytes."`
|
|
28
|
+
Summary bool `help:"Print totals to stderr after the listing."`
|
|
29
|
+
JSON bool `name:"json" help:"Emit the full result as JSON."`
|
|
30
|
+
Zero bool `short:"0" help:"Separate paths with NUL, for xargs -0."`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func (c *ContextLsCmd) Run(st *Streams) error {
|
|
34
|
+
mode := dctx.ModeIncluded
|
|
35
|
+
switch {
|
|
36
|
+
case c.All:
|
|
37
|
+
mode = dctx.ModeAll
|
|
38
|
+
case c.Ignored:
|
|
39
|
+
mode = dctx.ModeIgnored
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
res, err := dctx.Walk(context.Background(), dctx.Options{
|
|
43
|
+
Context: c.Path,
|
|
44
|
+
Dockerfile: c.File,
|
|
45
|
+
Mode: mode,
|
|
46
|
+
})
|
|
47
|
+
if err != nil {
|
|
48
|
+
return err
|
|
49
|
+
}
|
|
50
|
+
for _, w := range res.Warnings {
|
|
51
|
+
fmt.Fprintln(st.Stderr, "warning:", w)
|
|
52
|
+
}
|
|
53
|
+
if c.JSON {
|
|
54
|
+
return writeJSON(st.Stdout, res)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
sep := "\n"
|
|
58
|
+
if c.Zero {
|
|
59
|
+
sep = "\x00"
|
|
60
|
+
}
|
|
61
|
+
for _, e := range res.Entries {
|
|
62
|
+
var b strings.Builder
|
|
63
|
+
if c.All {
|
|
64
|
+
if e.Status == dctx.StatusIncluded {
|
|
65
|
+
b.WriteString("+ ")
|
|
66
|
+
} else {
|
|
67
|
+
b.WriteString("- ")
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if c.Size {
|
|
71
|
+
fmt.Fprintf(&b, "%10d ", e.Size)
|
|
72
|
+
}
|
|
73
|
+
b.WriteString(e.Path)
|
|
74
|
+
b.WriteString(sep)
|
|
75
|
+
if _, err := stringWrite(st.Stdout, b.String()); err != nil {
|
|
76
|
+
return err
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if c.Summary {
|
|
80
|
+
printSummary(st.Stderr, res)
|
|
81
|
+
}
|
|
82
|
+
return nil
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type ContextExplainCmd struct {
|
|
86
|
+
Path string `arg:"" help:"Path to explain, relative to the context or absolute. It need not exist."`
|
|
87
|
+
|
|
88
|
+
Dir string `short:"C" default:"." placeholder:"DIR" help:"Build context directory." type:"path"`
|
|
89
|
+
File string `short:"f" placeholder:"NAME" help:"Dockerfile name, which also selects <name>.dockerignore."`
|
|
90
|
+
JSON bool `name:"json" help:"Emit the explanation as JSON."`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func (c *ContextExplainCmd) Run(st *Streams) error {
|
|
94
|
+
exp, err := dctx.Explain(dctx.Options{Context: c.Dir, Dockerfile: c.File}, c.Path)
|
|
95
|
+
if err != nil {
|
|
96
|
+
return err
|
|
97
|
+
}
|
|
98
|
+
if c.JSON {
|
|
99
|
+
return writeJSON(st.Stdout, exp)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fmt.Fprintf(st.Stdout, "%s: %s\n", exp.Path, exp.Status)
|
|
103
|
+
if !exp.Exists {
|
|
104
|
+
fmt.Fprintln(st.Stdout, " (path does not exist in the context)")
|
|
105
|
+
}
|
|
106
|
+
label := exp.Ignorefile
|
|
107
|
+
if label == "" {
|
|
108
|
+
label = "(no ignore file)"
|
|
109
|
+
}
|
|
110
|
+
if len(exp.Rules) == 0 {
|
|
111
|
+
fmt.Fprintf(st.Stdout, " no rule in %s matches; included by default\n", label)
|
|
112
|
+
return nil
|
|
113
|
+
}
|
|
114
|
+
for _, r := range exp.Rules {
|
|
115
|
+
effect := "ignored"
|
|
116
|
+
if r.Negated {
|
|
117
|
+
effect = "re-included"
|
|
118
|
+
}
|
|
119
|
+
line := fmt.Sprintf(" %s:%d", label, r.Line)
|
|
120
|
+
fmt.Fprintf(st.Stdout, "%-24s %-24s %s", line, r.Rule, effect)
|
|
121
|
+
if r.Decisive {
|
|
122
|
+
fmt.Fprint(st.Stdout, " (decisive)")
|
|
123
|
+
}
|
|
124
|
+
fmt.Fprintln(st.Stdout)
|
|
125
|
+
}
|
|
126
|
+
return nil
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func printSummary(w io.Writer, res *dctx.Result) {
|
|
130
|
+
fmt.Fprintf(w, "included: %s, %s\n",
|
|
131
|
+
plural(res.Summary.Included.Files, "file"), humanBytes(res.Summary.Included.Bytes))
|
|
132
|
+
if res.Summary.Ignored != nil {
|
|
133
|
+
fmt.Fprintf(w, "ignored: %s, %s\n",
|
|
134
|
+
plural(res.Summary.Ignored.Files, "file"), humanBytes(res.Summary.Ignored.Bytes))
|
|
135
|
+
} else {
|
|
136
|
+
fmt.Fprintln(w, "ignored: not counted (ignored directories were skipped; use --all)")
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
func plural(n int64, noun string) string {
|
|
141
|
+
if n == 1 {
|
|
142
|
+
return fmt.Sprintf("%d %s", n, noun)
|
|
143
|
+
}
|
|
144
|
+
return fmt.Sprintf("%d %ss", n, noun)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
func humanBytes(n int64) string {
|
|
148
|
+
const unit = 1024
|
|
149
|
+
if n < unit {
|
|
150
|
+
return fmt.Sprintf("%d B", n)
|
|
151
|
+
}
|
|
152
|
+
div, exp := int64(unit), 0
|
|
153
|
+
for v := n / unit; v >= unit; v /= unit {
|
|
154
|
+
div *= unit
|
|
155
|
+
exp++
|
|
156
|
+
}
|
|
157
|
+
return fmt.Sprintf("%.1f %ciB", float64(n)/float64(div), "KMGTPE"[exp])
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
func writeJSON(w io.Writer, v any) error {
|
|
161
|
+
enc := json.NewEncoder(w)
|
|
162
|
+
enc.SetIndent("", " ")
|
|
163
|
+
return enc.Encode(v)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
func stringWrite(w io.Writer, s string) (int, error) { return io.WriteString(w, s) }
|