build(ci): add z2m-seed runtime image (alpine + jq) #442

Merged
k.renner merged 2 commits from z2m-seed-drop-apk into master 2026-07-30 07:11:38 +00:00
Member

Step 1 of 2 toward removing apk add from the zigbee2mqtt seed-data init container, which currently installs jq on every pod start. Per owner directive: baking tools into a layered image is fine, adding them at runtime is not.

Why this is split in two

Runtime images in this repo are pinned by digest (as the secret-rotator CronJobs are), and the digest doesn't exist until the image has been built and pushed. So this PR adds only the image and its build step — nothing consumes it yet, so it is inert on merge.

To make step 2 cheap, the build writes the digest via kaniko's --image-name-with-digest-file and echoes it as PIN THIS:, so the follow-up can copy the reference straight out of the build log with no registry query. (--image-name-with-digest-file was verified against executor:v1.23.2-debug --help rather than assumed.)

Why an image for one jq call

I first implemented this with busybox awk to avoid a new image, and testing turned up a genuine hazard worth recording:

"key" occurs 12 times in the real backup — once per device with an APS link_key — and the first occurrence is a device key, because "devices" sorts before "network_key". An extraction that isn't correctly anchored returns a device link key; herdsman then forms a NEW network and every device drops off.

I hit exactly that during testing: a corruption fixture aimed at the network key silently modified a device key instead, which is what exposed the ordering. The awk version I built was anchored and did produce byte-identical output to the live value (sha256 27c5c77a945bcecc), with guards covering non-hex, wrong-length, and missing-key inputs — but the failure mode if anyone later edits that expression is losing the whole Zigbee network. A real JSON parser earns an image here rather than a clever one-liner in a manifest.

Conventions followed

  • Containerfile alongside the other ci/images/ entries
  • Base pinned by digest to the same alpine:3.21 digest the ci-k8s image already uses
  • kaniko step gated on its own path, so unrelated pushes don't rebuild it (per the #336 note in that pipeline)
  • date + latest tags pushed, matching the other images
  • Labelled "Runtime image, not CI tooling", following the build-secret-rotator precedent for a non-CI image in this pipeline

Step 2 (follow-up, after this builds)

Switch the seed-data init container to z2m-seed@sha256:<digest>, replace apk add jq + the jq call, and keep the fail-loudly validation (non-hex / wrong-length / missing key → explicit FATAL and non-zero exit) so a bad network_key can never be silently rendered.

Note this init container is unrelated to PR #441 (the interview watchdog), which already complies with the directive by reusing eclipse-mosquitto for its bundled mosquitto_pub.

🤖 Generated with Claude Code

Step **1 of 2** toward removing `apk add` from the zigbee2mqtt `seed-data` init container, which currently installs `jq` on **every pod start**. Per owner directive: baking tools into a layered image is fine, adding them at runtime is not. ## Why this is split in two Runtime images in this repo are pinned by **digest** (as the secret-rotator CronJobs are), and the digest doesn't exist until the image has been built and pushed. So this PR adds *only* the image and its build step — **nothing consumes it yet, so it is inert on merge.** To make step 2 cheap, the build writes the digest via kaniko's `--image-name-with-digest-file` and echoes it as `PIN THIS:`, so the follow-up can copy the reference straight out of the build log with no registry query. (`--image-name-with-digest-file` was verified against `executor:v1.23.2-debug --help` rather than assumed.) ## Why an image for one `jq` call I first implemented this with busybox `awk` to avoid a new image, and testing turned up a genuine hazard worth recording: **`"key"` occurs 12 times in the real backup** — once per device with an APS `link_key` — and the **first occurrence is a device key**, because `"devices"` sorts before `"network_key"`. An extraction that isn't correctly anchored returns a device link key; herdsman then forms a **NEW network and every device drops off**. I hit exactly that during testing: a corruption fixture aimed at the network key silently modified a *device* key instead, which is what exposed the ordering. The awk version I built was anchored and did produce byte-identical output to the live value (sha256 `27c5c77a945bcecc`), with guards covering non-hex, wrong-length, and missing-key inputs — but the failure mode if anyone later edits that expression is losing the whole Zigbee network. A real JSON parser earns an image here rather than a clever one-liner in a manifest. ## Conventions followed - `Containerfile` alongside the other `ci/images/` entries - Base pinned by digest to the **same** `alpine:3.21` digest the `ci-k8s` image already uses - kaniko step gated on its own path, so unrelated pushes don't rebuild it (per the #336 note in that pipeline) - date + `latest` tags pushed, matching the other images - Labelled "Runtime image, not CI tooling", following the `build-secret-rotator` precedent for a non-CI image in this pipeline ## Step 2 (follow-up, after this builds) Switch the `seed-data` init container to `z2m-seed@sha256:<digest>`, replace `apk add jq` + the jq call, and keep the fail-loudly validation (non-hex / wrong-length / missing key → explicit FATAL and non-zero exit) so a bad `network_key` can never be silently rendered. Note this init container is unrelated to PR #441 (the interview watchdog), which already complies with the directive by reusing `eclipse-mosquitto` for its bundled `mosquitto_pub`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude-bot added 1 commit 2026-07-30 06:52:06 +00:00
build(ci): add z2m-seed runtime image (alpine + jq)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/push/build-ci-images Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
0d7fecad8d
Step 1 of 2 toward removing `apk add` from the zigbee2mqtt seed-data init
container, which currently installs jq on every pod start. Owner
directive: baking tools into a layered image is fine, adding them at
runtime is not.

