diff --git a/.woodpecker/build-ci-images.yml b/.woodpecker/build-ci-images.yml index 9b746e9a..2bc5caef 100644 --- a/.woodpecker/build-ci-images.yml +++ b/.woodpecker/build-ci-images.yml @@ -217,3 +217,43 @@ steps: --dockerfile Containerfile \ --destination gitea.renner.uno/gitea_admin/alpine-openssl:$TAG \ --destination gitea.renner.uno/gitea_admin/alpine-openssl:latest + + # Runtime image, not CI tooling: alpine-jq PLUS curl, LAYERED on that image + # rather than repeating its build, so jq lives in exactly one Containerfile and + # the shared layers are pulled once. Consumed by the mosquitto bootstrap Jobs, + # which talk to the Kubernetes API and build JSON bodies. Replaces an + # `apk add curl jq` that ran on every Job execution. + # + # depends_on build-alpine-jq (not []) because this FROMs it: the base must be + # built and pushed before it is layered on. Combined with --build-arg + # BASE_TAG=$TAG below, that means this image is ALWAYS layered on the base + # built in the SAME pipeline run, so a base change (e.g. a CVE patch) + # propagates automatically instead of silently rebuilding against a stale + # hardcoded tag. + - name: build-alpine-curl-jq + image: gcr.io/kaniko-project/executor:v1.23.2-debug + directory: ci/images/alpine-curl-jq + depends_on: [build-alpine-jq] + when: + - event: manual + - event: push + path: + - "ci/images/alpine-curl-jq/**" + - "ci/images/alpine-jq/**" + - ".woodpecker/build-ci-images.yml" + environment: + GITEA_USER: gitea_admin + GITEA_TOKEN: + from_secret: ci_images_token + commands: + - mkdir -p /kaniko/.docker + - AUTH=$(printf '%s:%s' "$GITEA_USER" "$GITEA_TOKEN" | base64 | tr -d '\n') + - printf '{"auths":{"gitea.renner.uno":{"auth":"%s"}}}' "$AUTH" > /kaniko/.docker/config.json + - TAG=$(date +%Y-%m-%d) + - | + /kaniko/executor \ + --context . \ + --dockerfile Containerfile \ + --build-arg BASE_TAG=$TAG \ + --destination gitea.renner.uno/gitea_admin/alpine-curl-jq:$TAG \ + --destination gitea.renner.uno/gitea_admin/alpine-curl-jq:latest diff --git a/ci/images/alpine-curl-jq/Containerfile b/ci/images/alpine-curl-jq/Containerfile new file mode 100644 index 00000000..20a44e63 --- /dev/null +++ b/ci/images/alpine-curl-jq/Containerfile @@ -0,0 +1,29 @@ +# LAYERED on alpine-jq rather than repeating its build. jq is installed in +# exactly one place (ci/images/alpine-jq/Containerfile); this image is that plus +# curl, and shares every layer below it — so consumers that pull both get the +# common layers once. +# +# BASE_TAG is supplied by the pipeline as the SAME `date +%Y-%m-%d` value used for +# this image's own tags, and build-alpine-curl-jq depends_on build-alpine-jq, so +# the base for today's date is always pushed before this is layered onto it. +# +# Deliberately NOT a hardcoded dated tag. With one, a rebuild of alpine-jq — a CVE +# patch, say — would never reach this image: the pipeline's when.path already +# retriggers this step when the base's directory changes, but it would rebuild +# against the OLD pinned tag. That is worse than not rebuilding at all, because +# the step goes green while shipping a stale base. Passing the tag through makes +# the two images move together. +# +# The `latest` default applies only to a manual `docker build` outside the +# pipeline; CI always passes BASE_TAG explicitly. +ARG BASE_TAG=latest +FROM gitea.renner.uno/gitea_admin/alpine-jq:${BASE_TAG} + +# curl, not busybox wget, on purpose. busybox wget has --header and --post-data +# but NO --ca-certificate, so talking to the Kubernetes API with the +# ServiceAccount CA needs the CA appended to the system trust bundle first. +# That works (verified: TLS validated, request reached RBAC and returned 403), +# but it changes error semantics versus `curl -f` and busybox sets its own +# Content-Type for --post-data. The consumers here mint Secrets, so keeping curl +# means their scripts are untouched apart from deleting the apk add line. +RUN apk add --no-cache curl diff --git a/kubernetes/apps/home-assistant/mosquitto-bootstrap.yaml b/kubernetes/apps/home-assistant/mosquitto-bootstrap.yaml index e5f60cd6..633ff76d 100644 --- a/kubernetes/apps/home-assistant/mosquitto-bootstrap.yaml +++ b/kubernetes/apps/home-assistant/mosquitto-bootstrap.yaml @@ -1,4 +1,21 @@ --- +# ALL SEVEN bootstrap Jobs in this file run +# gitea.renner.uno/gitea_admin/alpine-curl-jq (built by +# .woodpecker/build-ci-images.yml from ci/images/alpine-curl-jq/, itself layered +# FROM alpine-jq so jq is installed in exactly one place). They previously used +# alpine:3.21 and ran `apk add --no-cache curl jq` on every execution, which made +# each Job depend on package-index availability at run time. Tools belong in the +# image; the scripts below are otherwise unchanged. +# +# curl rather than busybox wget on purpose: busybox wget has --header and +# --post-data but no --ca-certificate, so reaching the Kubernetes API with the +# ServiceAccount CA would need the CA appended to the system trust bundle, and +# its error semantics and Content-Type handling differ from `curl -f`. These Jobs +# mint credentials, so their logic was left alone. +# +# Dated tag, matching the ci-ansible / alpine-jq convention: a change to what +# these Jobs run is a reviewable diff here rather than a silent rebuild. +--- # Generates mosquitto-credentials on first deploy. The password is created # in-cluster, lives only in the Secret object, and is never written to git # or seen by a human. The Mosquitto broker hashes it via mosquitto_passwd @@ -55,13 +72,12 @@ spec: restartPolicy: OnFailure containers: - name: bootstrap - image: alpine:3.21 + image: gitea.renner.uno/gitea_admin/alpine-curl-jq:2026-07-30 command: - sh - -c - | set -eu - apk add --no-cache curl jq >/dev/null NS=home-assistant NAME=mosquitto-credentials @@ -123,13 +139,12 @@ spec: restartPolicy: OnFailure containers: - name: bootstrap - image: alpine:3.21 + image: gitea.renner.uno/gitea_admin/alpine-curl-jq:2026-07-30 command: - sh - -c - | set -eu - apk add --no-cache curl jq >/dev/null NS=home-assistant NAME=mosquitto-admin-credentials @@ -190,13 +205,12 @@ spec: restartPolicy: OnFailure containers: - name: bootstrap - image: alpine:3.21 + image: gitea.renner.uno/gitea_admin/alpine-curl-jq:2026-07-30 command: - sh - -c - | set -eu - apk add --no-cache curl jq >/dev/null NS=home-assistant NAME=mosquitto-shellies-credentials @@ -256,13 +270,12 @@ spec: restartPolicy: OnFailure containers: - name: bootstrap - image: alpine:3.21 + image: gitea.renner.uno/gitea_admin/alpine-curl-jq:2026-07-30 command: - sh - -c - | set -eu - apk add --no-cache curl jq >/dev/null NS=home-assistant NAME=mosquitto-sml2mqtt-credentials @@ -324,13 +337,12 @@ spec: restartPolicy: OnFailure containers: - name: bootstrap - image: alpine:3.21 + image: gitea.renner.uno/gitea_admin/alpine-curl-jq:2026-07-30 command: - sh - -c - | set -eu - apk add --no-cache curl jq >/dev/null NS=home-assistant NAME=mosquitto-open3e-credentials @@ -392,13 +404,12 @@ spec: restartPolicy: OnFailure containers: - name: bootstrap - image: alpine:3.21 + image: gitea.renner.uno/gitea_admin/alpine-curl-jq:2026-07-30 command: - sh - -c - | set -eu - apk add --no-cache curl jq >/dev/null NS=home-assistant NAME=mosquitto-health-credentials @@ -461,13 +472,12 @@ spec: restartPolicy: OnFailure containers: - name: bootstrap - image: alpine:3.21 + image: gitea.renner.uno/gitea_admin/alpine-curl-jq:2026-07-30 command: - sh - -c - | set -eu - apk add --no-cache curl jq >/dev/null NS=home-assistant NAME=mosquitto-zigbee2mqtt-credentials