From d2040789e54270b7d3c4d251bc5e85c7393a7ed6 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Sun, 27 Sep 2026 17:55:29 +1000 Subject: [PATCH 1/5] Add matrixed gobuilder image build Replace the packer-images gobuilder with a Dockerfile matrix built on each container-base distro image, on a single repo semver. - install golang, gcc and nfpm - bake the sha256-pinned go-cache-plugin v0.1.0 binary onto PATH - run in-image assertions as a --target test build stage - add pull_request build/test/pre-commit and tag-triggered docker workflows --- .woodpecker/build.yaml | 34 +++++++++++++++++++ .woodpecker/docker.yaml | 66 +++++++++++++++++++++++++++++++++++++ .woodpecker/pre-commit.yaml | 18 ++++++++++ .woodpecker/test.yaml | 36 ++++++++++++++++++++ Dockerfile | 30 +++++++++++++++++ Makefile | 42 +++++++++++++++++++++++ README.md | 28 +++++++++++++++- distros/alma9/distro.env | 9 +++++ pre-commit.sh | 27 +++++++++++++++ test.sh | 49 +++++++++++++++++++++++++++ 10 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 .woodpecker/build.yaml create mode 100644 .woodpecker/docker.yaml create mode 100644 .woodpecker/pre-commit.yaml create mode 100644 .woodpecker/test.yaml create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 distros/alma9/distro.env create mode 100755 pre-commit.sh create mode 100755 test.sh diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml new file mode 100644 index 0000000..a6da167 --- /dev/null +++ b/.woodpecker/build.yaml @@ -0,0 +1,34 @@ +when: + - event: pull_request + +matrix: + include: + # One entry per distro. Adding a distro: add distros// and one entry here. + - DISTRO: alma9 + BASE_IMAGE_NAME: almalinux9 + TAG_SUFFIX: alma9 + DEFAULT: "true" + +steps: + - name: build + image: "artifactapi.k8s.syd1.au.unkin.net/docker-internal/plugin-docker-buildx:latest" + settings: + repo: "artifactapi.k8s.syd1.au.unkin.net/docker-internal/gobuilder" + target: image + dry_run: true + build_args: + BASE_IMAGE_NAME: "${BASE_IMAGE_NAME}" + DISTRO: "${DISTRO}" + buildkit_config: | + [registry."artifactapi.k8s.syd1.au.unkin.net"] + ca = ["/etc/docker/certs.d/artifactapi.k8s.syd1.au.unkin.net/ca.crt"] + backend_options: + kubernetes: + serviceAccountName: default + resources: + requests: + memory: 1Gi + cpu: 1 + limits: + memory: 4Gi + cpu: 2 diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml new file mode 100644 index 0000000..b42a902 --- /dev/null +++ b/.woodpecker/docker.yaml @@ -0,0 +1,66 @@ +when: + - event: tag + ref: refs/tags/v* + +matrix: + include: + # One entry per distro. Adding a distro: add distros// and one entry here. + - DISTRO: alma9 + BASE_IMAGE_NAME: almalinux9 + TAG_SUFFIX: alma9 + DEFAULT: "true" + +steps: + - name: docker + image: "artifactapi.k8s.syd1.au.unkin.net/docker-internal/plugin-docker-buildx:latest" + settings: + registry: artifactapi.k8s.syd1.au.unkin.net + repo: "artifactapi.k8s.syd1.au.unkin.net/docker-internal/gobuilder" + target: image + build_args: + BASE_IMAGE_NAME: "${BASE_IMAGE_NAME}" + DISTRO: "${DISTRO}" + buildkit_config: | + [registry."artifactapi.k8s.syd1.au.unkin.net"] + ca = ["/etc/docker/certs.d/artifactapi.k8s.syd1.au.unkin.net/ca.crt"] + tags: + - "${CI_COMMIT_TAG##v}-${TAG_SUFFIX}" + backend_options: + kubernetes: + serviceAccountName: default + resources: + requests: + memory: 1Gi + cpu: 1 + limits: + memory: 4Gi + cpu: 2 + + # The default distro also carries the unsuffixed tags. + - name: docker-default + image: "artifactapi.k8s.syd1.au.unkin.net/docker-internal/plugin-docker-buildx:latest" + when: + - evaluate: 'DEFAULT == "true"' + settings: + registry: artifactapi.k8s.syd1.au.unkin.net + repo: "artifactapi.k8s.syd1.au.unkin.net/docker-internal/gobuilder" + target: image + build_args: + BASE_IMAGE_NAME: "${BASE_IMAGE_NAME}" + DISTRO: "${DISTRO}" + buildkit_config: | + [registry."artifactapi.k8s.syd1.au.unkin.net"] + ca = ["/etc/docker/certs.d/artifactapi.k8s.syd1.au.unkin.net/ca.crt"] + tags: + - "${CI_COMMIT_TAG##v}" + - latest + backend_options: + kubernetes: + serviceAccountName: default + resources: + requests: + memory: 1Gi + cpu: 1 + limits: + memory: 4Gi + cpu: 2 diff --git a/.woodpecker/pre-commit.yaml b/.woodpecker/pre-commit.yaml new file mode 100644 index 0000000..b6239c8 --- /dev/null +++ b/.woodpecker/pre-commit.yaml @@ -0,0 +1,18 @@ +when: + - event: pull_request + +steps: + - name: pre-commit + image: "docker.io/almalinux:9" + commands: + - ./pre-commit.sh + backend_options: + kubernetes: + serviceAccountName: default + resources: + requests: + memory: 512Mi + cpu: 1 + limits: + memory: 2Gi + cpu: 2 diff --git a/.woodpecker/test.yaml b/.woodpecker/test.yaml new file mode 100644 index 0000000..152ab47 --- /dev/null +++ b/.woodpecker/test.yaml @@ -0,0 +1,36 @@ +when: + - event: pull_request + +matrix: + include: + # One entry per distro. Adding a distro: add distros// and one entry here. + - DISTRO: alma9 + BASE_IMAGE_NAME: almalinux9 + TAG_SUFFIX: alma9 + DEFAULT: "true" + +steps: + # The test stage runs test.sh inside the freshly built image, so a failed assertion + # fails the build. No docker daemon required. + - name: test + image: "artifactapi.k8s.syd1.au.unkin.net/docker-internal/plugin-docker-buildx:latest" + settings: + repo: "artifactapi.k8s.syd1.au.unkin.net/docker-internal/gobuilder" + target: test + dry_run: true + build_args: + BASE_IMAGE_NAME: "${BASE_IMAGE_NAME}" + DISTRO: "${DISTRO}" + buildkit_config: | + [registry."artifactapi.k8s.syd1.au.unkin.net"] + ca = ["/etc/docker/certs.d/artifactapi.k8s.syd1.au.unkin.net/ca.crt"] + backend_options: + kubernetes: + serviceAccountName: default + resources: + requests: + memory: 1Gi + cpu: 1 + limits: + memory: 4Gi + cpu: 2 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..303bb71 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# syntax=docker/dockerfile:1 +ARG REGISTRY=artifactapi.k8s.syd1.au.unkin.net/docker-internal +# The container-base release this image is built on — the one place to bump it. +ARG BASE_VERSION=0.1.0 +ARG BASE_IMAGE_NAME=almalinux9 + +FROM ${REGISTRY}/${BASE_IMAGE_NAME}:${BASE_VERSION} AS image +ARG DISTRO=alma9 +ARG GO_CACHE_PLUGIN_VERSION=v0.1.0 +ARG GO_CACHE_PLUGIN_SHA256=0eb23d06a0dcf395559cb6447864579c220e60148fdcd6a6f841c2b17169b9b8 +COPY distros/${DISTRO}/ /tmp/distro/ +RUN set -eux; \ + . /tmp/distro/distro.env; \ + dnf install -y $PACKAGES; \ + dnf clean all; \ + curl -fsSL -o /usr/local/bin/go-cache-plugin \ + "https://git.unkin.net/unkin/go-cache-plugin/releases/download/${GO_CACHE_PLUGIN_VERSION}/go-cache-plugin-linux-amd64"; \ + echo "$GO_CACHE_PLUGIN_SHA256 /usr/local/bin/go-cache-plugin" | sha256sum -c -; \ + chmod 0755 /usr/local/bin/go-cache-plugin; \ + rm -rf /var/cache/dnf /tmp/distro +CMD ["/bin/bash"] + +# Built only via `--target test`; assertions run as a build step so a broken image +# fails the build, with no docker daemon needed in CI. +FROM image AS test +ARG DISTRO=alma9 +COPY distros/${DISTRO}/distro.env test.sh /tmp/test/ +RUN cd /tmp/test && ./test.sh + +FROM image AS final diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6a76311 --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ +REGISTRY := artifactapi.k8s.syd1.au.unkin.net/docker-internal +DISTROS := $(notdir $(wildcard distros/*)) +VERSION ?= $(patsubst v%,%,$(or $(shell git describe --tags --exact-match 2>/dev/null),0.1.0-dev)) + +.PHONY: build test clean patch minor major +build: $(addprefix build-,$(DISTROS)) +test: $(addprefix test-,$(DISTROS)) +clean: $(addprefix clean-,$(DISTROS)) + +# Not .PHONY: GNU make skips implicit-rule search for phony targets. + +build-%: + @set -eu; . ./distros/$*/distro.env; \ + docker build --build-arg "BASE_IMAGE_NAME=$$BASE_IMAGE_NAME" --build-arg "DISTRO=$*" \ + -t "$(REGISTRY)/$$IMAGE:$(VERSION)-$$TAG_SUFFIX" . + +test-%: + @set -eu; . ./distros/$*/distro.env; \ + docker build --target test --progress plain \ + --build-arg "BASE_IMAGE_NAME=$$BASE_IMAGE_NAME" --build-arg "DISTRO=$*" . + +clean-%: + @set -eu; . ./distros/$*/distro.env; \ + docker rmi "$(REGISTRY)/$$IMAGE:$(VERSION)-$$TAG_SUFFIX" 2>/dev/null || true + +_LATEST := $(shell git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$$' | head -1) +_BASE := $(if $(_LATEST),$(_LATEST),v0.0.0) +_MAJ := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f1) +_MIN := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f2) +_PAT := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f3) + +patch: + @NEW=v$(_MAJ).$(_MIN).$(shell expr $(_PAT) + 1); \ + git tag $$NEW && echo "Tagged $$NEW" && git push origin $$NEW + +minor: + @NEW=v$(_MAJ).$(shell expr $(_MIN) + 1).0; \ + git tag $$NEW && echo "Tagged $$NEW" && git push origin $$NEW + +major: + @NEW=v$(shell expr $(_MAJ) + 1).0.0; \ + git tag $$NEW && echo "Tagged $$NEW" && git push origin $$NEW diff --git a/README.md b/README.md index 2fade9a..f74a4d1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,29 @@ # container-gobuilder -Go toolchain builder container image, built on each supported container-base distro image, used by Woodpecker pipelines to compile Go projects. \ No newline at end of file +The estate's Go toolchain image, built on each `container-base` distro image. +Published as `artifactapi.k8s.syd1.au.unkin.net/docker-internal/gobuilder:-`; +the default distro also gets the unsuffixed `:` and `:latest`. + +| distro | from | tag suffix | default | +| --- | --- | --- | --- | +| `alma9` | `docker-internal/almalinux9:` | `alma9` | yes | + +Adds `golang`, `gcc` and `nfpm`, and bakes in the `go-cache-plugin` binary on `PATH` +so Go pipelines get the shared build cache with no download step. No credentials and no +`GOCACHEPROG` value are baked in — consumers set those. + +The `container-base` release this image builds on is `BASE_VERSION` in the `Dockerfile`. + +## Adding a distro + +1. `distros//distro.env` — `BASE_IMAGE_NAME`, `IMAGE`, `TAG_SUFFIX`, `DEFAULT`, `PACKAGES`, `GO_MIN`. +2. One `include` entry in each of `.woodpecker/{build,test,docker}.yaml`. + +`pre-commit.sh` fails if step 2 is forgotten, or if more than one distro claims `DEFAULT`. + +## Make + + make build # build every distro + make test # build the test stage of every distro (assertions run in-image) + make build-alma9 # one distro + make patch|minor|major diff --git a/distros/alma9/distro.env b/distros/alma9/distro.env new file mode 100644 index 0000000..c749a3f --- /dev/null +++ b/distros/alma9/distro.env @@ -0,0 +1,9 @@ +# alma9 — built on the container-base almalinux9 image +BASE_IMAGE_NAME="almalinux9" +IMAGE="gobuilder" +TAG_SUFFIX="alma9" +# The default distro also gets the unsuffixed : and :latest tags. +DEFAULT="true" + +PACKAGES="golang gcc nfpm" +GO_MIN="1.24" diff --git a/pre-commit.sh b/pre-commit.sh new file mode 100755 index 0000000..88c47cb --- /dev/null +++ b/pre-commit.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Guards the one mistake this layout invites: a distro added under distros/ but left +# out of a .woodpecker matrix, so it silently never gets built. +set -uo pipefail + +REQUIRED_KEYS="BASE_IMAGE_NAME IMAGE TAG_SUFFIX DEFAULT PACKAGES GO_MIN" +WORKFLOWS=".woodpecker/build.yaml .woodpecker/test.yaml .woodpecker/docker.yaml" + +fail=0 +for d in distros/*/; do + n=${d#distros/}; n=${n%/} + for k in $REQUIRED_KEYS; do + grep -q "^${k}=" "${d}distro.env" || { echo "${d}distro.env: missing ${k}"; fail=1; } + done + for wf in $WORKFLOWS; do + grep -q "DISTRO: ${n}\$" "$wf" || { echo "${wf}: distro ${n} missing from matrix"; fail=1; } + done +done + +# Exactly one distro owns the unsuffixed : and :latest tags. +n=$(grep -l 'DEFAULT="true"' distros/*/distro.env | wc -l) +[ "$n" -eq 1 ] || { echo "expected exactly one distro with DEFAULT=\"true\", found ${n}"; fail=1; } + +for s in ./*.sh; do bash -n "$s" || fail=1; done + +[ "$fail" -eq 0 ] || { echo "FAILED"; exit 1; } +echo "PASS" diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..e04da28 --- /dev/null +++ b/test.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# In-image assertions for the gobuilder image. Run via `docker build --target test`. +set -uo pipefail + +ARTIFACTAPI=https://artifactapi.k8s.syd1.au.unkin.net +. ./distro.env + +fail=0 +check() { + local name=$1; shift + if out=$("$@" 2>&1); then + echo "ok $name" + else + echo "NOT OK $name" + printf '%s\n' "$out" | sed 's/^/ /' + fail=1 + fi +} + +# Inherited from container-base: no -k and no --cacert. +check "internal CA trusted (artifactapi)" \ + curl -fsS -o /dev/null "$ARTIFACTAPI/api/v1/local/rpm-vendor-el9/repodata/repomd.xml" + +for p in $PACKAGES; do + check "package $p installed" rpm -q "$p" +done + +# GOCACHEPROG is only enabled by default from Go 1.24, so the toolchain floor matters. +go_min_ok() { + local have + have=$(go env GOVERSION 2>/dev/null | awk '{sub(/^go/,"",$1); print $1}') || return 1 + [ "$(printf '%s\n%s\n' "$GO_MIN" "$have" | sort -V | head -1)" = "$GO_MIN" ] +} +check "go >= $GO_MIN (have $(go env GOVERSION 2>/dev/null | awk '{print $1}'))" go_min_ok + +# gcc is what `go test -race` needs. +check "gcc runs" gcc --version +check "nfpm runs" nfpm --version +check "go-cache-plugin on PATH" command -v go-cache-plugin +check "go-cache-plugin executable" test -x /usr/local/bin/go-cache-plugin +# `version` prints correctly but exits 2, so assert on its output. +check "go-cache-plugin runs" bash -c 'go-cache-plugin version 2>&1 | grep -q "^go-cache-plugin version v"' + +# cgo must actually link, which is the thing -race depends on. +check "cgo builds" bash -c 'cd "$(mktemp -d)" && go mod init t >/dev/null && + printf "package main\n\nimport \"C\"\n\nfunc main() {}\n" > m.go && CGO_ENABLED=1 go build ./...' + +[ "$fail" -eq 0 ] || { echo "FAILED"; exit 1; } +echo "PASS" From c90a0e1fe39b4c6b3826f2d01b4faedb031b5151 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Sun, 27 Sep 2026 18:07:07 +1000 Subject: [PATCH 2/5] Take the estate repo name from distro.env Keeps the test assertions distro-agnostic so a non-el9 matrix entry needs no script edit. --- distros/alma9/distro.env | 3 +++ pre-commit.sh | 2 +- test.sh | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/distros/alma9/distro.env b/distros/alma9/distro.env index c749a3f..62fd458 100644 --- a/distros/alma9/distro.env +++ b/distros/alma9/distro.env @@ -5,5 +5,8 @@ TAG_SUFFIX="alma9" # The default distro also gets the unsuffixed : and :latest tags. DEFAULT="true" +# Estate local repo used to prove inherited HTTPS trust. +ESTATE_REPO="rpm-vendor-el9" + PACKAGES="golang gcc nfpm" GO_MIN="1.24" diff --git a/pre-commit.sh b/pre-commit.sh index 88c47cb..937de53 100755 --- a/pre-commit.sh +++ b/pre-commit.sh @@ -3,7 +3,7 @@ # out of a .woodpecker matrix, so it silently never gets built. set -uo pipefail -REQUIRED_KEYS="BASE_IMAGE_NAME IMAGE TAG_SUFFIX DEFAULT PACKAGES GO_MIN" +REQUIRED_KEYS="BASE_IMAGE_NAME IMAGE TAG_SUFFIX DEFAULT ESTATE_REPO PACKAGES GO_MIN" WORKFLOWS=".woodpecker/build.yaml .woodpecker/test.yaml .woodpecker/docker.yaml" fail=0 diff --git a/test.sh b/test.sh index e04da28..0d5273d 100755 --- a/test.sh +++ b/test.sh @@ -19,7 +19,7 @@ check() { # Inherited from container-base: no -k and no --cacert. check "internal CA trusted (artifactapi)" \ - curl -fsS -o /dev/null "$ARTIFACTAPI/api/v1/local/rpm-vendor-el9/repodata/repomd.xml" + curl -fsS -o /dev/null "$ARTIFACTAPI/api/v1/local/$ESTATE_REPO/repodata/repomd.xml" for p in $PACKAGES; do check "package $p installed" rpm -q "$p" From f755ae30b9e877daeca6cf7d290bc395de23b2a3 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Sun, 27 Sep 2026 18:37:23 +1000 Subject: [PATCH 3/5] Drop the syntax directive It pulled a frontend image from Docker Hub for features this Dockerfile does not use. --- Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 303bb71..b160ba6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,3 @@ -# syntax=docker/dockerfile:1 ARG REGISTRY=artifactapi.k8s.syd1.au.unkin.net/docker-internal # The container-base release this image is built on — the one place to bump it. ARG BASE_VERSION=0.1.0 From 06b159694cbba9f06d82ea376b918387d21f770a Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Sun, 27 Sep 2026 20:17:06 +1000 Subject: [PATCH 4/5] Replace pre-commit.sh with the pre-commit framework and drop the test harness The hand-rolled pre-commit.sh reimplemented what upstream hooks already do. Move to pre-commit.com with the estate's usual pre-commit-hooks set plus hadolint and shellcheck, and keep the one check with no upstream equivalent (distros/ vs the Woodpecker matrices) as a repo: local hook. The test harness goes; tests will be reintroduced differently. - add .pre-commit-config.yaml and ci/check-distros.sh - delete pre-commit.sh, test.sh and .woodpecker/test.yaml - run uvx pre-commit run --all-files from .woodpecker/pre-commit.yaml and make pre-commit - drop the Dockerfile test stage and the make test targets --- .pre-commit-config.yaml | 30 +++++++++++++++++++++++ .woodpecker/pre-commit.yaml | 4 +-- .woodpecker/test.yaml | 36 --------------------------- Dockerfile | 13 +++------- Makefile | 11 +++------ README.md | 7 +++--- ci/check-distros.sh | 24 ++++++++++++++++++ pre-commit.sh | 27 -------------------- test.sh | 49 ------------------------------------- 9 files changed, 68 insertions(+), 133 deletions(-) create mode 100644 .pre-commit-config.yaml delete mode 100644 .woodpecker/test.yaml create mode 100755 ci/check-distros.sh delete mode 100755 pre-commit.sh delete mode 100755 test.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..fc38784 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,30 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-merge-conflict + - id: mixed-line-ending + args: [--fix=lf] + + - repo: https://github.com/AleksaC/hadolint-py + rev: v2.15.1 + hooks: + - id: hadolint + + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: v0.11.0.1 + hooks: + - id: shellcheck + + # No upstream equivalent: a distro added under distros/ but left out of a + # Woodpecker matrix silently never gets built. + - repo: local + hooks: + - id: check-distros + name: distros match the woodpecker matrices + entry: ci/check-distros.sh + language: system + pass_filenames: false diff --git a/.woodpecker/pre-commit.yaml b/.woodpecker/pre-commit.yaml index b6239c8..e4446ad 100644 --- a/.woodpecker/pre-commit.yaml +++ b/.woodpecker/pre-commit.yaml @@ -3,9 +3,9 @@ when: steps: - name: pre-commit - image: "docker.io/almalinux:9" + image: "git.unkin.net/unkin/almalinux9-gobuilder:20260606" commands: - - ./pre-commit.sh + - uvx pre-commit run --all-files backend_options: kubernetes: serviceAccountName: default diff --git a/.woodpecker/test.yaml b/.woodpecker/test.yaml deleted file mode 100644 index 152ab47..0000000 --- a/.woodpecker/test.yaml +++ /dev/null @@ -1,36 +0,0 @@ -when: - - event: pull_request - -matrix: - include: - # One entry per distro. Adding a distro: add distros// and one entry here. - - DISTRO: alma9 - BASE_IMAGE_NAME: almalinux9 - TAG_SUFFIX: alma9 - DEFAULT: "true" - -steps: - # The test stage runs test.sh inside the freshly built image, so a failed assertion - # fails the build. No docker daemon required. - - name: test - image: "artifactapi.k8s.syd1.au.unkin.net/docker-internal/plugin-docker-buildx:latest" - settings: - repo: "artifactapi.k8s.syd1.au.unkin.net/docker-internal/gobuilder" - target: test - dry_run: true - build_args: - BASE_IMAGE_NAME: "${BASE_IMAGE_NAME}" - DISTRO: "${DISTRO}" - buildkit_config: | - [registry."artifactapi.k8s.syd1.au.unkin.net"] - ca = ["/etc/docker/certs.d/artifactapi.k8s.syd1.au.unkin.net/ca.crt"] - backend_options: - kubernetes: - serviceAccountName: default - resources: - requests: - memory: 1Gi - cpu: 1 - limits: - memory: 4Gi - cpu: 2 diff --git a/Dockerfile b/Dockerfile index b160ba6..d03f216 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,10 @@ ARG DISTRO=alma9 ARG GO_CACHE_PLUGIN_VERSION=v0.1.0 ARG GO_CACHE_PLUGIN_SHA256=0eb23d06a0dcf395559cb6447864579c220e60148fdcd6a6f841c2b17169b9b8 COPY distros/${DISTRO}/ /tmp/distro/ +# DL3041: versions are pinned by the estate dnf repos, not here. SC2086: $PACKAGES is a +# word list on purpose. DL4006: the only pipe ends in sha256sum, whose status is the +# pipeline's, and set -e catches it. +# hadolint ignore=DL3041,DL4006,SC2086 RUN set -eux; \ . /tmp/distro/distro.env; \ dnf install -y $PACKAGES; \ @@ -18,12 +22,3 @@ RUN set -eux; \ chmod 0755 /usr/local/bin/go-cache-plugin; \ rm -rf /var/cache/dnf /tmp/distro CMD ["/bin/bash"] - -# Built only via `--target test`; assertions run as a build step so a broken image -# fails the build, with no docker daemon needed in CI. -FROM image AS test -ARG DISTRO=alma9 -COPY distros/${DISTRO}/distro.env test.sh /tmp/test/ -RUN cd /tmp/test && ./test.sh - -FROM image AS final diff --git a/Makefile b/Makefile index 6a76311..e9577ff 100644 --- a/Makefile +++ b/Makefile @@ -2,11 +2,13 @@ REGISTRY := artifactapi.k8s.syd1.au.unkin.net/docker-internal DISTROS := $(notdir $(wildcard distros/*)) VERSION ?= $(patsubst v%,%,$(or $(shell git describe --tags --exact-match 2>/dev/null),0.1.0-dev)) -.PHONY: build test clean patch minor major +.PHONY: build clean pre-commit patch minor major build: $(addprefix build-,$(DISTROS)) -test: $(addprefix test-,$(DISTROS)) clean: $(addprefix clean-,$(DISTROS)) +pre-commit: + @uvx pre-commit run --all-files + # Not .PHONY: GNU make skips implicit-rule search for phony targets. build-%: @@ -14,11 +16,6 @@ build-%: docker build --build-arg "BASE_IMAGE_NAME=$$BASE_IMAGE_NAME" --build-arg "DISTRO=$*" \ -t "$(REGISTRY)/$$IMAGE:$(VERSION)-$$TAG_SUFFIX" . -test-%: - @set -eu; . ./distros/$*/distro.env; \ - docker build --target test --progress plain \ - --build-arg "BASE_IMAGE_NAME=$$BASE_IMAGE_NAME" --build-arg "DISTRO=$*" . - clean-%: @set -eu; . ./distros/$*/distro.env; \ docker rmi "$(REGISTRY)/$$IMAGE:$(VERSION)-$$TAG_SUFFIX" 2>/dev/null || true diff --git a/README.md b/README.md index f74a4d1..1f0aaee 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,14 @@ The `container-base` release this image builds on is `BASE_VERSION` in the `Dock ## Adding a distro 1. `distros//distro.env` — `BASE_IMAGE_NAME`, `IMAGE`, `TAG_SUFFIX`, `DEFAULT`, `PACKAGES`, `GO_MIN`. -2. One `include` entry in each of `.woodpecker/{build,test,docker}.yaml`. +2. One `include` entry in each of `.woodpecker/{build,docker}.yaml`. -`pre-commit.sh` fails if step 2 is forgotten, or if more than one distro claims `DEFAULT`. +The `check-distros` pre-commit hook fails if step 1 or 2 is incomplete, or if more than +one distro claims `DEFAULT`. ## Make make build # build every distro - make test # build the test stage of every distro (assertions run in-image) + make pre-commit # uvx pre-commit run --all-files make build-alma9 # one distro make patch|minor|major diff --git a/ci/check-distros.sh b/ci/check-distros.sh new file mode 100755 index 0000000..44f8e66 --- /dev/null +++ b/ci/check-distros.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Every distros// needs a complete distro.env and an entry in every Woodpecker +# matrix, or a newly added distro silently never gets built. +set -uo pipefail + +KEYS="BASE_IMAGE_NAME IMAGE TAG_SUFFIX DEFAULT ESTATE_REPO PACKAGES GO_MIN" +WORKFLOWS=(.woodpecker/build.yaml .woodpecker/docker.yaml) + +fail=0 +for d in distros/*/; do + n=$(basename "$d") + for k in $KEYS; do + grep -q "^${k}=" "${d}distro.env" || { echo "${d}distro.env: missing ${k}"; fail=1; } + done + for wf in "${WORKFLOWS[@]}"; do + grep -q "DISTRO: ${n}\$" "$wf" || { echo "${wf}: distro ${n} missing from matrix"; fail=1; } + done +done + +# Exactly one distro owns the unsuffixed : and :latest tags. +defaults=$(grep -l 'DEFAULT="true"' distros/*/distro.env | wc -l) +[ "$defaults" -eq 1 ] || { echo "expected exactly one distro with DEFAULT=\"true\", found ${defaults}"; fail=1; } + +exit "$fail" diff --git a/pre-commit.sh b/pre-commit.sh deleted file mode 100755 index 937de53..0000000 --- a/pre-commit.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash -# Guards the one mistake this layout invites: a distro added under distros/ but left -# out of a .woodpecker matrix, so it silently never gets built. -set -uo pipefail - -REQUIRED_KEYS="BASE_IMAGE_NAME IMAGE TAG_SUFFIX DEFAULT ESTATE_REPO PACKAGES GO_MIN" -WORKFLOWS=".woodpecker/build.yaml .woodpecker/test.yaml .woodpecker/docker.yaml" - -fail=0 -for d in distros/*/; do - n=${d#distros/}; n=${n%/} - for k in $REQUIRED_KEYS; do - grep -q "^${k}=" "${d}distro.env" || { echo "${d}distro.env: missing ${k}"; fail=1; } - done - for wf in $WORKFLOWS; do - grep -q "DISTRO: ${n}\$" "$wf" || { echo "${wf}: distro ${n} missing from matrix"; fail=1; } - done -done - -# Exactly one distro owns the unsuffixed : and :latest tags. -n=$(grep -l 'DEFAULT="true"' distros/*/distro.env | wc -l) -[ "$n" -eq 1 ] || { echo "expected exactly one distro with DEFAULT=\"true\", found ${n}"; fail=1; } - -for s in ./*.sh; do bash -n "$s" || fail=1; done - -[ "$fail" -eq 0 ] || { echo "FAILED"; exit 1; } -echo "PASS" diff --git a/test.sh b/test.sh deleted file mode 100755 index 0d5273d..0000000 --- a/test.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash -# In-image assertions for the gobuilder image. Run via `docker build --target test`. -set -uo pipefail - -ARTIFACTAPI=https://artifactapi.k8s.syd1.au.unkin.net -. ./distro.env - -fail=0 -check() { - local name=$1; shift - if out=$("$@" 2>&1); then - echo "ok $name" - else - echo "NOT OK $name" - printf '%s\n' "$out" | sed 's/^/ /' - fail=1 - fi -} - -# Inherited from container-base: no -k and no --cacert. -check "internal CA trusted (artifactapi)" \ - curl -fsS -o /dev/null "$ARTIFACTAPI/api/v1/local/$ESTATE_REPO/repodata/repomd.xml" - -for p in $PACKAGES; do - check "package $p installed" rpm -q "$p" -done - -# GOCACHEPROG is only enabled by default from Go 1.24, so the toolchain floor matters. -go_min_ok() { - local have - have=$(go env GOVERSION 2>/dev/null | awk '{sub(/^go/,"",$1); print $1}') || return 1 - [ "$(printf '%s\n%s\n' "$GO_MIN" "$have" | sort -V | head -1)" = "$GO_MIN" ] -} -check "go >= $GO_MIN (have $(go env GOVERSION 2>/dev/null | awk '{print $1}'))" go_min_ok - -# gcc is what `go test -race` needs. -check "gcc runs" gcc --version -check "nfpm runs" nfpm --version -check "go-cache-plugin on PATH" command -v go-cache-plugin -check "go-cache-plugin executable" test -x /usr/local/bin/go-cache-plugin -# `version` prints correctly but exits 2, so assert on its output. -check "go-cache-plugin runs" bash -c 'go-cache-plugin version 2>&1 | grep -q "^go-cache-plugin version v"' - -# cgo must actually link, which is the thing -race depends on. -check "cgo builds" bash -c 'cd "$(mktemp -d)" && go mod init t >/dev/null && - printf "package main\n\nimport \"C\"\n\nfunc main() {}\n" > m.go && CGO_ENABLED=1 go build ./...' - -[ "$fail" -eq 0 ] || { echo "FAILED"; exit 1; } -echo "PASS" From 83beaff2db19bebe718da8483c8f94fffc87042c Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Sun, 27 Sep 2026 22:19:24 +1000 Subject: [PATCH 5/5] Get the internal CA into the buildkit container via registry The buildkit_config setting does not exist in the plugin-docker-buildx build we run: the binary has no PLUGIN_BUILDKIT_CONFIG, so the block was dead YAML and `docker buildx create` ran without --config, leaving the spawned buildkit container with no internal CA. The plugin only writes a buildkit.toml (pointing at /etc/docker/certs.d//ca.crt) when `registry` is set. Set registry on the build step, drop the dead block from every step, and fail ci/check-distros.sh when a matrix value restating a distro.env key drifts from it. --- .woodpecker/build.yaml | 7 ++++--- .woodpecker/docker.yaml | 9 +++------ ci/check-distros.sh | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml index a6da167..3946f8e 100644 --- a/.woodpecker/build.yaml +++ b/.woodpecker/build.yaml @@ -13,15 +13,16 @@ steps: - name: build image: "artifactapi.k8s.syd1.au.unkin.net/docker-internal/plugin-docker-buildx:latest" settings: + # registry is what gets the internal CA into the buildkit container: the plugin + # writes a buildkit.toml pointing at /etc/docker/certs.d//ca.crt only + # when it is set. The plugin has no buildkit_config setting. + registry: artifactapi.k8s.syd1.au.unkin.net repo: "artifactapi.k8s.syd1.au.unkin.net/docker-internal/gobuilder" target: image dry_run: true build_args: BASE_IMAGE_NAME: "${BASE_IMAGE_NAME}" DISTRO: "${DISTRO}" - buildkit_config: | - [registry."artifactapi.k8s.syd1.au.unkin.net"] - ca = ["/etc/docker/certs.d/artifactapi.k8s.syd1.au.unkin.net/ca.crt"] backend_options: kubernetes: serviceAccountName: default diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index b42a902..12b4a86 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -14,15 +14,15 @@ steps: - name: docker image: "artifactapi.k8s.syd1.au.unkin.net/docker-internal/plugin-docker-buildx:latest" settings: + # registry is what gets the internal CA into the buildkit container: the plugin + # writes a buildkit.toml pointing at /etc/docker/certs.d//ca.crt only + # when it is set. The plugin has no buildkit_config setting. registry: artifactapi.k8s.syd1.au.unkin.net repo: "artifactapi.k8s.syd1.au.unkin.net/docker-internal/gobuilder" target: image build_args: BASE_IMAGE_NAME: "${BASE_IMAGE_NAME}" DISTRO: "${DISTRO}" - buildkit_config: | - [registry."artifactapi.k8s.syd1.au.unkin.net"] - ca = ["/etc/docker/certs.d/artifactapi.k8s.syd1.au.unkin.net/ca.crt"] tags: - "${CI_COMMIT_TAG##v}-${TAG_SUFFIX}" backend_options: @@ -48,9 +48,6 @@ steps: build_args: BASE_IMAGE_NAME: "${BASE_IMAGE_NAME}" DISTRO: "${DISTRO}" - buildkit_config: | - [registry."artifactapi.k8s.syd1.au.unkin.net"] - ca = ["/etc/docker/certs.d/artifactapi.k8s.syd1.au.unkin.net/ca.crt"] tags: - "${CI_COMMIT_TAG##v}" - latest diff --git a/ci/check-distros.sh b/ci/check-distros.sh index 44f8e66..6948081 100755 --- a/ci/check-distros.sh +++ b/ci/check-distros.sh @@ -14,6 +14,25 @@ for d in distros/*/; do done for wf in "${WORKFLOWS[@]}"; do grep -q "DISTRO: ${n}\$" "$wf" || { echo "${wf}: distro ${n} missing from matrix"; fail=1; } + + # A matrix entry that restates a distro.env key must not drift from it — a stale + # BASE_IMAGE here silently overrides distro.env for every CI build. + while IFS='=' read -r k v; do + [ -n "$k" ] || continue + grep -q "^${k}=" "${d}distro.env" || continue + ev=$(sed -n "s/^${k}=//p" "${d}distro.env" | head -1 | sed 's/^"//;s/"$//') + [ "$v" = "$ev" ] || { echo "${wf}: matrix ${k}=\"${v}\" for ${n} disagrees with ${d}distro.env (\"${ev}\")"; fail=1; } + done <