This commit only adds the image and its build step; nothing consumes it
yet, so it is inert on merge. The Deployment cannot be switched in the
same commit because runtime images in this repo are pinned by DIGEST (as
the secret-rotator CronJobs are), and the digest does not exist until
this has built and pushed. The build therefore writes it out via kaniko's
--image-name-with-digest-file and echoes it as "PIN THIS:", so the
follow-up commit can copy the reference straight from the build log
without a separate registry query.

Why a real JSON parser rather than busybox sed/awk: "key" occurs 12 times
in the actual backup — once per device with an APS link_key — and the
FIRST occurrence is a device key, because "devices" sorts before
"network_key". An extraction that is not correctly anchored yields a
device link key, herdsman then forms a NEW network, and every device
drops off. This was reproduced while prototyping the awk approach, which
is what makes jq worth an image here rather than clever one-liners in the
manifest.

Follows the existing conventions: Containerfile alongside the other
ci/images entries, base pinned by digest to the same alpine:3.21 the
ci-k8s image uses, kaniko build gated on its own path so unrelated pushes
do not rebuild it, and date + latest tags pushed as the other images do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
review-bot left a comment
Member

🤖 Automated review — model CLI default (complexity: complex), commit 0d7fecad8df083b462ba081df63b486495db077a.

Advisory review by the review-bot pipeline. It never approves; a human merges. Push new commits to get a fresh review.


Summary

Adds a new build-z2m-seed kaniko step to .woodpecker/build-ci-images.yml and a
matching ci/images/z2m-seed/Containerfile (alpine:3.21 + jq), intended as the
base for the zigbee2mqtt seed-data init container in a follow-up PR. Nothing in
the repo currently references the new image (confirmed by grepping the
zigbee2mqtt manifests and the rest of the tree), so the PR is genuinely inert on
merge as claimed. No blocking issues found.

🟡 Suggestions

  • .woodpecker/build-ci-images.yml:161-162 — --image-name-with-digest-file is
    used together with two --destination flags (:$TAG and :latest).
    kaniko writes one line per destination to that file, so
    /kaniko/z2m-seed.digest (and thus the PIN THIS: log output) will print two
    lines with the same digest but different tags. Since the digest is identical
    either way this doesn't affect correctness of the follow-up step, but it's
    worth a one-line comment noting the log will show two lines so whoever does
    step 2 isn't confused about which to copy.

Tests

