Owner directive: tools belong in the image, not installed at container
start. This is the highest-blast-radius instance of the pattern left in the
repo — a DaemonSet, so it runs on every node.
No image was needed. `apk add --no-cache curl tar` turns out to have been
unnecessary from the start: verified in alpine:3.21 that `tar` is already
busybox (v1.37.0) and handles -xzf, and that busybox `wget` follows
GitHub's release redirect to objects.githubusercontent.com over TLS using
the base image's /etc/ssl/certs/ca-certificates.crt. Only `curl` is
genuinely absent, and wget covers what it was used for. So this replaces
the install with `wget -q -O` rather than introducing another layered
image.
Severity note for the record: the apk add sat inside the else branch, so
steady-state nodes never reached it. It ran when plugins were absent — i.e.
on a fresh node or after /opt/cni/bin was wiped, which is precisely the
moment you least want a dependency on package-index availability.
Verified by running the RENDERED init script in alpine:3.21 against the
real release URL, both paths:
empty dir -> Fetching ...; Installed: host-local, macvlan, static
macvlan executable, /opt/cni/bin owner 0:0, exit 0
already there -> "CNI plugins already installed (CNI macvlan plugin
v1.5.1)", exit 0, no fetch
The second line is worth noting: that version string comes from executing
the extracted binary, so the download and extraction are confirmed good,
not merely present. The chown 0:0 also still applies, which the existing
comment explains is required or Cilium's apply-sysctl-overwrites init
container fails with "Permission denied".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review point on #444, which merged before this could be folded in.
The comment claimed the listed shapes yield "null" at rc=0. That describes
BARE `.network_key.key`, not the expression actually written, and it
contradicted its own closing line two lines below. Measured against jq
1.7.1 in the image, `(.network_key?.key? // "")` returns an EMPTY string at
rc=0 for all of:
{} {"devices":[]} {"network_key":{}} {"network_key":null}
{"network_key":"abc"} {"network_key":123}
[] "a string"
and never the literal text null, because `// ""` maps a null result to
empty. Bare `.network_key.key` is what exits 5 on the last four shapes.
Checking that also turned up something the review did not mention: because
KEY can never be "null" with the current expression, the `[ "$KEY" =
"null" ]` arm is dead code today. Kept deliberately as belt-and-braces for
a future edit that drops the `// ""` fallback, and now labelled as such
rather than left looking like the arm that fires.
Comment-only: `git diff` shows zero changed lines outside comments, and the
rendered script is unchanged in behaviour.
NOTE: this text lives in the Deployment's init container command, so it is
part of the pod template — merging rolls the zigbee2mqtt pod (~20s of
Zigbee downtime, replicas: 1 + Recreate). Incidental benefit: the restart
resets Z2M's in-memory configure attempt counter, giving the Philips
devices that are interviewed but not yet bound (both RDM001 Treppenhaus
switches, the RWL022, and Sarah) three fresh configure attempts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round-3 review on #444. Both suggestions were optional; both were cheap to
close properly.
`.network_key.key?` only shielded the leaf selector. If the backup's TOP
level were not an object, `.network_key` itself raises before `?` applies.
Verified in the image with jq 1.7.1:
[] (.network_key.key? // "") -> rc=5 Cannot index array ...
"a string" (.network_key.key? // "") -> rc=5 Cannot index string ...
[] (.network_key?.key? // "") -> rc=0 empty
"a string" (.network_key?.key? // "") -> rc=0 empty
So `?` now sits on both selectors. Not a realistic shape for an Open
Coordinator Backup export and it failed closed either way, but it was the
last path that produced jq's raw error rather than a tailored FATAL line.
Full matrix against the rendered script, on the alpine-jq:2026-07-30 tag:
real backup -> exit 0, hash 27c5c77a945bcecc (matches live)
non-hex key -> FATAL: contains non-hex characters
4-char key -> FATAL: length 4, expected 32 hex chars
no network_key -> FATAL: .network_key.key absent
network_key non-object -> FATAL: .network_key.key absent
network_key number -> FATAL: .network_key.key absent
top-level array -> FATAL: .network_key.key absent
top-level string -> FATAL: .network_key.key absent
device link_key in doc -> correctly ignored; network_key used
On the registry suggestion: re-verified the tag pulls immediately before
merge — jq 1.7.1, alpine 3.21.7. It was published by pipeline 3060, the
push build for this branch. No build ran for the later commits because they
touch only kubernetes/apps/**, which correctly does not match the build
step's when.path; the image content is unchanged by them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round-2 review on #444.
The 🟡 identified a real gap, though not via the input shape it described.
Verified against jq 1.7.1 in the actual image:
{"devices":[]} -> "null", rc=0 (absent path is LENIENT)
{"network_key":{}} -> "null", rc=0
{"network_key":null} -> "null", rc=0
{"network_key":"abc"} -> rc=5, Cannot index string with string "key"
{"network_key":123} -> rc=5, Cannot index number with string "key"
So a missing network_key was already handled correctly and did produce the
intended FATAL line. What was NOT handled is a network_key that exists but
is not an object: plain `.key` exits 5 there, and under `set -e` that
aborts before the checks run, surfacing jq's raw error instead. Fixed with
`.network_key.key?`, which yields empty for those shapes and routes them
into the existing absent-key branch.
Now every malformed shape reports the same readable message:
real backup -> exit 0, hash 27c5c77a945bcecc (matches live)
non-hex key -> FATAL: contains non-hex characters
4-char key -> FATAL: length 4, expected 32 hex chars
no network_key -> FATAL: .network_key.key absent
network_key non-object -> FATAL: .network_key.key absent
network_key number -> FATAL: .network_key.key absent
device link_key in doc -> correctly ignored; network_key used
Verification ran against the alpine-jq:2026-07-30 tag itself.
On the 🔴: the premise does not hold — that tag already exists and is
pullable, verified twice by pulling it. The build step fires on branch
pushes as well as master, so pipeline 3060 published it when this branch
was pushed; the reviewer could not query the registry to see that. The
underlying caution is still worth recording, so the manifest now documents
that TAG is derived from `date` AT RUN TIME, that a date must never be
written here before a build has produced it, and that a later master build
publishes its own tag without deleting this one — so this keeps resolving,
though the image then came from a branch build rather than master. Also
recorded why it matters: Flux reconciles on a 10m poll with no ordering
against kaniko, and replicas: 1 + Recreate means an unpullable init image
would leave the sole coordinator in ImagePullBackOff.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review point on #444. The reviewer cited a precedent I had missed:
ci-ansible:2026-04-29 in kubernetes/infrastructure/netbird-backup/
cronjob.yaml. Verified — for self-built images referenced by tag rather
than digest, this repo already uses DATED tags, with :latest used nowhere.
The argument holds independently of convention: with :latest, rebuilding
this base for some unrelated consumer silently changes what the seed-data
init container runs on its next pod restart, with no diff here to review.
That container renders the Zigbee network key, so "what exactly does it
run" should be visible in the manifest. A dated tag makes a change to it a
deliberate, reviewable act.
This does give up automatic pickup of rebuilds, which is the tradeoff the
previous commit chose the other way round. Given the base is thin and
rarely changes, and the consumer is security-sensitive, reviewability wins
— and it stays consistent with the owner directive that our own images do
not need digest pinning, since a dated tag is not a digest.
Verified gitea.renner.uno/gitea_admin/alpine-jq:2026-07-30 pulls and
carries jq 1.7.1, so the reference resolves before merge rather than after.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Owner directives, both applied here:
- "z2m-seed is a shit name for the image — name it for what it is, not for
how it is used." Renamed to alpine-jq. Nothing in it is
zigbee-specific; it is alpine plus jq, useful to anything that needs to
read JSON from a shell script. The Containerfile now describes it that
way, with the zigbee2mqtt case recorded as its first consumer rather
than its identity.
- "You do not need to pin the digest for our own images." Referenced by
tag. This also collapses what was going to be two PRs into one, since
the digest no longer has to exist before the Deployment can name it.
The seed-data init container now uses that image instead of
`apk add --no-cache jq` on every pod start.
Two consequences of dropping digest pinning, both good:
- The whole --image-name-with-digest-file / "PIN THIS:" apparatus is gone
from the pipeline. That machinery existed only to hand a digest to a
follow-up commit, and it is what broke every pipeline in the repo (#443)
via an unquoted ": " in a YAML scalar. Less surface, same outcome.
- :latest means Kubernetes defaults imagePullPolicy to Always, so a
rebuilt image is picked up without editing the manifest.
Kept the fail-loudly validation, since a wrong network_key costs the whole
Zigbee network: explicit checks for an absent key (jq prints "null" for a
missing path, so that is checked separately rather than being reported as
merely "non-hex"), non-hex characters, and a length other than 32.
Verified by running the manifest's RENDERED script, against the real
coordinator backup and synthetic fixtures. The alpine-jq tag does not
exist until this merges and builds, so the run used the existing image of
identical content (alpine 3.21.7 + jq 1.7.1, verified present):
real backup -> exit 0, network_key line byte-identical to the
live value (sha256 27c5c77a945bcecc)
non-hex key -> exit 1, "contains non-hex characters"
4-char key -> exit 1, "length 4, expected 32 hex chars"
absent network_key -> exit 1, ".network_key.key absent from ..."
device link_key also
present in the doc -> correctly ignored; the network_key is used
Validated with `woodpecker lint` (not just `yq`, which is what let #443
through), plus kustomize build and a server-side dry run. No stale
z2m-seed references remain.
NOTE: the now-orphaned z2m-seed package is still in the registry and
should be deleted; that needs package-delete rights this account does not
have.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
URGENT: this restores CI for the WHOLE repo. My commit 66dfd56b (merged
via #442) left an unquoted YAML scalar containing ": ", so Woodpecker
failed to compile the config:
yaml: construct errors: line 152: cannot unmarshal
'map[echo "PIN THIS:gitea.renner.uno/gitea_admin/z2m-seed@$DIGEST"]'
of type map[string]interface {} into a string value
Because a compile error rejects the entire pipeline set, every pipeline on
master has errored since that merge — not only build-ci-images. Confirmed
from pipeline history: 3048 (the push that introduced it) is the first
failure, and 3049-3053 follow, including an unrelated merge (#440) and the
alert-monitor cron.
Fixed by quoting the command as a single YAML scalar.
Why my pre-merge validation missed it: I checked with `yq '.' >/dev/null`,
which passed because the unquoted form IS valid YAML — it just parses as a
map instead of a string. Parsing successfully is not the same as matching
the schema. `woodpecker lint` catches it immediately and is now noted in a
comment at the line, along with the specific failure mode, so the trap is
not reinstated.
All 17 .woodpecker/*.yml files now lint clean apart from pre-existing
"Additional property timeout is not allowed" warnings in four deploy-*
files, which are untouched by this change and are warnings rather than
errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
# Installing tools at container start is not acceptable here: it
# makes the install path depend on package-index availability at
# exactly the moment a node is being (re)built, and this is a
# DaemonSet — it runs on every node.
#
# Nothing needed to be added in the first place. Verified in
# alpine:3.21: `tar` is already busybox (v1.37.0) and handles
# -xzf, and busybox `wget` follows GitHub's release redirect to
# objects.githubusercontent.com over TLS using the base image's
# /etc/ssl/certs/ca-certificates.crt — fetched the real 48MB
# tarball and extracted an executable macvlan from it.
# Only `curl` is genuinely absent from the base, and wget covers
# what it was used for here.
wget -q -O /tmp/plugins.tgz "${URL}"
# Extract everything: the tarball entries are stored with a
# `./` prefix, so selective extraction by bare name fails.
# All standard plugins are tiny (~5-10MB total) and harmless
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.