docker-devkit 0.1.14__tar.gz → 0.1.16__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_devkit-0.1.14 → docker_devkit-0.1.16}/AGENTS.md +6 -5
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/PKG-INFO +1 -1
- docker_devkit-0.1.16/plan.md +145 -0
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/pyproject.toml +7 -2
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/src/docker_devkit/build_docker.py +31 -48
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/src/docker_devkit/context_sha.py +6 -9
- docker_devkit-0.1.16/src/docker_devkit/documents.py +64 -0
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/src/docker_devkit/down.py +2 -1
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/src/docker_devkit/image_refs.py +26 -71
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/src/docker_devkit/score_generate.py +2 -1
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/src/docker_devkit/up.py +2 -1
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/tests/test_context_sha.py +13 -12
- docker_devkit-0.1.16/tests/test_default_targets.py +86 -0
- docker_devkit-0.1.16/tests/test_documents.py +63 -0
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/tests/test_image_refs.py +38 -57
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/uv.lock +11 -0
- docker_devkit-0.1.14/plan.md +0 -122
- docker_devkit-0.1.14/tests/test_default_targets.py +0 -60
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/.gitattributes +0 -0
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/.github/workflows/ci.yml +0 -0
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/.github/workflows/release.yml +0 -0
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/.gitignore +0 -0
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/.python-version +0 -0
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/CLAUDE.md +0 -0
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/LICENSE +0 -0
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/NOTICE +0 -0
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/README.md +0 -0
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/publish-config.json +0 -0
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/ruff.base.toml +0 -0
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/ruff.toml +0 -0
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/src/docker_devkit/__init__.py +0 -0
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/src/docker_devkit/detect_gpu.py +0 -0
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/src/docker_devkit/lifecycle.py +0 -0
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/src/docker_devkit/modes.py +0 -0
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/src/docker_devkit/py.typed +0 -0
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/tests/test_lifecycle.py +0 -0
- {docker_devkit-0.1.14 → docker_devkit-0.1.16}/tests/test_score_generate.py +0 -0
|
@@ -18,13 +18,14 @@ Entry points (`[project.scripts]`): `up` → `up.py:app`, `down` → `down.py:ap
|
|
|
18
18
|
|---|---|
|
|
19
19
|
| `up.py` | `docker compose up`. Flags: `--attached`/`-a`, `--quiet-pull`/`-q`, `--build`, `--gpu auto\|cuda\|rocm\|none`, `--dev`. |
|
|
20
20
|
| `down.py` | `docker compose down`. Flags: `--volumes`/`-v` (also removes named volumes), `--gpu`. |
|
|
21
|
-
| `build_docker.py` | `run_build()` + the `build` command — cross-builds all images per `compose.bake.yml`, writes `.env.lock` (
|
|
21
|
+
| `build_docker.py` | `run_build()` + the `build` command — cross-builds all images per `compose.bake.yml`, writes `.env.lock` (one `NAME=path:tag@sha256:…` entry per `x-base-images` declaration; `@digest`-pinned declarations pass through unresolved) and `.env.shas` (locally-built `tree-<hash>` tags). `--mode ci` hard-requires `x-registry-cache` in the bake file. |
|
|
22
22
|
| `score_generate.py` | `generate-score` — deterministic Score-manifest generation from a consumer's `score/` sources: `${ALL_CAPS}` placeholder expansion into tempdir-rendered workload copies, idempotent `score-compose`/`score-k8s` init (state preserved across runs), and a determinism suite (document sort, state-path normalisation, 0644 modes, LF newlines). Consumer policy — workloads, provisioners, patch templates, output paths, storage classes, publishes, tool versions — is declared in the consumer's root `pyproject.toml` under `[tool.docker-devkit.generate-score]` (`ScoreConfig`); `load_score_config()` reads it, `ensure_score_tools()` fetches the pinned score-compose/score-k8s release binaries (for CI staleness gates). `--local` writes the k8s artifact to the configured gitignored local path with the local storage class. |
|
|
23
23
|
| `lifecycle.py` | `LifecycleConfig` — the `[tool.docker-devkit.lifecycle]` declaration (`files`: compose files in layering order with `{gpu}` templating; `dev_file`: optional dev overlay); `load_lifecycle_config()` (walks `tool → docker-devkit → lifecycle`, `None` when absent), `enforce_bake_declaration()` (a canonical `compose.bake.yml` requires a declaration), `expand_compose_files()` (assembles the `-f` list; a `{gpu}` entry is dropped when the GPU resolves to `none`; `dev_file` appended when a dev overlay applies). |
|
|
24
24
|
| `detect_gpu.py` | `detect_gpu()` → `cuda`/`rocm`/`none` from host devices; the `Gpu` literal. |
|
|
25
25
|
| `modes.py` | `resolve_auth_mode()` — validates `PUBLIC_URL` + `AUTH_MODE` from `.env`, rejecting `keycloak` over cleartext `http://`. |
|
|
26
|
-
| `context_sha.py` | `compute_service_shas()` — per-service `tree-<hash>` image tags over the `.dockerignore`-allowlisted git tree. |
|
|
27
|
-
| `
|
|
26
|
+
| `context_sha.py` | `compute_service_shas(repo_root, bake: BakeDocument)` — per-service `tree-<hash>` image tags over the `.dockerignore`-allowlisted git tree. |
|
|
27
|
+
| `documents.py` | `BakeDocument` + `parse_bake(path)` — the typed bake-file model every consumer of bake data goes through (build, up, down, generate-score, the reference collectors). A field validator enforces at parse time that every `x-base-images` value is `path:tag` or `path@digest` (a `${…}` hole or a bare path is a validation error, killing the silent-unpinned `FROM` → `:latest` failure mode). `digest_pinned(reference)` is the shared tail test. |
|
|
28
|
+
| `image_refs.py` | Image-reference collection, split by source. `declared_references(root)` enumerates bake `x-base-images` declarations (parsed and validated via `BakeDocument`) — the single source consumer mirroring consumes. `stray_references(root, …globs)` scans Dockerfile `FROM`, Dockerfile `COPY --from=` (slash-bearing captures only, so build-stage names are skipped), and yaml `image:` lines — hand-written literal refs that bypass the declaration system; `unpinned_references(root)` polices exactly those strays (every slash-bearing reference must carry a tag, a digest, or a trailing `${build-arg}` suffix — consumer CIs fail on its result). `bake_base_image_refs(bake: BakeDocument)` adapts declarations to `ImageReference` pairs; `version_coupling_violations(root, couplings)` (each `VersionCoupling` names a pyproject-declared version and the `VersionSite` regexes that must restate it — bake `x-base-images` values or raw file text — so non-derivable couplings like uv `required-version` ↔ uv-base-image tag can't drift silently; the mechanism lives here, the registry in consumer repos' preflight) is unchanged. Remote digest resolution goes through `imagetools inspect`. Compose files are parsed by nothing in this package. |
|
|
28
29
|
|
|
29
30
|
## Constraints
|
|
30
31
|
|
|
@@ -32,14 +33,14 @@ Entry points (`[project.scripts]`): `up` → `up.py:app`, `down` → `down.py:ap
|
|
|
32
33
|
|
|
33
34
|
**Context.** A repo that authors its own stack builds its own images and assembles a multi-file compose graph (e.g. `compose.yml` + `compose.postgres.yml` + a GPU layer + `compose.dev.yml`), injecting per-service `${*_SHA}` image tags and a `.env.lock` of third-party digests. A consumer repo has none of that: it ships a single self-contained compose file that OCI-includes the upstream artifact already baked (image digests and internal vars frozen as literals) or includes a sibling checkout (which supplies its own `.env.lock`/`.env.shas` through the include's `env_file`).
|
|
34
35
|
|
|
35
|
-
**Constraint.** `up`/`down` assemble the graph from `[tool.docker-devkit.lifecycle]` in the consumer's root pyproject — one code path, declared data. Keys: `files` (compose files in layering order, `{gpu}` templating, a `{gpu}` entry dropped when the GPU resolves to `none`; defaults to empty), `dev_file` (dev overlay, appended by `up --dev` and always by `down` so teardown removes containers from prior dev bring-ups). No table → single-file default (`-f compose.yml`). `--compose-file` does not exist; raw `docker compose` is the pressure valve. `--dev` is opt-in — production-shaped bring-up is the default. `.env.lock` is required unconditionally, no knob; consumers pin stock images via `${…
|
|
36
|
+
**Constraint.** `up`/`down` assemble the graph from `[tool.docker-devkit.lifecycle]` in the consumer's root pyproject — one code path, declared data. Keys: `files` (compose files in layering order, `{gpu}` templating, a `{gpu}` entry dropped when the GPU resolves to `none`; defaults to empty), `dev_file` (dev overlay, appended by `up --dev` and always by `down` so teardown removes containers from prior dev bring-ups). No table → single-file default (`-f compose.yml`). `--compose-file` does not exist; raw `docker compose` is the pressure valve. `--dev` is opt-in — production-shaped bring-up is the default. `.env.lock` is required unconditionally, no knob; consumers pin stock images via `${…_IMAGE:?err}` holes filled by a committed lock. A canonical `compose.bake.yml` present without a lifecycle declaration is a hard error (declare the table, or the file doesn't belong under that name) — guards are budgeted for silent failures only, and a missing lock already fails loudly. The bake file remains the build/up contract: SHA injection and `--build` acceptance key on `compose.bake.yml` existing, and `build` always reads exactly that name. An empty assembly (`files` empty/omitted, no dev overlay) is a loud `up`/`down` error — the honest state for a builds-only repo.
|
|
36
37
|
|
|
37
38
|
**Consequences.** placeframe's shape lives in placeframe's pyproject, not in this package: `files = ["compose.yml", "compose.postgres.yml", "compose.{gpu}.yml"]`, `dev_file = "compose.dev.yml"`. A consumer repo declares a bare table (builds images, no repo-local runtime graph) and runs `uv run up` against its own `.env` + committed `.env.lock`.
|
|
38
39
|
|
|
39
40
|
### File and env conventions the commands assume
|
|
40
41
|
|
|
41
42
|
- `.env` (required) — carries `PUBLIC_URL` and `AUTH_MODE`; `modes.py` reads it and rejects `keycloak` over cleartext `http://`.
|
|
42
|
-
- `.env.lock` (required in every repo, checked in) — third-party and base-image
|
|
43
|
+
- `.env.lock` (required in every repo, checked in) — third-party and base-image pins, written by `build`: one `NAME=path:tag@sha256:…` entry per bake declaration. `up`/`down` refuse without it; consumers satisfy it with a committed lock filling the `${…_IMAGE:?err}` holes.
|
|
43
44
|
- `.env.shas` (where images are built locally, gitignored, per-build) — `tree-<hash>` tags of locally-built images, written by `build`. `up` injects the `${*_SHA}` values into the environment directly and reads neither file; `.env.shas` exists so a consumer running the compose files through raw `docker compose` can resolve those holes via `--env-file .env.shas`.
|
|
44
45
|
- `compose.bake.yml` (where a local build graph exists, required) — the buildx-bake description; its top-level `x-cross-compile-targets` list (optional) is the set of services excluded from the default target list because they need per-arch treatment, opt-in via `--targets`. One bake file name, ever: `build` reads exactly this name.
|
|
45
46
|
- the compose layers declared in `[tool.docker-devkit.lifecycle]` — e.g. placeframe's `compose.yml` / `compose.postgres.yml` / `compose.<gpu>.yml` / `compose.dev.yml`; the GPU one is picked by `detect_gpu()` (or the `--gpu` override).
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# One home for third-party image declarations; docker-devkit stops parsing compose
|
|
2
|
+
|
|
3
|
+
Status note for pickup: COMPLETE. Shipped: docker-devkit 0.1.15, release-devkit
|
|
4
|
+
0.1.11 (both on PyPI). placeframe `dev`: `f2c94d16` (declarations conversion +
|
|
5
|
+
generate-score port completion), `33ddfb26` (score artifacts), `e0bcd9f8`
|
|
6
|
+
(prose) — preflight green. capture-tool `dev`: `ef7660d` (renames + repin),
|
|
7
|
+
`6025c5e` (prose) — lock round-trip stable, rig render green. All four repos
|
|
8
|
+
need operator pushes. Resolved during execution: the fluent-bit declaration key
|
|
9
|
+
is `SEAWEEDFS_AUDIT_IMAGE` (the plan's `FLUENT_BIT_IMAGE` never matched the
|
|
10
|
+
actual compose hole — holes stayed textually unchanged); stale `DOTNET_*`
|
|
11
|
+
declarations and `PYTHON_BASE`/`ZED_BASE` lock entries were pruned in placeframe
|
|
12
|
+
(zero consumers); `INITIALIZE_S3_IMAGE` (consumed by scripts/sweep_postprocess)
|
|
13
|
+
was promoted to a declaration; the generate-score console-script collision was
|
|
14
|
+
resolved by completing the port (config table in placeframe pyproject,
|
|
15
|
+
duplicate module deleted); capture-tool's bake file is `compose.bake.yml`, not
|
|
16
|
+
the `compose.zed.bake.yml` this plan named. NEXT: make-it-sing's own plan.md
|
|
17
|
+
resumes — flip its committed Stage 3 checkpoint (`6b4120c`) from
|
|
18
|
+
`ARG UV_BASE_DIGEST` + `FROM mirror/…${UV_BASE_DIGEST}` to
|
|
19
|
+
`ARG UV_BASE_IMAGE` + `FROM ${UV_BASE_IMAGE}`, repin docker-devkit >=0.1.15.
|
|
20
|
+
|
|
21
|
+
## Why this initiative exists
|
|
22
|
+
|
|
23
|
+
docker-devkit parses consumer compose files for exactly one purpose: discovering
|
|
24
|
+
`x-image-ref` labels (declarations of externally-pulled images) so `run_build`
|
|
25
|
+
can digest-lock them and `mirror-images` can mirror them. That single need dragged
|
|
26
|
+
in a pile of wrong-shaped machinery:
|
|
27
|
+
|
|
28
|
+
- Compose's YAML dialect tags (`!reset`) require PyYAML constructor registration,
|
|
29
|
+
and PyYAML's constructor API is untyped — a type-checker fight with no clean
|
|
30
|
+
resolution (the only surviving form was a suppression-boundary debate).
|
|
31
|
+
- The include-following logic in `run_build` serves zero org repos (the only
|
|
32
|
+
compose include anywhere is make-it-sing's `oci://` one, which contributes
|
|
33
|
+
nothing and was skipped anyway).
|
|
34
|
+
- Two declaration mechanisms exist for one need: `x-image-ref` labels in compose
|
|
35
|
+
(placeframe: fluent-bit, cloudbeaver, keycloak, ngrok) and `x-base-images` in
|
|
36
|
+
the bake file (placeframe: loki, alloy, grafana, and every Dockerfile base).
|
|
37
|
+
- The `_DIGEST` suffix lock shape (`image: path${X_DIGEST:?err}` in compose,
|
|
38
|
+
lock `X_DIGEST=@sha256:…`) authors the mirror path TWICE — bake declaration +
|
|
39
|
+
compose image line — a drift exposure (path drift fails loud at pull; tags
|
|
40
|
+
cannot drift, but the duplication is authored).
|
|
41
|
+
- The Dockerfile suffix form (`FROM path${X_DIGEST}`) has a silent-unpinned
|
|
42
|
+
failure mode: an empty arg yields a tagless `FROM path`, and Docker silently
|
|
43
|
+
pulls `:latest`.
|
|
44
|
+
|
|
45
|
+
## The design
|
|
46
|
+
|
|
47
|
+
- **Single declaration home**: bake `x-base-images`. Keys use the `_IMAGE`
|
|
48
|
+
convention (`UV_BASE_IMAGE`, `KEYCLOAK_IMAGE`, …). One authored string
|
|
49
|
+
(`path:tag`) per third-party image, anywhere in the org.
|
|
50
|
+
- **run_build single-emits** one lock entry per declaration:
|
|
51
|
+
`NAME=path:tag@sha256:…`. No dual shapes, no suffix entries.
|
|
52
|
+
- **Compose image lines** are pure holes: `image: "${KEYCLOAK_IMAGE:?err}"`.
|
|
53
|
+
- **Dockerfiles** use `ARG NAME` + `FROM ${NAME}`. An empty arg is a hard parse
|
|
54
|
+
error — this fixes the silent-`:latest` foot-gun.
|
|
55
|
+
- **BakeDocument gains a field validator**: every `base_images` value must be
|
|
56
|
+
`path:tag` or `path@digest`. The declarations half of `unpinned_references`
|
|
57
|
+
moves to parse time (enforced on every command run, not a CI-only scan).
|
|
58
|
+
- **`collect_repo_references` splits**: `declared_references(root)` (bake
|
|
59
|
+
declarations only — what mirror-images consumes) and a stray-ref scan
|
|
60
|
+
(Dockerfile `FROM`/`COPY --from` literals + score yaml `image:` lines — what
|
|
61
|
+
`unpinned_references` polices; it survives with that narrow job of catching
|
|
62
|
+
hand-written literal refs that bypass the declaration system).
|
|
63
|
+
- **`version_coupling_violations` is unchanged** — it guards a genuinely
|
|
64
|
+
non-derivable agreement (pyproject `tool.uv.required-version` ↔ image tag; the
|
|
65
|
+
tag carries suffixes like `-python3.13-trixie-slim` that pyproject doesn't
|
|
66
|
+
hold). Two authored truths that must agree is a different problem from one
|
|
67
|
+
truth scattered; the check is correct, keep it.
|
|
68
|
+
|
|
69
|
+
## Changes
|
|
70
|
+
|
|
71
|
+
### docker-devkit (this repo) — release 0.1.15
|
|
72
|
+
|
|
73
|
+
- `image_refs.py`: delete `compose_image_refs`, `ComposeDialectLoader`, both
|
|
74
|
+
`add_constructor` registrations, and the compose branch of
|
|
75
|
+
`collect_repo_references`; implement the `declared_references` / stray-scan
|
|
76
|
+
split. `bake_base_image_refs` and `version_coupling_violations` stay.
|
|
77
|
+
- `documents.py`: add the `base_images` field validator.
|
|
78
|
+
- `build_docker.py`: delete the third-party `x-image-ref` resolution block;
|
|
79
|
+
single-emit lock entries from `x-base-images`; wire all bake consumption
|
|
80
|
+
through `parse_bake`/`BakeDocument` (typed access replaces dict plumbing —
|
|
81
|
+
including `compute_service_shas(repo_root, bake: BakeDocument)` and
|
|
82
|
+
`compute_default_targets(bake: BakeDocument, …)` signatures, with `up.py`,
|
|
83
|
+
`down.py`, `score_generate.py` parsing at their call sites); loud guard:
|
|
84
|
+
`--mode ci` requires `x-registry-cache`.
|
|
85
|
+
- Tests: delete the compose-dialect tests; add validator + split-function tests;
|
|
86
|
+
existing fixtures via `BakeDocument.model_validate`.
|
|
87
|
+
- `types-pyyaml` stays as a dev dependency (it types `parse_bake`'s PyYAML
|
|
88
|
+
surface; with the dialect code gone, zero suppressions are needed anywhere).
|
|
89
|
+
- AGENTS.md: update the `image_refs.py` row and any compose-parsing references
|
|
90
|
+
(separate prose commit from the code).
|
|
91
|
+
- Release via the normal tag-ledger flow; operator push (App token cannot push).
|
|
92
|
+
|
|
93
|
+
### release-devkit — next patch (0.1.11)
|
|
94
|
+
|
|
95
|
+
- `mirror_images.py`: switch from
|
|
96
|
+
`collect_repo_references(Path.cwd(), dockerfile_glob=None)` to
|
|
97
|
+
`declared_references(root)` — same refs, declarations-only source.
|
|
98
|
+
|
|
99
|
+
### placeframe — one PR
|
|
100
|
+
|
|
101
|
+
- `compose.bake.yml`: add declarations for the 4 `x-image-ref` services
|
|
102
|
+
(`CLOUDBEAVER_IMAGE`, `KEYCLOAK_IMAGE`, `FLUENT_BIT_IMAGE`, `NGROK_IMAGE`,
|
|
103
|
+
values = today's label strings); rename every existing `_DIGEST` declaration
|
|
104
|
+
key to the `_IMAGE` convention; `x-base-args` keys follow the renames.
|
|
105
|
+
- `compose.yml`: delete the 4 labels (their image lines are textually unchanged —
|
|
106
|
+
they were already `${X_IMAGE:?err}` holes); flip loki×2 / alloy / grafana from
|
|
107
|
+
`path${X_DIGEST:?err}` to `${X_IMAGE:?err}`; inventory every `${…_IMAGE}` hole
|
|
108
|
+
(at minimum `SEAWEEDFS_AUDIT_IMAGE`) and ensure a declaration exists for each.
|
|
109
|
+
- Dockerfiles: convert every base `FROM` to `ARG NAME` + `FROM ${NAME}`.
|
|
110
|
+
- Version-coupling site registry (consumer preflight): rename `base_image` site
|
|
111
|
+
names to the new declaration keys — locate the registry during execution.
|
|
112
|
+
- Regenerate `.env.lock` (`uv run build --lock-only`); preflight green; repin
|
|
113
|
+
docker-devkit `>=0.1.15`.
|
|
114
|
+
- AGENTS.md mentions of `x-image-ref` updated (separate prose commit).
|
|
115
|
+
|
|
116
|
+
### capture-tool
|
|
117
|
+
|
|
118
|
+
- 3 Dockerfiles + `compose.zed.bake.yml` `x-base-args` key renames; regen
|
|
119
|
+
`.env.lock`; repin docker-devkit.
|
|
120
|
+
|
|
121
|
+
### make-it-sing — NOT this initiative
|
|
122
|
+
|
|
123
|
+
Its own `plan.md` resumes AFTER this one lands (its Stage 3/5 consume the new
|
|
124
|
+
pattern). Its committed Stage 3 checkpoint (`6b4120c`) still carries the suffix
|
|
125
|
+
form (`ARG UV_BASE_DIGEST` + `FROM mirror/…${UV_BASE_DIGEST}`) — flip to
|
|
126
|
+
`ARG UV_BASE_IMAGE` + `FROM ${UV_BASE_IMAGE}` when resuming that plan.
|
|
127
|
+
|
|
128
|
+
## Ordering
|
|
129
|
+
|
|
130
|
+
docker-devkit 0.1.15 → release-devkit 0.1.11 → placeframe PR → capture-tool.
|
|
131
|
+
All releases are operator pushes; the sandbox App token has Contents:read only.
|
|
132
|
+
|
|
133
|
+
## Verification
|
|
134
|
+
|
|
135
|
+
- docker-devkit: pytest + ruff + basedpyright all green (the checkpoint's known
|
|
136
|
+
failures must be gone — their absence is the proof the deletion was complete).
|
|
137
|
+
- placeframe / capture-tool: `uv run build --lock-only` round-trip, preflight
|
|
138
|
+
battery, `docker compose config` render.
|
|
139
|
+
|
|
140
|
+
## Deliberately out of scope
|
|
141
|
+
|
|
142
|
+
- Score yaml scanning changes.
|
|
143
|
+
- Renaming the `x-base-images` key itself (it now covers runtime images too —
|
|
144
|
+
placeframe's loki already made that de facto; note it in a bake-file comment).
|
|
145
|
+
- make-it-sing CI stages (owned by its own plan).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "docker-devkit"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.16"
|
|
4
4
|
description = "Docker-stack lifecycle commands (up/down/build) driven by a pyproject-declared compose assembly"
|
|
5
5
|
requires-python = ">=3.13"
|
|
6
6
|
dependencies = [
|
|
@@ -13,7 +13,12 @@ dependencies = [
|
|
|
13
13
|
]
|
|
14
14
|
|
|
15
15
|
[dependency-groups]
|
|
16
|
-
dev = [
|
|
16
|
+
dev = [
|
|
17
|
+
"basedpyright>=1.39.10",
|
|
18
|
+
"ruff>=0.14.11",
|
|
19
|
+
"pytest>=8.0.0",
|
|
20
|
+
"types-pyyaml>=6.0.12.20260906",
|
|
21
|
+
]
|
|
17
22
|
|
|
18
23
|
[build-system]
|
|
19
24
|
requires = ["hatchling"]
|
|
@@ -9,13 +9,13 @@ from subprocess import CalledProcessError
|
|
|
9
9
|
from typing import Annotated, Any, Literal
|
|
10
10
|
|
|
11
11
|
import typer
|
|
12
|
-
import yaml
|
|
13
12
|
from bashrun.bash import bash, bash_output
|
|
14
13
|
from .detect_gpu import GPU_TYPES, Gpu, detect_gpu
|
|
14
|
+
from .documents import BakeDocument, digest_pinned, parse_bake
|
|
15
15
|
from pydantic_settings import BaseSettings
|
|
16
16
|
|
|
17
17
|
from .context_sha import compute_service_shas
|
|
18
|
-
from .image_refs import bake_base_image_refs,
|
|
18
|
+
from .image_refs import bake_base_image_refs, resolve_remote_digest
|
|
19
19
|
from .modes import parse_env_file
|
|
20
20
|
|
|
21
21
|
|
|
@@ -27,7 +27,6 @@ settings = Settings.model_validate({})
|
|
|
27
27
|
|
|
28
28
|
LOCK_FILE = Path(".env.lock")
|
|
29
29
|
ENV_SHAS_FILE = Path(".env.shas")
|
|
30
|
-
COMPOSE_FILE = Path("compose.yml")
|
|
31
30
|
DEFAULT_BAKE_FILE = Path("compose.bake.yml")
|
|
32
31
|
METADATA_PATH = Path("metadata.json")
|
|
33
32
|
|
|
@@ -75,29 +74,26 @@ def run_build(
|
|
|
75
74
|
no_cache: bool = False,
|
|
76
75
|
targets_opt: list[str] | None = None,
|
|
77
76
|
) -> None:
|
|
78
|
-
|
|
77
|
+
bake = parse_bake(DEFAULT_BAKE_FILE)
|
|
78
|
+
|
|
79
|
+
service_shas = compute_service_shas(Path.cwd(), bake)
|
|
79
80
|
os.environ.update(service_shas)
|
|
80
81
|
|
|
81
|
-
#
|
|
82
|
+
# Write .env.shas with the locally-built image tags
|
|
82
83
|
ENV_SHAS_FILE.write_text(
|
|
83
84
|
"".join(f"{key}={value}\n" for key, value in sorted(service_shas.items())), encoding="utf-8"
|
|
84
85
|
)
|
|
85
86
|
|
|
86
|
-
#
|
|
87
|
-
bake_data: dict[str, Any] = yaml.safe_load(DEFAULT_BAKE_FILE.read_text(encoding="utf-8"))
|
|
88
|
-
compose_data: dict[str, Any] = (
|
|
89
|
-
yaml.safe_load(COMPOSE_FILE.read_text(encoding="utf-8")) if COMPOSE_FILE.exists() else {}
|
|
90
|
-
)
|
|
91
|
-
for include in compose_data.pop("include", []):
|
|
92
|
-
include_path = COMPOSE_FILE.parent / (include if isinstance(include, str) else include["path"])
|
|
93
|
-
included: dict[str, Any] = yaml.safe_load(include_path.read_text(encoding="utf-8"))
|
|
94
|
-
compose_data.setdefault("services", {}).update(included.get("services", {}))
|
|
87
|
+
# Load any existing lock
|
|
95
88
|
lock_data = parse_env_file(LOCK_FILE) if LOCK_FILE.exists() else {}
|
|
96
89
|
|
|
97
90
|
# TOOD: Create separate commands for ci and local modes so typer can do this validation instead of us
|
|
98
91
|
if mode == "ci" and gpu == "auto":
|
|
99
92
|
raise typer.BadParameter("In CI mode, --gpu cannot be 'auto'; specify 'cuda' or 'rocm'.")
|
|
100
93
|
|
|
94
|
+
if mode == "ci" and bake.registry_cache is None:
|
|
95
|
+
raise typer.BadParameter("In CI mode, the bake file must declare x-registry-cache.")
|
|
96
|
+
|
|
101
97
|
if gpu_only and gpu not in GPU_TYPES:
|
|
102
98
|
raise typer.BadParameter("--gpu-only requires a concrete gpu (cuda or rocm), not 'auto' or 'none'.")
|
|
103
99
|
|
|
@@ -109,28 +105,18 @@ def run_build(
|
|
|
109
105
|
if gpu == "auto" and not lock_only:
|
|
110
106
|
gpu = detect_gpu()
|
|
111
107
|
|
|
112
|
-
#
|
|
113
|
-
for occurrence in
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
# Re-resolve when explicitly requested, when unseen, or when the image name in compose.yml
|
|
127
|
-
# changed from what's in the lock file (e.g. postgres:16-alpine → postgis/postgres:16-3.4-alpine)
|
|
128
|
-
for image, ref in {
|
|
129
|
-
image: ref
|
|
130
|
-
for image, ref in third_party_images.items()
|
|
131
|
-
if upgrade or image not in lock_data or not lock_data[image].startswith(ref + "@")
|
|
132
|
-
}.items():
|
|
133
|
-
lock_data[image] = f"{ref}@{'' if '$' in ref else resolve_remote_digest(ref)}"
|
|
108
|
+
# One lock entry per declared third-party image: NAME=path:tag@sha256:…
|
|
109
|
+
for occurrence in bake_base_image_refs(bake):
|
|
110
|
+
if digest_pinned(occurrence.reference):
|
|
111
|
+
lock_data[occurrence.name] = occurrence.reference
|
|
112
|
+
continue
|
|
113
|
+
if (
|
|
114
|
+
not upgrade
|
|
115
|
+
and occurrence.name in lock_data
|
|
116
|
+
and lock_data[occurrence.name].startswith(occurrence.reference + "@")
|
|
117
|
+
):
|
|
118
|
+
continue
|
|
119
|
+
lock_data[occurrence.name] = f"{occurrence.reference}@{resolve_remote_digest(occurrence.reference)}"
|
|
134
120
|
|
|
135
121
|
# Update main lock file
|
|
136
122
|
LOCK_FILE.write_text(
|
|
@@ -149,17 +135,17 @@ def run_build(
|
|
|
149
135
|
|
|
150
136
|
# Determine bake targets
|
|
151
137
|
if targets_opt:
|
|
152
|
-
unknown = set(targets_opt) - set(
|
|
138
|
+
unknown = set(targets_opt) - set(bake.services)
|
|
153
139
|
if unknown:
|
|
154
|
-
raise typer.BadParameter(f"Unknown targets: {unknown}. Available: {sorted(
|
|
155
|
-
targets = [t for t in
|
|
140
|
+
raise typer.BadParameter(f"Unknown targets: {unknown}. Available: {sorted(bake.services)}")
|
|
141
|
+
targets = [t for t in bake.services if t in set(targets_opt)]
|
|
156
142
|
else:
|
|
157
|
-
targets = compute_default_targets(
|
|
143
|
+
targets = compute_default_targets(bake, gpu, gpu_only)
|
|
158
144
|
|
|
159
145
|
# Configure registry caches in CI mode
|
|
160
146
|
if mode == "ci":
|
|
161
147
|
for target in targets:
|
|
162
|
-
target_cache = f"{
|
|
148
|
+
target_cache = f"{bake.registry_cache}:{target}"
|
|
163
149
|
command_arguments.append(
|
|
164
150
|
f"--set {target}.cache-to+=type=registry,ref={target_cache},mode=max,image-manifest=true,oci-mediatypes=true"
|
|
165
151
|
)
|
|
@@ -178,8 +164,7 @@ def run_build(
|
|
|
178
164
|
else:
|
|
179
165
|
raise RuntimeError(f"Unsupported host architecture: {machine}")
|
|
180
166
|
for target in targets:
|
|
181
|
-
|
|
182
|
-
if len(target_platforms) > 1:
|
|
167
|
+
if len(bake.services[target].build.platforms) > 1:
|
|
183
168
|
command_arguments.append(f"--set {target}.platform={host_platform}")
|
|
184
169
|
|
|
185
170
|
# Load or push images based on mode
|
|
@@ -253,11 +238,9 @@ def _check_gc_limits(min_gb: int = 60):
|
|
|
253
238
|
# Services without build.tags (e.g. neural-networks-base-*) stay out too: they are
|
|
254
239
|
# build-only dependencies pulled in via additional_contexts, and bake rejects a
|
|
255
240
|
# tagless target under --push.
|
|
256
|
-
def compute_default_targets(
|
|
257
|
-
cross_compile_targets: set[str] = set(
|
|
258
|
-
tagged_services = [
|
|
259
|
-
service for service, config in bake_data["services"].items() if config.get("build", {}).get("tags")
|
|
260
|
-
]
|
|
241
|
+
def compute_default_targets(bake: BakeDocument, gpu: Gpu, gpu_only: bool = False) -> list[str]:
|
|
242
|
+
cross_compile_targets: set[str] = set(bake.cross_compile_targets)
|
|
243
|
+
tagged_services = [service for service, config in bake.services.items() if config.build.tags]
|
|
261
244
|
if gpu_only:
|
|
262
245
|
return [
|
|
263
246
|
service
|
|
@@ -3,23 +3,20 @@ from __future__ import annotations
|
|
|
3
3
|
from pathlib import Path, PurePosixPath
|
|
4
4
|
from tempfile import TemporaryDirectory
|
|
5
5
|
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
6
|
import pathspec
|
|
9
|
-
import yaml
|
|
10
7
|
from bashrun.bash import bash, bash_output
|
|
11
8
|
|
|
9
|
+
from .documents import BakeDocument
|
|
10
|
+
|
|
12
11
|
|
|
13
|
-
def compute_service_shas(repo_root: Path,
|
|
12
|
+
def compute_service_shas(repo_root: Path, bake: BakeDocument) -> dict[str, str]:
|
|
14
13
|
repo_root = repo_root.resolve()
|
|
15
14
|
|
|
16
|
-
bake_data: dict[str, Any] = yaml.safe_load(bake_file.read_text(encoding="utf-8"))
|
|
17
15
|
service_dirs: set[str] = set()
|
|
18
|
-
for config in
|
|
19
|
-
|
|
20
|
-
if not build.get("tags"):
|
|
16
|
+
for config in bake.services.values():
|
|
17
|
+
if not config.build.tags:
|
|
21
18
|
continue
|
|
22
|
-
service_dirs.add(str(PurePosixPath(build.
|
|
19
|
+
service_dirs.add(str(PurePosixPath(config.build.dockerfile).parent))
|
|
23
20
|
|
|
24
21
|
tree_entries = bash_output("git ls-tree -r HEAD", cwd=repo_root).splitlines()
|
|
25
22
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TypeVar
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
9
|
+
|
|
10
|
+
ModelT = TypeVar("ModelT", bound=BaseModel)
|
|
11
|
+
|
|
12
|
+
DECLARED_REFERENCE_PATTERN = re.compile(
|
|
13
|
+
r"^(?P<path>[^:@\s$]+)(?::(?P<tag>[^@\s$]+))?(?:@(?P<digest>sha256:[a-f0-9]{64}))?$"
|
|
14
|
+
)
|
|
15
|
+
DIGEST_TAIL_PATTERN = re.compile(r"@sha256:[a-f0-9]{64}$")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BakeBuild(BaseModel):
|
|
19
|
+
model_config = ConfigDict(extra="forbid")
|
|
20
|
+
|
|
21
|
+
context: str = "."
|
|
22
|
+
dockerfile: str
|
|
23
|
+
target: str | None = None
|
|
24
|
+
platforms: list[str] = Field(default_factory=list)
|
|
25
|
+
args: dict[str, str] = Field(default_factory=dict)
|
|
26
|
+
tags: list[str] = Field(default_factory=list)
|
|
27
|
+
additional_contexts: dict[str, str] = Field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BakeService(BaseModel):
|
|
31
|
+
model_config = ConfigDict(extra="forbid")
|
|
32
|
+
|
|
33
|
+
build: BakeBuild
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BakeDocument(BaseModel):
|
|
37
|
+
model_config = ConfigDict(extra="forbid")
|
|
38
|
+
|
|
39
|
+
base_images: dict[str, str] = Field(default_factory=dict, alias="x-base-images")
|
|
40
|
+
base_args: dict[str, str] = Field(default_factory=dict, alias="x-base-args")
|
|
41
|
+
registry_cache: str | None = Field(default=None, alias="x-registry-cache")
|
|
42
|
+
cross_compile_targets: list[str] = Field(default_factory=list, alias="x-cross-compile-targets")
|
|
43
|
+
services: dict[str, BakeService] = Field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
@field_validator("base_images")
|
|
46
|
+
@classmethod
|
|
47
|
+
def _require_tag_or_digest(cls, base_images: dict[str, str]) -> dict[str, str]:
|
|
48
|
+
for name, reference in base_images.items():
|
|
49
|
+
match = DECLARED_REFERENCE_PATTERN.fullmatch(reference)
|
|
50
|
+
if match is None or not (match.group("tag") or match.group("digest")):
|
|
51
|
+
raise ValueError(f"x-base-images.{name}: {reference!r} must be 'path:tag' or 'path@digest'")
|
|
52
|
+
return base_images
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def digest_pinned(reference: str) -> bool:
|
|
56
|
+
return DIGEST_TAIL_PATTERN.search(reference) is not None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def parse_bake(path: Path) -> BakeDocument:
|
|
60
|
+
loader = yaml.SafeLoader(path.read_text(encoding="utf-8"))
|
|
61
|
+
try:
|
|
62
|
+
return BakeDocument.model_validate(loader.get_single_data())
|
|
63
|
+
finally:
|
|
64
|
+
loader.dispose()
|
|
@@ -7,6 +7,7 @@ from bashrun.bash import bash_handoff
|
|
|
7
7
|
from .detect_gpu import Gpu, detect_gpu
|
|
8
8
|
|
|
9
9
|
from .context_sha import compute_service_shas
|
|
10
|
+
from .documents import parse_bake
|
|
10
11
|
from .lifecycle import BAKE_FILE, enforce_bake_declaration, expand_compose_files, load_lifecycle_config
|
|
11
12
|
from .modes import resolve_auth_mode
|
|
12
13
|
|
|
@@ -36,7 +37,7 @@ def down(
|
|
|
36
37
|
resolve_auth_mode(ENV_FILE)
|
|
37
38
|
|
|
38
39
|
if BAKE_FILE.exists():
|
|
39
|
-
os.environ.update(compute_service_shas(Path.cwd(), BAKE_FILE))
|
|
40
|
+
os.environ.update(compute_service_shas(Path.cwd(), parse_bake(BAKE_FILE)))
|
|
40
41
|
|
|
41
42
|
# Always layer the dev overlay so containers from a prior --dev bring-up get torn down too
|
|
42
43
|
compose_files = expand_compose_files(config, gpu, include_dev=True)
|
|
@@ -8,9 +8,10 @@ from dataclasses import dataclass
|
|
|
8
8
|
from itertools import starmap
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
|
-
import yaml
|
|
12
11
|
from bashrun.bash import bash_output
|
|
13
|
-
from pydantic import
|
|
12
|
+
from pydantic import RootModel
|
|
13
|
+
|
|
14
|
+
from .documents import BakeDocument, parse_bake
|
|
14
15
|
|
|
15
16
|
BUILD_ARG_PATTERN = re.compile(r"\$\{[A-Za-z0-9_]+\}")
|
|
16
17
|
FROM_PATTERN = re.compile(r"^FROM\s+(?:--platform=\S+\s+)?(\S+)", re.MULTILINE)
|
|
@@ -20,24 +21,6 @@ DIGEST_PATTERN = re.compile(r"^Digest:\s+(sha256:[a-f0-9]+)", re.MULTILINE)
|
|
|
20
21
|
MISSING_VERSION = "<missing>"
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
class ComposeService(BaseModel):
|
|
24
|
-
model_config = ConfigDict(extra="ignore")
|
|
25
|
-
|
|
26
|
-
image_ref: str | None = Field(default=None, alias="x-image-ref")
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class ComposeDocument(BaseModel):
|
|
30
|
-
model_config = ConfigDict(extra="ignore")
|
|
31
|
-
|
|
32
|
-
services: dict[str, ComposeService] = Field(default_factory=dict)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class BakeDocument(BaseModel):
|
|
36
|
-
model_config = ConfigDict(extra="ignore")
|
|
37
|
-
|
|
38
|
-
base_images: dict[str, str] = Field(default_factory=dict, alias="x-base-images")
|
|
39
|
-
|
|
40
|
-
|
|
41
24
|
type TomlValue = (
|
|
42
25
|
str
|
|
43
26
|
| int
|
|
@@ -79,7 +62,7 @@ class VersionCoupling:
|
|
|
79
62
|
def unpinned_references(root: Path) -> list[str]:
|
|
80
63
|
return [
|
|
81
64
|
occurrence.reference
|
|
82
|
-
for occurrence in
|
|
65
|
+
for occurrence in stray_references(root)
|
|
83
66
|
if "/" in (reference := strip_build_args(occurrence.reference))
|
|
84
67
|
and not BUILD_ARG_PATTERN.search(occurrence.reference)
|
|
85
68
|
and ":" not in (tail := reference[reference.rfind("/") + 1 :])
|
|
@@ -87,48 +70,32 @@ def unpinned_references(root: Path) -> list[str]:
|
|
|
87
70
|
]
|
|
88
71
|
|
|
89
72
|
|
|
90
|
-
def
|
|
73
|
+
def declared_references(root: Path, bake_glob: str = "compose*.bake.yml") -> list[ImageReference]:
|
|
74
|
+
return [reference for path in sorted(root.glob(bake_glob)) for reference in bake_base_image_refs(parse_bake(path))]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def stray_references(
|
|
91
78
|
root: Path,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
dockerfile_glob: str | None = "docker/*/Dockerfile*",
|
|
95
|
-
image_glob: str | None = "score/*.yaml",
|
|
79
|
+
dockerfile_glob: str = "docker/*/Dockerfile*",
|
|
80
|
+
image_glob: str = "score/*.yaml",
|
|
96
81
|
) -> list[ImageReference]:
|
|
97
82
|
return (
|
|
98
83
|
[
|
|
99
|
-
|
|
100
|
-
for path in sorted(root.glob(
|
|
101
|
-
for
|
|
84
|
+
ImageReference("", match.group(1))
|
|
85
|
+
for path in sorted(root.glob(dockerfile_glob))
|
|
86
|
+
for match in FROM_PATTERN.finditer(path.read_text(encoding="utf-8"))
|
|
102
87
|
]
|
|
103
88
|
+ [
|
|
104
|
-
|
|
105
|
-
for path in sorted(root.glob(
|
|
106
|
-
for
|
|
89
|
+
ImageReference("", match.group(1))
|
|
90
|
+
for path in sorted(root.glob(dockerfile_glob))
|
|
91
|
+
for match in COPY_FROM_PATTERN.finditer(path.read_text(encoding="utf-8"))
|
|
92
|
+
if "/" in match.group(1)
|
|
93
|
+
]
|
|
94
|
+
+ [
|
|
95
|
+
ImageReference("", match.group(1))
|
|
96
|
+
for path in sorted(root.glob(image_glob))
|
|
97
|
+
for match in IMAGE_LINE_PATTERN.finditer(path.read_text(encoding="utf-8"))
|
|
107
98
|
]
|
|
108
|
-
+ (
|
|
109
|
-
[
|
|
110
|
-
ImageReference("", match.group(1))
|
|
111
|
-
for path in sorted(root.glob(dockerfile_glob))
|
|
112
|
-
for match in FROM_PATTERN.finditer(path.read_text(encoding="utf-8"))
|
|
113
|
-
]
|
|
114
|
-
+ [
|
|
115
|
-
ImageReference("", match.group(1))
|
|
116
|
-
for path in sorted(root.glob(dockerfile_glob))
|
|
117
|
-
for match in COPY_FROM_PATTERN.finditer(path.read_text(encoding="utf-8"))
|
|
118
|
-
if "/" in match.group(1)
|
|
119
|
-
]
|
|
120
|
-
if dockerfile_glob
|
|
121
|
-
else []
|
|
122
|
-
)
|
|
123
|
-
+ (
|
|
124
|
-
[
|
|
125
|
-
ImageReference("", match.group(1))
|
|
126
|
-
for path in sorted(root.glob(image_glob))
|
|
127
|
-
for match in IMAGE_LINE_PATTERN.finditer(path.read_text(encoding="utf-8"))
|
|
128
|
-
]
|
|
129
|
-
if image_glob
|
|
130
|
-
else []
|
|
131
|
-
)
|
|
132
99
|
)
|
|
133
100
|
|
|
134
101
|
|
|
@@ -153,14 +120,6 @@ def resolve_remote_digest(reference: str) -> str:
|
|
|
153
120
|
return match.group(1)
|
|
154
121
|
|
|
155
122
|
|
|
156
|
-
def compose_service_refs(document: object) -> list[ImageReference]:
|
|
157
|
-
return [
|
|
158
|
-
ImageReference(service_name, service.image_ref)
|
|
159
|
-
for service_name, service in ComposeDocument.model_validate(document).services.items()
|
|
160
|
-
if service.image_ref is not None
|
|
161
|
-
]
|
|
162
|
-
|
|
163
|
-
|
|
164
123
|
def strip_build_args(reference: str) -> str:
|
|
165
124
|
return BUILD_ARG_PATTERN.sub("", reference).strip()
|
|
166
125
|
|
|
@@ -183,7 +142,7 @@ def _site_version(path: Path, site: VersionSite) -> str:
|
|
|
183
142
|
haystack = next(
|
|
184
143
|
(
|
|
185
144
|
reference.reference
|
|
186
|
-
for reference in bake_base_image_refs(
|
|
145
|
+
for reference in bake_base_image_refs(parse_bake(path))
|
|
187
146
|
if reference.name == site.base_image
|
|
188
147
|
),
|
|
189
148
|
"",
|
|
@@ -194,9 +153,5 @@ def _site_version(path: Path, site: VersionSite) -> str:
|
|
|
194
153
|
return match.group(1) if match else MISSING_VERSION
|
|
195
154
|
|
|
196
155
|
|
|
197
|
-
def bake_base_image_refs(document:
|
|
198
|
-
return list(starmap(ImageReference,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def _load_yaml_document(path: Path) -> object:
|
|
202
|
-
return yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
156
|
+
def bake_base_image_refs(document: BakeDocument) -> list[ImageReference]:
|
|
157
|
+
return list(starmap(ImageReference, document.base_images.items()))
|