From 3f5387979c33eb21cde42c33e4f43d3dd6989ff5 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Thu, 30 Jul 2026 14:41:36 +0200 Subject: [PATCH 1/2] fix(mosquitto): drop runtime apk add from the 7 bootstrap Jobs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Last of the runtime `apk add` cleanups. Each of the seven credential bootstrap Jobs ran `apk add --no-cache curl jq` on every execution, making them depend on package-index availability at run time. Adds ci/images/alpine-curl-jq, LAYERED `FROM alpine-jq` rather than repeating that image's build. jq is therefore installed in exactly one Containerfile, the shared layers are pulled once, and the relationship between the two images is explicit instead of implied by duplicated instructions. The pipeline step uses depends_on: [build-alpine-jq] and also triggers on changes to the base's path, so the base is built before anything is layered onto it. Kept curl rather than switching to busybox wget, deliberately. wget has --header and --post-data but NO --ca-certificate, so reaching the Kubernetes API with the ServiceAccount CA needs the CA appended to the system trust bundle first. That does work — verified in-cluster: TLS validated and the request reached RBAC, returning 403 rather than a certificate error — but it changes error semantics versus `curl -f` and busybox sets its own Content-Type for --post-data. These Jobs mint credentials, and the existence check they branch on decides whether a Secret gets created at all, so their logic was left untouched: the diff is one deleted line and one image change per Job. Rationale recorded once at the top of the file rather than repeated seven times. Verified all seven Jobs render with the new image and no apk add: mosquitto-admin-credentials-bootstrap -> alpine-curl-jq:2026-07-30 mosquitto-credentials-bootstrap -> alpine-curl-jq:2026-07-30 mosquitto-health-credentials-bootstrap -> alpine-curl-jq:2026-07-30 mosquitto-open3e-credentials-bootstrap -> alpine-curl-jq:2026-07-30 mosquitto-shellies-credentials-bootstrap -> alpine-curl-jq:2026-07-30 mosquitto-sml2mqtt-credentials-bootstrap -> alpine-curl-jq:2026-07-30 mosquitto-zigbee2mqtt-credentials-bootstrap-> alpine-curl-jq:2026-07-30 Co-Authored-By: Claude Opus 5 (1M context) --- .woodpecker/build-ci-images.yml | 40 +++++++++++++++++++ ci/images/alpine-curl-jq/Containerfile | 29 ++++++++++++++ .../home-assistant/mosquitto-bootstrap.yaml | 38 +++++++++++------- 3 files changed, 93 insertions(+), 14 deletions(-) create mode 100644 ci/images/alpine-curl-jq/Containerfile 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 -- 2.54.0 From fe6de1804822e5d82c3845dc39e1fdd8b89f5315 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Thu, 30 Jul 2026 14:55:23 +0200 Subject: [PATCH 2/2] fix(mosquitto): propagate the alpine-jq base tag instead of hardcoding it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review point on #449, and a real flaw rather than a nit. The Containerfile pinned `FROM alpine-jq:2026-07-30`. The pipeline step already retriggers on changes to ci/images/alpine-jq/**, so a base rebuild — a CVE patch, say — WOULD rebuild this image, but against the old pinned tag. That is worse than not rebuilding: the step goes green while shipping a stale base, so the fix looks applied and is not. Now ARG BASE_TAG, passed by the pipeline as the same date value used for this image own tags, with depends_on: [build-alpine-jq] guaranteeing the base for that date is pushed first. The two images therefore always move together. The `latest` default applies only to a manual docker build outside CI. Co-Authored-By: Claude Opus 5 (1M context) -- 2.54.0