Operator rule 2026-10-29: image creation, hosting and lifecycle belong to CI/CD, never to a workstation build/push. Nothing had ever been pushed to the Gitea registry. - .gitea/workflows/images.yaml + scripts/images.sh: PRs build+verify (no credential); main builds, verifies and pushes toolbox, agent-runner (FROM toolbox) and conformance-runner as sha-<commit> + main, reading the digest back from the registry. Only changed images build. - Each image's own build-image.sh does the build and in-image verification, now engine-neutral (DinD: no podman-only calls, probe over stdin not a bind mount). New toolbox build-image.sh. - Digest references carry `# renovate-digest:` markers; renovate.json5 gains a custom.regex manager for them. The worker Job template is deliberately untracked (it would loop). - renovate.yaml pins renovate-version 44.167.0: the action's default (39.264.0) rejects the current config's managerFilePatterns, so every run would have died at CONFIG_VALIDATION. - Push credential: REGISTRY_PUSH_TOKEN secret; the Actions token has no package scope in Gitea 1.24.3 (container.go Authenticate). Retention: infra/gitea/registry-retention.sh. Co-Authored-By: Claude Opus 5.5 <[email protected]>
109 lines
5.4 KiB
Bash
Executable File
109 lines
5.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
###############################################################################
|
|
# infra/devcontainer/build-image.sh — build the pinned dev `toolbox` image and prove it works.
|
|
#
|
|
# ./build-image.sh # build + verify (tag: home-k3s-dev:local)
|
|
# ./build-image.sh --no-verify # build only
|
|
# IMAGE=foo:bar ./build-image.sh # build under another local name
|
|
# ENGINE=docker ./build-image.sh # build with docker instead of podman
|
|
#
|
|
# ONE BUILD DEFINITION, TWO CALLERS. `dev.sh build` calls this for local development, and the
|
|
# CI image pipeline (.gitea/workflows/images.yaml -> .gitea/workflows/scripts/images.sh) calls
|
|
# it to build the image it then PUBLISHES as gitea.home.keithjackson.dev/home-network/toolbox.
|
|
# Keeping the build args in exactly one place is what makes "the toolbox CI publishes" and "the
|
|
# toolbox you gate with locally" the same recipe.
|
|
#
|
|
# THIS SCRIPT NEVER PUSHES. Publishing is CI's job (images.yaml, on push to main), not a
|
|
# workstation's: an image that exists only because someone ran `podman push` from a laptop has
|
|
# no build log, no recorded inputs and no verification anyone else can read.
|
|
#
|
|
# VERSIONS ARE NOT DEFINED HERE. infra/devcontainer/versions.env is the single source of truth.
|
|
###############################################################################
|
|
set -euo pipefail
|
|
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
ENGINE="${ENGINE:-podman}"
|
|
IMAGE="${IMAGE:-home-k3s-dev:local}"
|
|
|
|
VERIFY=1
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--no-verify) VERIFY=0 ;;
|
|
-h|--help) sed -n '2,/^####/p' "${BASH_SOURCE[0]}" | sed '$d' | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
*) echo "unknown argument: $arg" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
log() { printf '\n\033[1m==> %s\033[0m\n' "$*"; }
|
|
die() { printf '\033[31mERROR: %s\033[0m\n' "$*" >&2; exit 1; }
|
|
|
|
command -v "${ENGINE}" >/dev/null 2>&1 || die "'${ENGINE}' not on PATH (set ENGINE=docker to use docker)"
|
|
|
|
# shellcheck disable=SC1091
|
|
source "${HERE}/versions.env"
|
|
|
|
# Provenance: CI passes the commit it is building, so a pulled image names its source tree.
|
|
LABEL_ARGS=()
|
|
[ -z "${BUILD_REVISION:-}" ] || LABEL_ARGS+=(--label "org.opencontainers.image.revision=${BUILD_REVISION}")
|
|
|
|
log "Building ${IMAGE}"
|
|
"$ENGINE" build "${LABEL_ARGS[@]}" \
|
|
--build-arg FEDORA_VERSION="$FEDORA_VERSION" \
|
|
--build-arg OPENTOFU_VERSION="$OPENTOFU_VERSION" \
|
|
--build-arg AGE_VERSION="$AGE_VERSION" \
|
|
--build-arg SOPS_VERSION="$SOPS_VERSION" \
|
|
--build-arg FLUX_VERSION="$FLUX_VERSION" \
|
|
--build-arg KUBECTL_VERSION="$KUBECTL_VERSION" \
|
|
--build-arg HELM_VERSION="$HELM_VERSION" \
|
|
--build-arg KYVERNO_VERSION="$KYVERNO_VERSION" \
|
|
--build-arg KYVERNO_JSON_VERSION="$KYVERNO_JSON_VERSION" \
|
|
--build-arg CHAINSAW_VERSION="$CHAINSAW_VERSION" \
|
|
--build-arg YAMLLINT_VERSION="$YAMLLINT_VERSION" \
|
|
--build-arg YQ_VERSION="$YQ_VERSION" \
|
|
--build-arg CHECK_JSONSCHEMA_VERSION="$CHECK_JSONSCHEMA_VERSION" \
|
|
--build-arg PROMTOOL_VERSION="$PROMTOOL_VERSION" \
|
|
-t "$IMAGE" -f "${HERE}/Containerfile" "${HERE}"
|
|
|
|
# --- verify ------------------------------------------------------------------
|
|
# A build that succeeded is not evidence the toolbox can run the gate. Every tool the gate and
|
|
# the runbooks call must EXECUTE, and the version-sensitive ones must report the version
|
|
# versions.env pins — promtool above all, because ci/versions.env asserts a promtool/Prometheus
|
|
# lockstep and a drifted engine evaluates alert-rule tests differently (see versions.env).
|
|
if [ "${VERIFY}" -eq 1 ]; then
|
|
log "Verifying the toolchain inside ${IMAGE}"
|
|
"$ENGINE" run --rm --network none --entrypoint "" \
|
|
-e PROMTOOL_VERSION="${PROMTOOL_VERSION}" \
|
|
-e KUBECTL_VERSION="${KUBECTL_VERSION}" \
|
|
-e CHAINSAW_VERSION="${CHAINSAW_VERSION}" \
|
|
-e KYVERNO_VERSION="${KYVERNO_VERSION}" \
|
|
-e SOPS_VERSION="${SOPS_VERSION}" \
|
|
"$IMAGE" /bin/bash -euo pipefail -c '
|
|
echo "--- baked versions ---"; cat /etc/dev-toolchain-versions
|
|
echo "--- every tool is on PATH ---"
|
|
for t in tofu age age-keygen sops flux kubectl helm kyverno kyverno-json chainsaw yq \
|
|
yamllint check-jsonschema promtool ansible-playbook git make jq diff python3 \
|
|
wg qrencode; do
|
|
if command -v "$t" >/dev/null 2>&1; then printf " ok %s\n" "$t";
|
|
else printf " MISSING %s\n" "$t"; missing=1; fi
|
|
done
|
|
[ -z "${missing:-}" ] || { echo "toolbox is missing tools"; exit 1; }
|
|
echo "--- version-sensitive tools report their pins ---"
|
|
check() { # name pin actual-output
|
|
if printf "%s" "$3" | grep -qF "$2"; then printf " ok %-10s %s\n" "$1" "$2";
|
|
else printf " DRIFT %-10s want %s, got: %s\n" "$1" "$2" "$(printf "%s" "$3" | head -1)"; drift=1; fi
|
|
}
|
|
check promtool "${PROMTOOL_VERSION}" "$(promtool --version 2>&1)"
|
|
check kubectl "${KUBECTL_VERSION}" "$(kubectl version --client 2>&1)"
|
|
check chainsaw "${CHAINSAW_VERSION}" "$(chainsaw version 2>&1)"
|
|
check kyverno "${KYVERNO_VERSION}" "$(kyverno version 2>&1)"
|
|
check sops "${SOPS_VERSION}" "$(sops --version --disable-version-check 2>&1 || sops --version 2>&1)"
|
|
[ -z "${drift:-}" ] || { echo "a tool does not report its pinned version"; exit 1; }
|
|
echo "--- python can import what the renderer and the gate need ---"
|
|
python3 -c "import yaml; print(\" PyYAML\", yaml.__version__)"
|
|
'
|
|
log "Toolbox verified"
|
|
fi
|
|
|
|
log "Built ${IMAGE}"
|
|
printf ' image id %s\n' "$("$ENGINE" image inspect --format '{{.Id}}' "$IMAGE")"
|