From a5a24a559a5a987317c290598c1f35c1d00ab4ae Mon Sep 17 00:00:00 2001 From: lczyk Date: Fri, 17 Jul 2026 09:51:36 +0200 Subject: [PATCH 1/2] feat: ubuntu 22.04 image --- .github/workflows/build-oci.yaml | 2 +- .github/workflows/push-ghcr.yaml | 4 +- images/Dockerfile.bread-22.04 | 37 ++++ images/Dockerfile.bread-chisel-releases-22.04 | 30 ++++ inlined/bread-22.04.yaml | 154 +++++++++++++++++ inlined/bread-chisel-releases-22.04.yaml | 160 ++++++++++++++++++ makefile | 2 +- templates/bread-22.04.yaml.in | 35 ++++ templates/bread-chisel-releases-22.04.yaml.in | 42 +++++ 9 files changed, 462 insertions(+), 4 deletions(-) create mode 100644 images/Dockerfile.bread-22.04 create mode 100644 images/Dockerfile.bread-chisel-releases-22.04 create mode 100644 inlined/bread-22.04.yaml create mode 100644 inlined/bread-chisel-releases-22.04.yaml create mode 100644 templates/bread-22.04.yaml.in create mode 100644 templates/bread-chisel-releases-22.04.yaml.in diff --git a/.github/workflows/build-oci.yaml b/.github/workflows/build-oci.yaml index 51a4967..6476b53 100644 --- a/.github/workflows/build-oci.yaml +++ b/.github/workflows/build-oci.yaml @@ -74,7 +74,7 @@ jobs: set -e mkdir -p images-out for flavour in bread bread-chisel-releases; do - for ver in 24.04 25.10 26.04 26.10; do + for ver in 22.04 24.04 25.10 26.04 26.10; do tag="$flavour:$ver-${{ matrix.arch }}" echo "==> saving $tag" docker save "$tag" -o "images-out/$flavour-$ver-${{ matrix.arch }}.tar" diff --git a/.github/workflows/push-ghcr.yaml b/.github/workflows/push-ghcr.yaml index ffb8474..623655c 100644 --- a/.github/workflows/push-ghcr.yaml +++ b/.github/workflows/push-ghcr.yaml @@ -45,7 +45,7 @@ jobs: run: | set -e for flavour in bread bread-chisel-releases; do - for ver in 24.04 25.10 26.04 26.10; do + for ver in 22.04 24.04 25.10 26.04 26.10; do m="$flavour-$ver-manifest" buildah manifest create "$m" for arch in amd64 arm64 s390x ppc64le; do @@ -71,7 +71,7 @@ jobs: run: | set -e for flavour in bread bread-chisel-releases; do - for ver in 24.04 25.10 26.04 26.10; do + for ver in 22.04 24.04 25.10 26.04 26.10; do cosign sign --yes \ "$REGISTRY/${{ github.repository }}/$flavour:$ver" done diff --git a/images/Dockerfile.bread-22.04 b/images/Dockerfile.bread-22.04 new file mode 100644 index 0000000..15cf957 --- /dev/null +++ b/images/Dockerfile.bread-22.04 @@ -0,0 +1,37 @@ +# Bread image: Ubuntu 22.04 + sshd only. + +FROM docker.io/library/ubuntu:22.04@sha256:0e0a0fc6d18feda9db1590da249ac93e8d5abfea8f4c3c0c849ce512b5ef8982 + +# Skip man/doc/info install for every subsequent package -- dpkg's man-db +# trigger is a major cost under qemu emulation (s390x / ppc64le). Persists in +# the image fs, so the bread-chisel-releases install inherits the exclusion. +RUN printf 'path-exclude /usr/share/man/*\npath-exclude /usr/share/doc/*\npath-exclude /usr/share/info/*\n' \ + > /etc/dpkg/dpkg.cfg.d/01-nodoc && \ + apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssh-server && \ + mkdir /var/run/sshd || true && \ + echo 'root:bread' | chpasswd && \ + echo "PermitRootLogin yes" >> /etc/ssh/sshd_config && \ + echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +# Replace /etc/pam.d/sshd with a pam_permit-only stack. These images are +# throwaway test containers; we don't care about password validity, and the +# default pam_unix stack pulls in unix_chkpwd which can fail to load +# libcrypt.so.1 inside privileged containers on some hosts. +RUN printf '%s\n' \ + 'auth required pam_permit.so' \ + 'account required pam_permit.so' \ + 'session required pam_permit.so' \ + 'password required pam_permit.so' \ + > /etc/pam.d/sshd + +# Insecure-image warning. Prints on interactive logins (ssh, `docker exec -it`). +# See hack/bread-warning.sh for design notes. +COPY hack/banner.txt /etc/bread-banner.txt +COPY hack/bread-warning.sh /etc/profile.d/00-bread-warning.sh +RUN chmod 0644 /etc/bread-banner.txt /etc/profile.d/00-bread-warning.sh && \ + printf '\n# bread-warning\n[ -r /etc/profile.d/00-bread-warning.sh ] && . /etc/profile.d/00-bread-warning.sh\n' >> /root/.bashrc + +EXPOSE 22 +CMD ["/usr/sbin/sshd", "-D"] diff --git a/images/Dockerfile.bread-chisel-releases-22.04 b/images/Dockerfile.bread-chisel-releases-22.04 new file mode 100644 index 0000000..c55d086 --- /dev/null +++ b/images/Dockerfile.bread-chisel-releases-22.04 @@ -0,0 +1,30 @@ +# Bread-chisel-releases image: bread + chisel + tooling commonly +# expected by chisel-releases spread tests. +# +# chisel is built from source (see hack/build_binaries.sh) and dropped +# into ./cache/binaries/chisel- by the binaries stamp rule before +# this image is built. + +ARG BASE_TAG=22.04-amd64 +ARG BUILD_ARCH=amd64 +FROM bread:${BASE_TAG} +ARG BUILD_ARCH + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + curl wget git jq file sudo tree \ + skopeo iproute2 && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +COPY cache/binaries/chisel-hacked-${BUILD_ARCH} /usr/local/bin/chisel-hacked +COPY cache/binaries/docker-${BUILD_ARCH} /usr/local/bin/docker +# NOTE: ship only the hacked chisel; `chisel` is a symlink to it so bare +# `chisel` invocations (e.g. the spread install-slices lib) pick up CHISEL_HACKS. +RUN chmod +x /usr/local/bin/chisel-hacked /usr/local/bin/docker && \ + ln -s chisel-hacked /usr/local/bin/chisel + +# Lazy-apt shim: auto-runs apt-get update on first non-update apt/apt-get call, +# then self-destructs so subsequent calls hit real apt directly. +# Needed because the image is built with apt lists wiped (apt-get clean above). +COPY hack/lazy-apt.sh /usr/local/bin/apt-get +RUN chmod +x /usr/local/bin/apt-get && cp /usr/local/bin/apt-get /usr/local/bin/apt diff --git a/inlined/bread-22.04.yaml b/inlined/bread-22.04.yaml new file mode 100644 index 0000000..c0b1fc9 --- /dev/null +++ b/inlined/bread-22.04.yaml @@ -0,0 +1,154 @@ +project: bread + +path: /bread + +environment: + PROJECT_PATH: /bread + SHARED_LIBRARIES: $PROJECT_PATH/tests/lib + PATH: $PATH:$SHARED_LIBRARIES + +exclude: + - .git + - .github + +backends: + docker: + type: adhoc + allocate: | + #spellchecker: ignore noninteractice + + set -e + + ver=$(echo "$SPREAD_SYSTEM" | cut -d- -f2) # e.g., ubuntu-24.04-amd64 -> 24.04 + arch=$(echo "$SPREAD_SYSTEM" | cut -d- -f3) # e.g., ubuntu-24.04-amd64 -> amd64 + echo "ver: $ver" + echo "arch: $arch" + + image="ghcr.io/lczyk/spread-bread/bread:$ver" + echo "image: $image" + + # Networking mode: how spread reaches the container's sshd. + # bridge -- connect to the container's docker bridge IP. Works where the host + # shares the docker bridge network (linux native), so 172.17.x.x is + # routable from the spread host. + # publish -- publish sshd to 127.0.0.1: and connect there. + # Needed where the bridge IP is not host-routable, e.g. Docker + # Desktop on macOS (docker runs in a VM). + # Default is per-OS; override with BREAD_NET=bridge|publish. + mode="${BREAD_NET:-}" + if [ -z "$mode" ]; then + case "$(uname -s)" in + Darwin) mode=publish ;; + *) mode=bridge ;; + esac + fi + case "$mode" in + bridge|publish) ;; + *) echo "BREAD_NET must be 'bridge' or 'publish', got: '$mode'" >&2; exit 1 ;; + esac + echo "net mode: $mode" + + if [ "$mode" = bridge ]; then + # Use a counter file to ensure unique container names. + # snippet thanks to @lengau + # https://github.com/canonical/charmcraft/blob/120a00a50f7ed3d0ae2fc2bea69e2e43b68b1594/spread.yaml#L72-L79 + sleep 0.$RANDOM # Minimize chances of a race condition + export counter_file=".spread-worker-num" + instance_num=$( + flock -x $counter_file bash -c ' + [ -s $counter_file ] || echo 0 > $counter_file + num=$(< $counter_file) && echo $num + echo $(( $num + 1 )) > $counter_file' + ) + container_name="bread-${ver}-${arch}-${instance_num}" + publish_flag="" + else + # publish mode: no shared counter file (no flock dependency); a pid+RANDOM + # suffix is unique enough for a name. Docker picks the free host port. + container_name="bread-${ver}-${arch}-$$-${RANDOM}" + publish_flag="-p 127.0.0.1::22" + fi + echo "container_name: $container_name" + + docker run \ + --rm \ + --platform "linux/$arch" \ + -e DEBIAN_FRONTEND=noninteractice \ + -e "usr=$SPREAD_SYSTEM_USERNAME" \ + -e "pass=$SPREAD_SYSTEM_PASSWORD" \ + $publish_flag \ + --name "$container_name" \ + -d "$image" + + until docker exec "$container_name" pgrep sshd; do sleep 1; done + + if [ "$mode" = publish ]; then + # The ephemeral host port docker mapped to the container's sshd. + port=$(docker port "$container_name" 22 | head -n1 | cut -d: -f2) + [ -n "$port" ] || { echo "could not find published sshd port for $container_name" >&2; exit 1; } + ADDRESS "127.0.0.1:$port" + else + ADDRESS "$(docker inspect "$container_name" --format '{{.NetworkSettings.Networks.bridge.IPAddress}}')" + fi + discard: | + #!/bin/bash + + set -e + + echo "Discarding container for system: $SPREAD_SYSTEM_ADDRESS" + + # The address format tells us how the container was allocated (see the allocate + # script's BREAD_NET modes): "host:port" -> publish mode, bare IP -> bridge mode. + # Inferring from the address makes discard robust even if BREAD_NET is flipped + # between allocate and discard. + container_name="" + case "$SPREAD_SYSTEM_ADDRESS" in + *:*) + # publish mode: match the container by its published host port. + target_port="${SPREAD_SYSTEM_ADDRESS##*:}" + for cid in $(docker ps -a --format '{{.ID}}'); do + if docker port "$cid" 22 2>/dev/null | grep -q ":${target_port}\$"; then + container_name=$(docker inspect "$cid" --format '{{.Name}}' | sed 's#^/##') + break + fi + done + not_found="No container found with published port: $target_port" + ;; + *) + # bridge mode: match the container by its bridge IP address. + for cid in $(docker ps -a --filter "network=bridge" --format '{{.ID}}'); do + cname=$(docker inspect "$cid" --format '{{.Name}}' | sed 's/^\/\(.*\)/\1/') + cip=$(docker inspect "$cid" --format '{{.NetworkSettings.Networks.bridge.IPAddress}}' || echo "") + if [ "$cip" == "$SPREAD_SYSTEM_ADDRESS" ]; then + container_name="$cname" + break + fi + done + not_found="No container found with IP address: $SPREAD_SYSTEM_ADDRESS" + ;; + esac + + if [ -n "$container_name" ]; then + echo "Removing container: $container_name" + docker rm -f "$container_name" 2>/dev/null || true + else + echo "$not_found" + exit 1 + fi + systems: + - ubuntu-22.04-amd64: + password: bread + workers: 2 + - ubuntu-22.04-arm64: + password: bread + workers: 2 + - ubuntu-22.04-s390x: + password: bread + workers: 2 + - ubuntu-22.04-ppc64le: + password: bread + workers: 2 + +suites: + tests/: + summary: Replace with your own suites diff --git a/inlined/bread-chisel-releases-22.04.yaml b/inlined/bread-chisel-releases-22.04.yaml new file mode 100644 index 0000000..f538906 --- /dev/null +++ b/inlined/bread-chisel-releases-22.04.yaml @@ -0,0 +1,160 @@ +project: chisel-releases + +path: /chisel-releases + +environment: + PROJECT_PATH: /chisel-releases + SHARED_LIBRARIES: $PROJECT_PATH/tests/spread/lib + PATH: /snap/bin:$PATH:$SHARED_LIBRARIES + # NOTE: empty by default; opt in per-run with `CHISEL_HACKS=1 spread ...`. + # $(HOST:...) forwards the host env var into the remote (spread sends only + # vars declared here, not the ambient host env). + CHISEL_HACKS: "$(HOST: echo $CHISEL_HACKS)" + +exclude: + - .git + - .github + - .vscode + - work + - rootfs + +backends: + docker: + type: adhoc + allocate: | + #spellchecker: ignore noninteractice + + set -e + + ver=$(echo "$SPREAD_SYSTEM" | cut -d- -f2) # e.g., ubuntu-24.04-amd64 -> 24.04 + arch=$(echo "$SPREAD_SYSTEM" | cut -d- -f3) # e.g., ubuntu-24.04-amd64 -> amd64 + echo "ver: $ver" + echo "arch: $arch" + + image="ghcr.io/lczyk/spread-bread/bread-chisel-releases:$ver" + echo "image: $image" + + # Networking mode: how spread reaches the container's sshd. + # bridge -- connect to the container's docker bridge IP. Works where the host + # shares the docker bridge network (linux native), so 172.17.x.x is + # routable from the spread host. + # publish -- publish sshd to 127.0.0.1: and connect there. + # Needed where the bridge IP is not host-routable, e.g. Docker + # Desktop on macOS (docker runs in a VM). + # Default is per-OS; override with BREAD_NET=bridge|publish. + mode="${BREAD_NET:-}" + if [ -z "$mode" ]; then + case "$(uname -s)" in + Darwin) mode=publish ;; + *) mode=bridge ;; + esac + fi + case "$mode" in + bridge|publish) ;; + *) echo "BREAD_NET must be 'bridge' or 'publish', got: '$mode'" >&2; exit 1 ;; + esac + echo "net mode: $mode" + + if [ "$mode" = bridge ]; then + sleep 0.$RANDOM # Minimize chances of a race condition + export counter_file=".spread-worker-num" + instance_num=$( + flock -x $counter_file bash -c ' + [ -s $counter_file ] || echo 0 > $counter_file + num=$(< $counter_file) && echo $num + echo $(( $num + 1 )) > $counter_file' + ) + container_name="bread-chisel-releases-${ver}-${arch}-${instance_num}" + publish_flag="" + else + # publish mode: no shared counter file (no flock dependency); a pid+RANDOM + # suffix is unique enough for a name. Docker picks the free host port. + container_name="bread-chisel-releases-${ver}-${arch}-$$-${RANDOM}" + publish_flag="-p 127.0.0.1::22" + fi + echo "container_name: $container_name" + + docker run \ + --rm \ + --platform "linux/$arch" \ + --privileged \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -e DEBIAN_FRONTEND=noninteractice \ + -e "usr=$SPREAD_SYSTEM_USERNAME" \ + -e "pass=$SPREAD_SYSTEM_PASSWORD" \ + $publish_flag \ + --name "$container_name" \ + -d "$image" + + until docker exec "$container_name" pgrep sshd; do sleep 1; done + + if [ "$mode" = publish ]; then + # The ephemeral host port docker mapped to the container's sshd. + port=$(docker port "$container_name" 22 | head -n1 | cut -d: -f2) + [ -n "$port" ] || { echo "could not find published sshd port for $container_name" >&2; exit 1; } + ADDRESS "127.0.0.1:$port" + else + ADDRESS "$(docker inspect "$container_name" --format '{{.NetworkSettings.Networks.bridge.IPAddress}}')" + fi + discard: | + #!/bin/bash + + set -e + + echo "Discarding container for system: $SPREAD_SYSTEM_ADDRESS" + + # The address format tells us how the container was allocated (see the allocate + # script's BREAD_NET modes): "host:port" -> publish mode, bare IP -> bridge mode. + # Inferring from the address makes discard robust even if BREAD_NET is flipped + # between allocate and discard. + container_name="" + case "$SPREAD_SYSTEM_ADDRESS" in + *:*) + # publish mode: match the container by its published host port. + target_port="${SPREAD_SYSTEM_ADDRESS##*:}" + for cid in $(docker ps -a --format '{{.ID}}'); do + if docker port "$cid" 22 2>/dev/null | grep -q ":${target_port}\$"; then + container_name=$(docker inspect "$cid" --format '{{.Name}}' | sed 's#^/##') + break + fi + done + not_found="No container found with published port: $target_port" + ;; + *) + # bridge mode: match the container by its bridge IP address. + for cid in $(docker ps -a --filter "network=bridge" --format '{{.ID}}'); do + cname=$(docker inspect "$cid" --format '{{.Name}}' | sed 's/^\/\(.*\)/\1/') + cip=$(docker inspect "$cid" --format '{{.NetworkSettings.Networks.bridge.IPAddress}}' || echo "") + if [ "$cip" == "$SPREAD_SYSTEM_ADDRESS" ]; then + container_name="$cname" + break + fi + done + not_found="No container found with IP address: $SPREAD_SYSTEM_ADDRESS" + ;; + esac + + if [ -n "$container_name" ]; then + echo "Removing container: $container_name" + docker rm -f "$container_name" 2>/dev/null || true + else + echo "$not_found" + exit 1 + fi + systems: + - ubuntu-22.04-amd64: + password: bread + workers: 4 + - ubuntu-22.04-arm64: + password: bread + workers: 4 + - ubuntu-22.04-s390x: + password: bread + workers: 4 + - ubuntu-22.04-ppc64le: + password: bread + workers: 4 + +suites: + tests/spread/integration/: + summary: Tests common scenarios diff --git a/makefile b/makefile index 9ab69d7..8449f82 100644 --- a/makefile +++ b/makefile @@ -14,7 +14,7 @@ GO_BUILDER_IMAGE := ubuntu/go:1.25-26.04_edge DOCKER_VERSION := 29.5.2 # Full matrix. -VERSIONS := 24.04 25.10 26.04 26.10 +VERSIONS := 22.04 24.04 25.10 26.04 26.10 ARCHES := amd64 arm64 s390x ppc64le # Arches with native runners (local dev + ci). s390x / ppc64le images build # under qemu and publish untested; tests + demo stay on the native pair. diff --git a/templates/bread-22.04.yaml.in b/templates/bread-22.04.yaml.in new file mode 100644 index 0000000..bf8e8f5 --- /dev/null +++ b/templates/bread-22.04.yaml.in @@ -0,0 +1,35 @@ +project: bread + +path: /bread + +environment: + PROJECT_PATH: /bread + SHARED_LIBRARIES: $PROJECT_PATH/tests/lib + PATH: $PATH:$SHARED_LIBRARIES + +exclude: + - .git + - .github + +backends: + docker: + type: adhoc + allocate: source scripts/spread_allocate_bread.sh + discard: source scripts/spread_discard_bread.sh + systems: + - ubuntu-22.04-amd64: + password: bread + workers: 2 + - ubuntu-22.04-arm64: + password: bread + workers: 2 + - ubuntu-22.04-s390x: + password: bread + workers: 2 + - ubuntu-22.04-ppc64le: + password: bread + workers: 2 + +suites: + tests/: + summary: Replace with your own suites diff --git a/templates/bread-chisel-releases-22.04.yaml.in b/templates/bread-chisel-releases-22.04.yaml.in new file mode 100644 index 0000000..9f2f923 --- /dev/null +++ b/templates/bread-chisel-releases-22.04.yaml.in @@ -0,0 +1,42 @@ +project: chisel-releases + +path: /chisel-releases + +environment: + PROJECT_PATH: /chisel-releases + SHARED_LIBRARIES: $PROJECT_PATH/tests/spread/lib + PATH: /snap/bin:$PATH:$SHARED_LIBRARIES + # NOTE: empty by default; opt in per-run with `CHISEL_HACKS=1 spread ...`. + # $(HOST:...) forwards the host env var into the remote (spread sends only + # vars declared here, not the ambient host env). + CHISEL_HACKS: "$(HOST: echo $CHISEL_HACKS)" + +exclude: + - .git + - .github + - .vscode + - work + - rootfs + +backends: + docker: + type: adhoc + allocate: source scripts/spread_allocate_bread-chisel-releases.sh + discard: source scripts/spread_discard_bread-chisel-releases.sh + systems: + - ubuntu-22.04-amd64: + password: bread + workers: 4 + - ubuntu-22.04-arm64: + password: bread + workers: 4 + - ubuntu-22.04-s390x: + password: bread + workers: 4 + - ubuntu-22.04-ppc64le: + password: bread + workers: 4 + +suites: + tests/spread/integration/: + summary: Tests common scenarios From 1edc686848dcbd95d4315f33fe65b4c33fc1f942 Mon Sep 17 00:00:00 2001 From: lczyk Date: Fri, 17 Jul 2026 09:51:36 +0200 Subject: [PATCH 2/2] docs: 22.04 in flavours list --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c243901..4d138d2 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ prebuilt docker images + ready-to-use spread yamls so you can drop Canonical [`s ## flavours -two image flavours, each for ubuntu 24.04, 25.10, 26.04, 26.10 x amd64 / arm64 / s390x / ppc64le, published as multiarch tags at `ghcr.io/lczyk/spread-bread/:`. heads-up: s390x + ppc64le images build under qemu and ship untested (no native runners); amd64 + arm64 are tested on every release: +two image flavours, each for ubuntu 22.04, 24.04, 25.10, 26.04, 26.10 x amd64 / arm64 / s390x / ppc64le, published as multiarch tags at `ghcr.io/lczyk/spread-bread/:`. heads-up: s390x + ppc64le images build under qemu and ship untested (no native runners); amd64 + arm64 are tested on every release: - **`bread`** -- base: ubuntu + sshd. general-purpose spread system; the test suite installs whatever else it needs. - **`bread-chisel-releases`** -- bread + `chisel` + the shell + container tooling typically needed by [chisel-releases](https://github.com/canonical/chisel-releases) spread tests (curl, wget, git, jq, file, sudo, tree, docker, skopeo). `chisel` and `docker` are built from source (canonical/chisel pinned by SHA, docker/cli pinned by version tag) so the bundled binaries are go 1.25+ and survive qemu emulation. @@ -38,8 +38,8 @@ allocate inside the yaml `docker run`s the matching multiarch ghcr image; `--pla available yamls in the release: -- `bread-{24.04,25.10,26.04,26.10}.yaml` -- `bread-chisel-releases-{24.04,25.10,26.04,26.10}.yaml` +- `bread-{22.04,24.04,25.10,26.04,26.10}.yaml` +- `bread-chisel-releases-{22.04,24.04,25.10,26.04,26.10}.yaml` ### networking (linux vs macOS)