From 970cfad2ee0b734f477317758b1a876a5cd8e2de Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Sun, 27 Sep 2026 17:47:22 +1000 Subject: [PATCH 1/4] Add matrixed container base image build Replace the packer-images almalinux base with a Dockerfile matrix over distros/, publishing one image per distro to docker-internal on a single repo semver. - bootstrap the internal CA from a sha256-pinned rpm fetched over plain HTTP - install the estate dnf repo configuration pointed at artifactapi - 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 | 33 +++++++++++++++++++ .woodpecker/docker.yaml | 37 +++++++++++++++++++++ .woodpecker/pre-commit.yaml | 18 ++++++++++ .woodpecker/test.yaml | 35 ++++++++++++++++++++ Dockerfile | 28 ++++++++++++++++ Makefile | 42 ++++++++++++++++++++++++ README.md | 34 ++++++++++++++++++- distros/alma9/distro.env | 12 +++++++ distros/alma9/yum.repos.d/almalinux.repo | 27 +++++++++++++++ distros/alma9/yum.repos.d/epel.repo | 6 ++++ distros/alma9/yum.repos.d/unkin.repo | 27 +++++++++++++++ pre-commit.sh | 23 +++++++++++++ test.sh | 40 ++++++++++++++++++++++ 13 files changed, 361 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 100644 distros/alma9/yum.repos.d/almalinux.repo create mode 100644 distros/alma9/yum.repos.d/epel.repo create mode 100644 distros/alma9/yum.repos.d/unkin.repo 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..9910e1a --- /dev/null +++ b/.woodpecker/build.yaml @@ -0,0 +1,33 @@ +when: + - event: pull_request + +matrix: + include: + # One entry per distro. Adding a distro: add distros// and one entry here. + - DISTRO: alma9 + IMAGE: almalinux9 + BASE_IMAGE: "docker.io/almalinux:9" + +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/${IMAGE}" + target: image + dry_run: true + build_args: + BASE_IMAGE: "${BASE_IMAGE}" + 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..78d8274 --- /dev/null +++ b/.woodpecker/docker.yaml @@ -0,0 +1,37 @@ +when: + - event: tag + ref: refs/tags/v* + +matrix: + include: + # One entry per distro. Adding a distro: add distros// and one entry here. + - DISTRO: alma9 + IMAGE: almalinux9 + BASE_IMAGE: "docker.io/almalinux:9" + +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/${IMAGE}" + target: image + build_args: + BASE_IMAGE: "${BASE_IMAGE}" + 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..557fcf7 --- /dev/null +++ b/.woodpecker/test.yaml @@ -0,0 +1,35 @@ +when: + - event: pull_request + +matrix: + include: + # One entry per distro. Adding a distro: add distros// and one entry here. + - DISTRO: alma9 + IMAGE: almalinux9 + BASE_IMAGE: "docker.io/almalinux:9" + +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/${IMAGE}" + target: test + dry_run: true + build_args: + BASE_IMAGE: "${BASE_IMAGE}" + 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..91cbb94 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# syntax=docker/dockerfile:1 +ARG BASE_IMAGE=docker.io/almalinux:9 + +FROM ${BASE_IMAGE} AS image +ARG DISTRO=alma9 +COPY distros/${DISTRO}/ /tmp/distro/ +RUN set -eux; \ + . /tmp/distro/distro.env; \ + rm -f /etc/yum.repos.d/*.repo; \ + curl -fsSL -o /tmp/ca.rpm "$CA_RPM_URL"; \ + echo "$CA_RPM_SHA256 /tmp/ca.rpm" | sha256sum -c -; \ + rpm -i /tmp/ca.rpm; \ + cp /tmp/distro/yum.repos.d/*.repo /etc/yum.repos.d/; \ + dnf makecache; \ + dnf update -y; \ + dnf install -y $PACKAGES; \ + dnf clean all; \ + rm -rf /var/cache/dnf /tmp/ca.rpm /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..d8e91ac --- /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=$$BASE_IMAGE" --build-arg "DISTRO=$*" \ + -t "$(REGISTRY)/$$IMAGE:$(VERSION)" . + +test-%: + @set -eu; . ./distros/$*/distro.env; \ + docker build --target test --progress plain \ + --build-arg "BASE_IMAGE=$$BASE_IMAGE" --build-arg "DISTRO=$*" . + +clean-%: + @set -eu; . ./distros/$*/distro.env; \ + docker rmi "$(REGISTRY)/$$IMAGE:$(VERSION)" 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 dfa88a8..0294588 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,35 @@ # container-base -Base container images for the estate, carrying internal CA trust and dnf/repo configuration, built as a matrix across the supported distros. \ No newline at end of file +The estate's base container images, one per distro, replacing what `packer-images` +built. Published as `artifactapi.k8s.syd1.au.unkin.net/docker-internal/:` +— the image name is the distro (`almalinux9`), the repo's single semver is the tag. + +| distro | image | from | +| --- | --- | --- | +| `alma9` | `almalinux9` | `docker.io/almalinux:9` | + +Each image bootstraps the internal CA, installs the estate dnf repository +configuration pointed at artifactapi, and installs the base package set. + +## Adding a distro + +1. `distros//distro.env` — `BASE_IMAGE`, `IMAGE`, `CA_RPM_URL`, `CA_RPM_SHA256`, `PACKAGES`. +2. `distros//yum.repos.d/*.repo` — repository configuration for that distro. +3. One `include` entry in each of `.woodpecker/{build,test,docker}.yaml`. + +`pre-commit.sh` fails if step 3 is forgotten. + +## CA bootstrap + +`unkin-ca-certificates` lives in artifactapi, but the image cannot trust artifactapi +over HTTPS until it is installed. The package is therefore fetched over plain HTTP and +checked against `CA_RPM_SHA256` before `rpm -i`; everything after that uses HTTPS. The +pinned hash is the `pkgid` checksum recorded for the file in the repo's own repodata +(`repodata/repomd.xml` → `*-primary.xml.gz`). + +## 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..f835c18 --- /dev/null +++ b/distros/alma9/distro.env @@ -0,0 +1,12 @@ +# alma9 — AlmaLinux 9 +BASE_IMAGE="docker.io/almalinux:9" +IMAGE="almalinux9" + +# Bootstrap of the internal CA. The image cannot trust artifactapi over HTTPS until +# this package is installed, so it is fetched over plain HTTP and verified against a +# pinned sha256 before rpm -i. The hash is the pkgid checksum recorded for this file +# in the rpm-vendor-el9 repodata (repodata/repomd.xml -> *-primary.xml.gz). +CA_RPM_URL="http://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-vendor-el9/Packages/unkin-ca-certificates-2025.7.13-1.el9.x86_64.rpm" +CA_RPM_SHA256="c12fb81ea639a92d19506e07e88df290635b15e175b1f8c063d4e8a2d86bab6e" + +PACKAGES="git jq make python3.11 python3.11-pip python3.12 python3.12-pip uv wget" diff --git a/distros/alma9/yum.repos.d/almalinux.repo b/distros/alma9/yum.repos.d/almalinux.repo new file mode 100644 index 0000000..d25e3e1 --- /dev/null +++ b/distros/alma9/yum.repos.d/almalinux.repo @@ -0,0 +1,27 @@ +[baseos] +name=baseos repository +baseurl=https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/almalinux/$releasever/BaseOS/$basearch/os/ +gpgkey=https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/almalinux/$releasever/BaseOS/$basearch/os/RPM-GPG-KEY-AlmaLinux-9 +enabled=1 +gpgcheck=1 + +[appstream] +name=appstream repository +baseurl=https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/almalinux/$releasever/AppStream/$basearch/os/ +gpgkey=https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/almalinux/$releasever/AppStream/$basearch/os/RPM-GPG-KEY-AlmaLinux-9 +enabled=1 +gpgcheck=1 + +[extras] +name=extras repository +baseurl=https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/almalinux/$releasever/extras/$basearch/os/ +gpgkey=https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/almalinux/$releasever/extras/$basearch/os/RPM-GPG-KEY-AlmaLinux-9 +enabled=1 +gpgcheck=1 + +[crb] +name=crb repository +baseurl=https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/almalinux/$releasever/CRB/$basearch/os/ +gpgkey=https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/almalinux/$releasever/CRB/$basearch/os/RPM-GPG-KEY-AlmaLinux-9 +enabled=1 +gpgcheck=1 diff --git a/distros/alma9/yum.repos.d/epel.repo b/distros/alma9/yum.repos.d/epel.repo new file mode 100644 index 0000000..7a49c70 --- /dev/null +++ b/distros/alma9/yum.repos.d/epel.repo @@ -0,0 +1,6 @@ +[epel] +name=epel repository +baseurl=https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/epel/$releasever/Everything/$basearch +gpgkey=https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/epel/RPM-GPG-KEY-EPEL-$releasever +enabled=1 +gpgcheck=1 diff --git a/distros/alma9/yum.repos.d/unkin.repo b/distros/alma9/yum.repos.d/unkin.repo new file mode 100644 index 0000000..337159e --- /dev/null +++ b/distros/alma9/yum.repos.d/unkin.repo @@ -0,0 +1,27 @@ +[rpm-internal-el9] +name=rpm-internal-el9 repository +baseurl=https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-internal-el9/ +enabled=1 +gpgcheck=0 +metadata_expire=60 + +[rpm-vendor-el9] +name=rpm-vendor-el9 repository +baseurl=https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-vendor-el9/ +enabled=1 +gpgcheck=0 +metadata_expire=60 + +[rpm-internal] +name=rpm-internal repository +baseurl=https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-internal/ +enabled=1 +gpgcheck=0 +metadata_expire=60 + +[rpm-vendor] +name=rpm-vendor repository +baseurl=https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-vendor/ +enabled=1 +gpgcheck=0 +metadata_expire=60 diff --git a/pre-commit.sh b/pre-commit.sh new file mode 100755 index 0000000..3b71437 --- /dev/null +++ b/pre-commit.sh @@ -0,0 +1,23 @@ +#!/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 IMAGE CA_RPM_URL CA_RPM_SHA256 PACKAGES" +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 + +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..8a8c118 --- /dev/null +++ b/test.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# In-image assertions for the base 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 +} + +# No -k and no --cacert: fails unless the bootstrapped internal CA is in the trust store. +check "internal CA trusted (artifactapi)" \ + curl -fsS -o /dev/null "$ARTIFACTAPI/api/v1/local/rpm-vendor-el9/repodata/repomd.xml" +check "unkin-ca-certificates installed" rpm -q unkin-ca-certificates + +# Every configured repo must refresh, and a package that exists only in the estate's +# local repos must resolve. +check "dnf refreshes every estate repo" dnf -q makecache --refresh +check "dnf resolves an estate-only package" \ + dnf -q --disablerepo='*' --enablerepo='rpm-vendor*' list --available nfpm + +for p in $PACKAGES; do + check "package $p installed" rpm -q "$p" +done + +for c in git jq make wget uv python3.11 python3.12; do + check "$c runs" "$c" --version +done + +[ "$fail" -eq 0 ] || { echo "FAILED"; exit 1; } +echo "PASS" From b3f46a715a82da5c850e7b129578519c4036413e Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Sun, 27 Sep 2026 18:07:05 +1000 Subject: [PATCH 2/4] 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 | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/distros/alma9/distro.env b/distros/alma9/distro.env index f835c18..877acde 100644 --- a/distros/alma9/distro.env +++ b/distros/alma9/distro.env @@ -9,4 +9,7 @@ IMAGE="almalinux9" CA_RPM_URL="http://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-vendor-el9/Packages/unkin-ca-certificates-2025.7.13-1.el9.x86_64.rpm" CA_RPM_SHA256="c12fb81ea639a92d19506e07e88df290635b15e175b1f8c063d4e8a2d86bab6e" +# Estate local repo used to prove HTTPS trust and dnf reachability. +ESTATE_REPO="rpm-vendor-el9" + PACKAGES="git jq make python3.11 python3.11-pip python3.12 python3.12-pip uv wget" diff --git a/pre-commit.sh b/pre-commit.sh index 3b71437..e61f01a 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 IMAGE CA_RPM_URL CA_RPM_SHA256 PACKAGES" +REQUIRED_KEYS="BASE_IMAGE IMAGE CA_RPM_URL CA_RPM_SHA256 ESTATE_REPO PACKAGES" WORKFLOWS=".woodpecker/build.yaml .woodpecker/test.yaml .woodpecker/docker.yaml" fail=0 diff --git a/test.sh b/test.sh index 8a8c118..cc86ba5 100755 --- a/test.sh +++ b/test.sh @@ -19,14 +19,14 @@ check() { # No -k and no --cacert: fails unless the bootstrapped internal CA is in the trust store. 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" check "unkin-ca-certificates installed" rpm -q unkin-ca-certificates # Every configured repo must refresh, and a package that exists only in the estate's # local repos must resolve. check "dnf refreshes every estate repo" dnf -q makecache --refresh check "dnf resolves an estate-only package" \ - dnf -q --disablerepo='*' --enablerepo='rpm-vendor*' list --available nfpm + dnf -q --disablerepo='*' --enablerepo="$ESTATE_REPO" list --available nfpm for p in $PACKAGES; do check "package $p installed" rpm -q "$p" From 03ab698efa4cd0fc3f6da078bf0213adc27c9116 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Sun, 27 Sep 2026 18:37:18 +1000 Subject: [PATCH 3/4] Pull the base image through the artifactapi dockerhub remote A buildkit FROM does its own pull and ignores the nodes' containerd mirror, so every build was an uncached, rate-limited Docker Hub pull. - point BASE_IMAGE at dockerhub/library/almalinux:9 - drop the syntax directive, which pulled a frontend image from Docker Hub for features this Dockerfile does not use --- Dockerfile | 3 +-- README.md | 2 +- distros/alma9/distro.env | 5 ++++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 91cbb94..c5b83c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,4 @@ -# syntax=docker/dockerfile:1 -ARG BASE_IMAGE=docker.io/almalinux:9 +ARG BASE_IMAGE=artifactapi.k8s.syd1.au.unkin.net/dockerhub/library/almalinux:9 FROM ${BASE_IMAGE} AS image ARG DISTRO=alma9 diff --git a/README.md b/README.md index 0294588..4c81b4f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ built. Published as `artifactapi.k8s.syd1.au.unkin.net/docker-internal/:< | distro | image | from | | --- | --- | --- | -| `alma9` | `almalinux9` | `docker.io/almalinux:9` | +| `alma9` | `almalinux9` | `dockerhub/library/almalinux:9` | Each image bootstraps the internal CA, installs the estate dnf repository configuration pointed at artifactapi, and installs the base package set. diff --git a/distros/alma9/distro.env b/distros/alma9/distro.env index 877acde..ffe5b18 100644 --- a/distros/alma9/distro.env +++ b/distros/alma9/distro.env @@ -1,5 +1,8 @@ # alma9 — AlmaLinux 9 -BASE_IMAGE="docker.io/almalinux:9" +# Pulled through the artifactapi dockerhub remote: a buildkit FROM does its own pull +# and ignores the nodes' containerd mirror. The library/ prefix is required — the +# remote allowlists ^library/almalinux, so dockerhub/almalinux:9 returns 403. +BASE_IMAGE="artifactapi.k8s.syd1.au.unkin.net/dockerhub/library/almalinux:9" IMAGE="almalinux9" # Bootstrap of the internal CA. The image cannot trust artifactapi over HTTPS until From 63cf28c90e19618594b655aee8801c1de4500b15 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Sun, 27 Sep 2026 20:17:04 +1000 Subject: [PATCH 4/4] 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 | 35 -------------------------------- Dockerfile | 13 ++++-------- Makefile | 11 ++++------ README.md | 6 +++--- ci/check-distros.sh | 20 +++++++++++++++++++ pre-commit.sh | 23 --------------------- test.sh | 40 ------------------------------------- 9 files changed, 63 insertions(+), 119 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 557fcf7..0000000 --- a/.woodpecker/test.yaml +++ /dev/null @@ -1,35 +0,0 @@ -when: - - event: pull_request - -matrix: - include: - # One entry per distro. Adding a distro: add distros// and one entry here. - - DISTRO: alma9 - IMAGE: almalinux9 - BASE_IMAGE: "docker.io/almalinux:9" - -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/${IMAGE}" - target: test - dry_run: true - build_args: - BASE_IMAGE: "${BASE_IMAGE}" - 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 c5b83c4..65bf7de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,10 @@ ARG BASE_IMAGE=artifactapi.k8s.syd1.au.unkin.net/dockerhub/library/almalinux:9 FROM ${BASE_IMAGE} AS image ARG DISTRO=alma9 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; \ rm -f /etc/yum.repos.d/*.repo; \ @@ -16,12 +20,3 @@ RUN set -eux; \ dnf clean all; \ rm -rf /var/cache/dnf /tmp/ca.rpm /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 d8e91ac..e5f1c65 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=$$BASE_IMAGE" --build-arg "DISTRO=$*" \ -t "$(REGISTRY)/$$IMAGE:$(VERSION)" . -test-%: - @set -eu; . ./distros/$*/distro.env; \ - docker build --target test --progress plain \ - --build-arg "BASE_IMAGE=$$BASE_IMAGE" --build-arg "DISTRO=$*" . - clean-%: @set -eu; . ./distros/$*/distro.env; \ docker rmi "$(REGISTRY)/$$IMAGE:$(VERSION)" 2>/dev/null || true diff --git a/README.md b/README.md index 4c81b4f..1ba8650 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@ configuration pointed at artifactapi, and installs the base package set. 1. `distros//distro.env` — `BASE_IMAGE`, `IMAGE`, `CA_RPM_URL`, `CA_RPM_SHA256`, `PACKAGES`. 2. `distros//yum.repos.d/*.repo` — repository configuration for that distro. -3. One `include` entry in each of `.woodpecker/{build,test,docker}.yaml`. +3. One `include` entry in each of `.woodpecker/{build,docker}.yaml`. -`pre-commit.sh` fails if step 3 is forgotten. +The `check-distros` pre-commit hook fails if step 1 or 3 is incomplete. ## CA bootstrap @@ -30,6 +30,6 @@ pinned hash is the `pkgid` checksum recorded for the file in the repo's own repo ## 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..8ac5ccd --- /dev/null +++ b/ci/check-distros.sh @@ -0,0 +1,20 @@ +#!/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 IMAGE CA_RPM_URL CA_RPM_SHA256 ESTATE_REPO PACKAGES" +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 + +exit "$fail" diff --git a/pre-commit.sh b/pre-commit.sh deleted file mode 100755 index e61f01a..0000000 --- a/pre-commit.sh +++ /dev/null @@ -1,23 +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 IMAGE CA_RPM_URL CA_RPM_SHA256 ESTATE_REPO PACKAGES" -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 - -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 cc86ba5..0000000 --- a/test.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash -# In-image assertions for the base 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 -} - -# No -k and no --cacert: fails unless the bootstrapped internal CA is in the trust store. -check "internal CA trusted (artifactapi)" \ - curl -fsS -o /dev/null "$ARTIFACTAPI/api/v1/local/$ESTATE_REPO/repodata/repomd.xml" -check "unkin-ca-certificates installed" rpm -q unkin-ca-certificates - -# Every configured repo must refresh, and a package that exists only in the estate's -# local repos must resolve. -check "dnf refreshes every estate repo" dnf -q makecache --refresh -check "dnf resolves an estate-only package" \ - dnf -q --disablerepo='*' --enablerepo="$ESTATE_REPO" list --available nfpm - -for p in $PACKAGES; do - check "package $p installed" rpm -q "$p" -done - -for c in git jq make wget uv python3.11 python3.12; do - check "$c runs" "$c" --version -done - -[ "$fail" -eq 0 ] || { echo "FAILED"; exit 1; } -echo "PASS"