Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c133f44d28 | ||
|
|
4c994d1ce3 | ||
|
|
8439a99a98 | ||
|
|
6e4729c7d0 | ||
|
|
f66b127035 | ||
|
|
c3280310b5 | ||
|
|
d1b3521b7f |
@@ -131,19 +131,19 @@ steps:
|
||||
--destination gitea.renner.uno/gitea_admin/secret-rotator:$TAG \
|
||||
--destination gitea.renner.uno/gitea_admin/secret-rotator:latest
|
||||
|
||||
# Runtime image, not CI tooling: alpine + jq, the base for the zigbee2mqtt
|
||||
# `seed-data` init container. Replaces an `apk add --no-cache jq` that ran on
|
||||
# every pod start — tools belong in the image, not installed at runtime.
|
||||
# Consumers pin it by DIGEST (as the secret-rotator CronJobs do), so this must
|
||||
# be built and pushed before the Deployment can reference it.
|
||||
- name: build-z2m-seed
|
||||
# Runtime image, not CI tooling: a general-purpose alpine + jq base for
|
||||
# workloads that need to read JSON from a shell script. Named for what it IS,
|
||||
# not for who uses it — its first consumer is the zigbee2mqtt seed-data init
|
||||
# container, but nothing about it is zigbee-specific. Exists so tools come from
|
||||
# the image rather than an `apk add` on every container start.
|
||||
- name: build-alpine-jq
|
||||
image: gcr.io/kaniko-project/executor:v1.23.2-debug
|
||||
directory: ci/images/z2m-seed
|
||||
directory: ci/images/alpine-jq
|
||||
depends_on: []
|
||||
when:
|
||||
- event: manual
|
||||
- event: push
|
||||
path: ["ci/images/z2m-seed/**", ".woodpecker/build-ci-images.yml"]
|
||||
path: ["ci/images/alpine-jq/**", ".woodpecker/build-ci-images.yml"]
|
||||
environment:
|
||||
GITEA_USER: gitea_admin
|
||||
GITEA_TOKEN:
|
||||
@@ -157,23 +157,5 @@ steps:
|
||||
/kaniko/executor \
|
||||
--context . \
|
||||
--dockerfile Containerfile \
|
||||
--destination gitea.renner.uno/gitea_admin/z2m-seed:$TAG \
|
||||
--destination gitea.renner.uno/gitea_admin/z2m-seed:latest \
|
||||
--image-name-with-digest-file /kaniko/z2m-seed.digest
|
||||
# Print the digest-pinned reference so the follow-up commit that pins the
|
||||
# Deployment can copy it straight from this build log, with no separate
|
||||
# registry query.
|
||||
#
|
||||
# kaniko writes ONE LINE PER --destination to that file, so it holds two
|
||||
# entries (:$TAG and :latest) carrying the same digest. Reduce to the bare
|
||||
# digest and dedupe, so exactly one unambiguous line is emitted rather than
|
||||
# two near-identical ones for whoever does step 2 to choose between.
|
||||
- DIGEST=$(cut -d@ -f2 /kaniko/z2m-seed.digest | sort -u | head -1)
|
||||
# MUST stay single-quoted as one YAML scalar. Unquoted, the ": " inside
|
||||
# makes YAML parse the whole command as a MAP rather than a string, and the
|
||||
# pipeline fails to compile with
|
||||
# cannot unmarshal 'map[echo "PIN THIS:...' into a string value
|
||||
# which errors EVERY pipeline in the repo, not just this one. Note `yq`
|
||||
# accepts the unquoted form because it is valid YAML — just a map, not a
|
||||
# string — so validate with `woodpecker lint`, not a YAML parser.
|
||||
- 'echo "PIN THIS: gitea.renner.uno/gitea_admin/z2m-seed@$DIGEST"'
|
||||
--destination gitea.renner.uno/gitea_admin/alpine-jq:$TAG \
|
||||
--destination gitea.renner.uno/gitea_admin/alpine-jq:latest
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
FROM alpine:3.21@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d
|
||||
|
||||
# Alpine plus jq. Nothing application-specific — a general-purpose base for any
|
||||
# workload that needs to read JSON from a shell script.
|
||||
#
|
||||
# It exists because installing tools at container start is not acceptable here:
|
||||
# `apk add` makes every start depend on network and package-index availability,
|
||||
# and the resolved version can drift between restarts of an otherwise pinned
|
||||
# image. Tools belong in the image.
|
||||
#
|
||||
# Everything else such a script typically needs (sh, sed, cp, cat, tr, awk) comes
|
||||
# from busybox in the base, so jq is the only addition.
|
||||
#
|
||||
# Runs as root like the other ci/images entries. Consumers that can drop
|
||||
# privilege should set runAsNonRoot/runAsUser themselves — what the workload
|
||||
# needs to read is workload-specific, so it does not belong baked in here.
|
||||
#
|
||||
# First consumer is the zigbee2mqtt seed-data init container. Worth recording why
|
||||
# a real JSON parser is justified there rather than a sed/awk match: "key" occurs
|
||||
# 12 times in that document (once per device with an APS link_key) and the FIRST
|
||||
# occurrence is a DEVICE key, because "devices" sorts before "network_key". An
|
||||
# unanchored extraction therefore yields a device link key, and herdsman then
|
||||
# forms a NEW Zigbee network and drops every device.
|
||||
RUN apk add --no-cache jq
|
||||
@@ -1,21 +0,0 @@
|
||||
FROM alpine:3.21@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d
|
||||
|
||||
# Runtime image, not CI tooling: the base for the zigbee2mqtt `seed-data` init
|
||||
# container.
|
||||
#
|
||||
# It exists for exactly one reason — jq. That init container reads
|
||||
# `.network_key.key` out of the Open Coordinator Backup JSON to render
|
||||
# /app/data/secret.yaml; everything else it does (sh, sed, cp, cat) is already
|
||||
# in busybox. jq is baked in here rather than `apk add`ed when the container
|
||||
# starts, so the init container has no network dependency at start and no
|
||||
# package-index drift between pod restarts.
|
||||
#
|
||||
# Parsing this with busybox sed/awk instead was considered and rejected: "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". Hand-rolled extraction that isn't correctly anchored yields a
|
||||
# device link key, and herdsman then forms a NEW network and drops every
|
||||
# device — so a real JSON parser earns its keep here.
|
||||
#
|
||||
# Stays root: the init container writes to the root-owned zigbee2mqtt-data PVC.
|
||||
RUN apk add --no-cache jq
|
||||
@@ -39,16 +39,80 @@ spec:
|
||||
# network_key, the latter extracted from the OpenBao coordinator backup
|
||||
# as a natural-byte-order int array.
|
||||
- name: seed-data
|
||||
image: alpine:3.21
|
||||
# alpine + jq, built by .woodpecker/build-ci-images.yml from
|
||||
# ci/images/alpine-jq/. Replaces `apk add --no-cache jq` on every pod
|
||||
# start: tools belong in the image, not installed at runtime.
|
||||
# Referenced by tag rather than digest — these are our own images.
|
||||
# DATED tag, not :latest, matching the ci-ansible:2026-04-29 precedent
|
||||
# in kubernetes/infrastructure/netbird-backup/cronjob.yaml. With
|
||||
# :latest, rebuilding this base for some unrelated consumer would
|
||||
# silently change what this init container runs on its next pod
|
||||
# restart, with no diff here to review — and this is the container that
|
||||
# renders the Zigbee network key. Bumping the tag is a deliberate,
|
||||
# reviewable act; the cost is remembering to bump it.
|
||||
#
|
||||
# This tag ALREADY EXISTS and was verified pullable before merge — the
|
||||
# build step also fires on branch pushes, so it was published when this
|
||||
# branch was pushed. That matters because Flux reconciles this directory
|
||||
# on a 10m poll with no ordering against the kaniko pipeline, and with
|
||||
# replicas: 1 + strategy: Recreate an unpullable init image would leave
|
||||
# the sole Zigbee coordinator in ImagePullBackOff.
|
||||
#
|
||||
# Caveat when bumping: the build derives TAG from `date` AT RUN TIME, so
|
||||
# never write a date here that a build has not already produced. If a
|
||||
# later master build runs on a different day it publishes that day's tag
|
||||
# and leaves this one untouched (tags are not deleted), so this keeps
|
||||
# resolving — but the image then came from the branch build rather than
|
||||
# master. Re-point it at a master-built tag when convenient.
|
||||
image: gitea.renner.uno/gitea_admin/alpine-jq:2026-07-30
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
set -eu
|
||||
umask 077
|
||||
apk add --no-cache jq >/dev/null
|
||||
# Read the network key with a real JSON parser. NOT a hand-rolled
|
||||
# sed/awk match: "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 unanchored
|
||||
# extraction yields a device link key, and herdsman then forms a
|
||||
# NEW network and drops every device.
|
||||
# `?` on BOTH selectors, plus the `// ""` fallback, so every
|
||||
# malformed shape reaches the checks below with a readable message
|
||||
# instead of aborting on jq's raw error under `set -e`.
|
||||
#
|
||||
# Measured against jq 1.7.1 in this image. The expression AS WRITTEN
|
||||
# returns an EMPTY string at rc=0 for every one of:
|
||||
# {} {"devices":[]} {"network_key":{}} {"network_key":null}
|
||||
# {"network_key":"abc"} {"network_key":123} (parent not an object)
|
||||
# [] "a string" (top level not an object)
|
||||
# and the key itself for a well-formed backup.
|
||||
#
|
||||
# The guards are what make that true. Bare `.network_key.key` exits
|
||||
# 5 with `Cannot index ... with string "key"` on the last four
|
||||
# shapes, and `set -e` would abort right here, surfacing jq's error
|
||||
# instead of the FATAL lines below.
|
||||
KEY=$(jq -r '(.network_key?.key? // "")' /backup/coordinator_backup.json)
|
||||
# Fail LOUDLY rather than render a wrong key, for the same reason.
|
||||
# `-z` is the arm that actually fires today: `// ""` maps a null
|
||||
# result to empty, so KEY is never the literal text "null" with the
|
||||
# expression above. The "null" arm is belt-and-braces for a future
|
||||
# edit that drops the fallback, where jq WOULD print `null`. Both
|
||||
# mean "absent" — reporting them as merely "non-hex" would send you
|
||||
# looking at the key instead of at the missing field.
|
||||
if [ "$KEY" = "null" ] || [ -z "$KEY" ]; then
|
||||
echo "FATAL: .network_key.key absent from coordinator_backup.json" >&2
|
||||
exit 1
|
||||
fi
|
||||
case "$KEY" in
|
||||
*[!0-9a-fA-F]*) echo "FATAL: network_key contains non-hex characters" >&2; exit 1 ;;
|
||||
esac
|
||||
if [ "${#KEY}" -ne 32 ]; then
|
||||
echo "FATAL: network_key length ${#KEY}, expected 32 hex chars" >&2
|
||||
exit 1
|
||||
fi
|
||||
# hex key "fe0b..2d" -> "0xfe,0x0b,..,0x2d" (natural order)
|
||||
NK=$(jq -r '.network_key.key' /backup/coordinator_backup.json | sed 's/../0x&,/g; s/,$//')
|
||||
NK=$(printf '%s' "$KEY" | sed 's/../0x&,/g; s/,$//')
|
||||
cat > /app/data/secret.yaml <<EOF
|
||||
mqtt_password: "${MQTT_PASSWORD}"
|
||||
network_key: [${NK}]
|
||||
|
||||
Reference in New Issue
Block a user