No test infrastructure exists for ci/images/*/Containerfile or the pipeline
YAML elsewhere in the repo (the sibling ansible/k8s/tofu/secret-rotator
images have none either), so the absence of tests here is consistent with
convention. The change is validated by the pipeline itself running kaniko on
merge/manual trigger; I can't execute that here, but the YAML structure mirrors
the existing build-secret-rotator step field-for-field (same env, auth
construction, tag scheme, gating), and the Containerfile reuses the exact
alpine:3.21 digest already pinned in ci/images/k8s/Containerfile, so no new
base-image trust is introduced.

Recommendation

Looks reasonable — no blocking issues; comment above is optional.

🤖 **Automated review** — model `CLI default` (complexity: complex), commit `0d7fecad8df083b462ba081df63b486495db077a`. > Advisory review by the `review-bot` pipeline. It never approves; a human merges. Push new commits to get a fresh review. --- ## Summary Adds a new `build-z2m-seed` kaniko step to `.woodpecker/build-ci-images.yml` and a matching `ci/images/z2m-seed/Containerfile` (alpine:3.21 + `jq`), intended as the base for the zigbee2mqtt `seed-data` init container in a follow-up PR. Nothing in the repo currently references the new image (confirmed by grepping the zigbee2mqtt manifests and the rest of the tree), so the PR is genuinely inert on merge as claimed. No blocking issues found. ## 🟡 Suggestions - `.woodpecker/build-ci-images.yml:161-162` — `--image-name-with-digest-file` is used together with **two** `--destination` flags (`:$TAG` and `:latest`). kaniko writes one line per destination to that file, so `/kaniko/z2m-seed.digest` (and thus the `PIN THIS:` log output) will print two lines with the same digest but different tags. Since the digest is identical either way this doesn't affect correctness of the follow-up step, but it's worth a one-line comment noting the log will show two lines so whoever does step 2 isn't confused about which to copy. ## Tests No test infrastructure exists for `ci/images/*/Containerfile` or the pipeline YAML elsewhere in the repo (the sibling `ansible`/`k8s`/`tofu`/`secret-rotator` images have none either), so the absence of tests here is consistent with convention. The change is validated by the pipeline itself running kaniko on merge/manual trigger; I can't execute that here, but the YAML structure mirrors the existing `build-secret-rotator` step field-for-field (same env, auth construction, tag scheme, gating), and the `Containerfile` reuses the exact `alpine:3.21` digest already pinned in `ci/images/k8s/Containerfile`, so no new base-image trust is introduced. ## Recommendation Looks reasonable — no blocking issues; comment above is optional. <!-- pr-review:sha=0d7fecad8df083b462ba081df63b486495db077a -->
Author
Member

Addressed in the latest push.

🟡 Two-line digest file

Good catch, and precise. Rather than add a comment warning about the ambiguity, I removed it: the step now reduces to the bare digest, dedupes, and prints one unambiguous copy-pasteable reference. The per-destination behaviour is documented inline so a future edit does not reintroduce it.

Verified against a two-line fixture in kaniko format:

gitea.renner.uno/gitea_admin/z2m-seed:2026-07-30@sha256:abc123
gitea.renner.uno/gitea_admin/z2m-seed:latest@sha256:abc123
-> PIN THIS: gitea.renner.uno/gitea_admin/z2m-seed@sha256:abc123

Thanks also for independently confirming the PR is inert on merge — that was the main property I wanted checked, since it is what makes the two-step split safe.

Addressed in the latest push. ### 🟡 Two-line digest file Good catch, and precise. Rather than add a comment warning about the ambiguity, I removed it: the step now reduces to the bare digest, dedupes, and prints one unambiguous copy-pasteable reference. The per-destination behaviour is documented inline so a future edit does not reintroduce it. Verified against a two-line fixture in kaniko format: ``` gitea.renner.uno/gitea_admin/z2m-seed:2026-07-30@sha256:abc123 gitea.renner.uno/gitea_admin/z2m-seed:latest@sha256:abc123 -> PIN THIS: gitea.renner.uno/gitea_admin/z2m-seed@sha256:abc123 ``` Thanks also for independently confirming the PR is inert on merge — that was the main property I wanted checked, since it is what makes the two-step split safe.
claude-bot added 1 commit 2026-07-30 07:09:24 +00:00
Review point on #442: kaniko writes one line per --destination to
--image-name-with-digest-file, so with both :$TAG and :latest the file
holds two entries carrying the same digest, and "PIN THIS:" would print
two near-identical lines for whoever does step 2 to choose between.

Rather than just comment on the ambiguity, remove it: reduce to the bare
digest, dedupe, and print one unambiguous copy-pasteable reference. The
multi-line behaviour is documented inline so a future edit does not
reintroduce it.

Verified the reduction against a two-line fixture matching kaniko's
format:

  gitea.renner.uno/gitea_admin/z2m-seed:2026-07-30@sha256:abc123
  gitea.renner.uno/gitea_admin/z2m-seed:latest@sha256:abc123
  -> PIN THIS: gitea.renner.uno/gitea_admin/z2m-seed@sha256:abc123

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
k.renner merged commit dde14f657b into master 2026-07-30 07:11:38 +00:00
Sign in to join this conversation.