Skip to content

feat: more bread - #1

Merged
lczyk merged 15 commits into
mainfrom
more-bread
May 25, 2026
Merged

lczyk merged 15 commits into
mainfrom
more-bread

Conversation

@lczyk

@lczyk lczyk commented May 25, 2026

Copy link
Copy Markdown
Owner

restructures spread-bread into a docker image host + a worked example + a test harness, and wires up ci/release to publish to ghcr.

image flavours

two flavours, three ubuntu versions x amd64/arm64, published as multiarch on tag push:

  • ghcr.io/lczyk/spread-bread/bread: -- ubuntu + sshd.
  • ghcr.io/lczyk/spread-bread/bread-chisel-releases: -- bread + chisel + curl/wget/git/jq/file/sudo/tree/docker/skopeo. consumers' inner spread runs use this for chisel-releases-style tests.

layout

top-level produces images + per-flavour, per-version inlined/*.yaml distribution artefacts (generated from templates/*.yaml.in by hack/inline_scripts.py, which splices the allocate/discard scripts inline). demo/ is a worked bread example. tests/ runs the spread-in-spread (spreadception) tests.

baked go binaries

chisel + spread cross-compiled w/ go 1.25.8+ in the Canonical ubuntu/go:1.25-26.04_edge container; docker CLI fetched from docker.com static (go 1.26.3). all baked binaries are recent-go so they survive qemu emulation -- ubuntu's apt-shipped docker.io is go 1.24 and crashes inside an amd64-on-arm64 container. cache lives in cache/binaries/ (gitignored).
pins live in makefile (CHISEL_REF, SPREAD_REF, DOCKER_VERSION, GO_BUILDER_IMAGE).

tests

tests/spread.yaml is an outer spread that allocates a bread-test container (bread + docker + spread, test-infra only, not published). each outer task copies one inlined yaml into a temp project + drops in a matching contract task, then runs an inner spread. the inner allocates real bread / bread-chisel-releases containers via the same scripts shipped to consumers, so the test exercises the published artefact literally rather than via a mock.

inner contract: ubuntu VERSION_ID matches expected, uname -m matches expected, and for the chisel flavour chisel --version matches the pinned tag + presence of the expected tools.

makefile

hash-stamp pattern; rebuilds fire only on real input changes. env-var narrowing for partial builds:

  • make build-bread VER=24.04 ARCH=amd64
  • make build-bread-chisel-releases ARCH=arm64
  • make demo runs the demo; cd tests && spread runs the contract suite.

ci / release

  • ci.yaml: PR + push to main. binaries -> build-and-test on per-arch native runners (ubuntu-24.04 + ubuntu-24.04-arm). no qemu, no publish.
  • release.yaml: push of r[0-9]+ tag. binaries -> build-and-test -> publish-ghcr + publish-release. multiarch tags assembled via buildah manifest from docker save tarballs (no per-arch tags published). github release rolls -- prior r* releases get deleted, only the latest stays; inlined/*.yaml attached as assets.

lczyk added 15 commits May 25, 2026 14:29
restructure repo to act as both a worked example of spread + a host
for prebuilt docker images. two image flavours, each across ubuntu
24.04 / 25.10 / 26.04 x amd64 / arm64:

- bread: lean ubuntu + sshd only.
- chisel-releases-bread: built on top of bread, adds chisel v1.4.1
  + the shell + container tooling chisel-releases spread tests expect.

top-level produces images + per-flavour, per-version distribution
yamls (templates/ -> hack/inline_scripts.py -> inlined/). makefile
uses pattern rules + hash-stamp pattern (hack/hash_inputs.sh) so
rebuilds only fire when relevant Dockerfile / scripts content
actually changed.

existing bread example moved into demo/ -- self-contained inlined
spread.yaml + tests, still consuming the lean bread images.

ghcr publication deferred.
spread's system-name validator (validSystem regex) requires names to
start with a letter. bare "24.04-amd64" gets rejected as invalid. add
the "ubuntu-" prefix everywhere systems are listed (templates, demo,
regenerated inlined yamls) and bump the cut indices in the allocate
scripts accordingly (ver = field 2, arch = field 3 once "ubuntu-" is
the first field).
three related changes to the makefile:

- new top-level demo target. ensures the lean bread images the demo
  needs are built, then delegates to demo/makefile's run target.
  demo is pinned to LTS only (24.04 + 26.04 x both arches) so non-LTS
  images aren't pulled into the demo dependency chain.
- env-var narrowing on build-bread / build-chisel-releases-bread:
  setting VER and/or ARCH narrows the matrix; unset means the full
  matrix. obsoletes the prior build-bread-% pattern aliases.
- extracted the inline shell from the stamp rules into
  hack/build_image.sh. one helper handles both flavours via case
  dispatch. makefile stamp rules now one-liners.
move docker.io from the chisel-releases-bread layer into the base
bread image. motivation: a bread container can now host nested docker
workloads (relevant for upcoming test flow where a bread container
acts as the outer host for a spread-in-spread run). also drops the
"lean" framing from comments and README -- bread is just the base
image, not advertised as minimal.

bread image size grows by ~340MB (docker.io + deps).
chisel-releases-bread net size unchanged (docker.io install removed,
inherited from base instead).
flavour names now share a common bread- prefix, which lines up with
the new bread-test test-host image landing in a follow-up commit and
groups all bread-family images together when listed alphabetically.

pure renaming change: files renamed via git mv, identifier rewritten
in every referencing file. no functional change.
undoes commit 97c8e55 (feat: ship docker.io in bread base). the
test-host image landing in a follow-up commit installs docker.io
itself, so the base bread image doesn't need it after all -- keeps
the base small and the docker.io concern scoped to images that
genuinely need it.

bread back to sshd-only. bread-chisel-releases re-adds docker.io to
its apt-install line. dockerfile comments realigned with the new
bread-chisel-releases naming.
new image flavour bread-test, only built at 26.04 x both arches. used
as the outer system in upcoming spread-in-spread test flow.

multi-stage Dockerfile (tests/Dockerfile.bread-test-26.04):
  - stage 1 (golang:1-bookworm): clones canonical/spread + builds the
    spread binary statically (CGO disabled).
  - stage 2 (FROM bread:${BASE_TAG}): apt-installs docker.io for
    nested docker workloads, copies the spread binary in.

wired through hack/hash_inputs.sh + hack/build_image.sh (case for the
new flavour) and makefile (BREAD_TEST_STAMPS, build-bread-test target,
stamp pattern rule, clean entry). final image lives outside the main
matrix so build-all / clean / etc treat it as an extra.
two related sshd hardening tweaks to the base bread Dockerfiles:

1. append PasswordAuthentication yes to /etc/ssh/sshd_config. ubuntu's
   default config has the line commented out, and on some recent
   OpenSSH builds the compile-time default is no rather than yes, so
   spread's password auth needs the override to land reliably.

2. replace /etc/pam.d/sshd with a pam_permit-only stack. the default
   pam_unix stack calls unix_chkpwd, which on some hosts (kernel +
   docker + libxcrypt combo) fails inside privileged containers with
   "libcrypt.so.1: cannot change memory protections" and returns
   PAM_PERM_DENIED. these are throwaway test containers; we don't
   care about password validity, only that spread can attach.

inherited by bread-chisel-releases (FROM bread:VER-ARCH) and the
bread-test image, so the fix applies everywhere sshd lives.
new tests/ tree exercising the published inlined yamls end-to-end.

outer layer: tests/spread.yaml uses an adhoc backend that spawns a
bread-test container as its sole system (one per arch). the container
gets the docker socket bind-mounted in plus the repo bind-mounted at
/spread-bread, so the outer task can reach both the host docker
daemon and the inlined yamls / contract task definitions.

inner layer: each outer task copies one inlined yaml into a temp
project dir, drops in the matching contract tasks, and runs spread.
the inner spread allocates bread or bread-chisel-releases containers
via the same scripts shipped to consumers, so the test exercises the
distribution artefact literally rather than via a mock.

contract assertions:
  - bread: /etc/os-release VERSION_ID matches expected ubuntu version,
    uname -m matches expected arch.
  - bread-chisel-releases: the above + chisel --version matches the
    pinned tag + curl/wget/git/jq/file/sudo/tree/docker/skopeo on PATH.

variants drive each suite over the three ubuntu versions.

makefile wiring + bread-test image build pipeline land separately.
bread-chisel-releases shipped ubuntu's apt docker.io (built with
go 1.24) and bread-test built spread inside a multi-stage docker
build. on hosts where amd64 containers run under qemu, the apt
docker.io's go 1.24 runtime crashes inside the emulated container.
moving every go binary we bake into images onto a single, recent-go
build path fixes that.

new flow:
  - hack/build_binaries.sh runs Canonical's ubuntu/go:1.25-26.04_edge
    container once. it cross-compiles chisel + spread for both arches
    with go 1.25.8+ (GOTOOLCHAIN=auto fetches the toolchain go.mod
    demands) and fetches the upstream docker static tarball for each
    arch (built with go 1.26.3 -- qemu-safe). chisel version is
    injected via -X ldflag so the binary self-reports the pinned tag
    rather than "unknown".
  - outputs land in cache/binaries/{chisel,spread,docker}-{amd64,arm64}
    (gitignored).
  - .stamp/binaries gates the build via the usual hash-of-inputs
    pattern (CHISEL_REF, SPREAD_REF, DOCKER_VERSION, GO_BUILDER_IMAGE,
    plus build_binaries.sh content).
  - bread-chisel-releases and bread-test stamps now depend on
    .stamp/binaries, so any binary refresh cascades into image
    rebuilds.

Dockerfile changes:
  - bread-chisel-releases-* drop docker.io from the apt line, COPY
    chisel + docker from cache/binaries via a BUILD_ARCH build-arg.
  - bread-test-26.04 drops its golang multi-stage builder, COPY's
    spread + docker from cache/binaries.

hack/build_image.sh passes --build-arg BUILD_ARCH=$arch through for
both chisel-releases and test images.
each outer arch now runs 2 spread-bread test containers in parallel
instead of serialising the 6 jobs through one. inner spread inside
each outer is unchanged (still workers: 2 inside its own inlined
yaml).

end-to-end full-matrix test wall clock drops from ~60s to ~20s on a
qcom arm64 dev box.
BREAD_TEST_STAMPS used the full ARCHES list rather than
SELECTED_ARCHES, so make build-bread-test ARCH=amd64 still built
both arches. consistent narrowing with the other stamp lists; allows
the ci build-and-test workflow to run per-arch jobs that only build
their own arch bread-test image.
five workflows modelled on the not-quite-rust-rock layout but adapted
for docker images and our two-flavour matrix.

reusable building blocks:
  - binaries.yaml: builds cache/binaries via the ubuntu/go cross-compile
    container once, uploads the 6 binaries + matching .stamp/binaries
    as a single artefact. downstream jobs restore them and avoid the
    expensive cross-compile.
  - build-and-test.yaml: matrix per-arch on native runners
    (ubuntu-24.04 + ubuntu-24.04-arm). each job restores binaries,
    builds bread / bread-chisel-releases / bread-test images for its
    own arch, installs spread, and runs the spread-in-spread tests
    scoped to outer:ubuntu-26.04-<arch>. no qemu in ci.
  - publish-ghcr.yaml: per-arch jobs save 6 image tarballs each via
    docker save. a single stitch job downloads both arches and uses
    buildah manifest to assemble + push multiarch manifests for the
    six (flavour, ver) pairs to ghcr.io/<repo>/<flavour>:<ver>. no
    per-arch tags published. bread-test is intentionally not
    published (test-infra-only).

entry workflows:
  - ci.yaml: PR + push to main + manual dispatch. binaries followed
    by build-and-test. concurrency cancel-in-progress on the same
    ref.
  - release.yaml: triggers on push of an r[0-9]+ tag (or manual
    dispatch). binaries + build-and-test + publish-ghcr + a
    publish-release job that regenerates inlined yamls, deletes any
    prior r* releases (rolling: only the latest stays on the GitHub
    side), and attaches inlined/*.yaml to the new release.
each inlined yaml lists both ubuntu arches, so before this patch a
ci per-arch job would spawn the missing-arch inner container too --
host docker daemon has only the arch we built locally, and the pull
fallback fails with denied access.

derive arch from SPREAD_SYSTEM and pass a spread filter expression
(docker:ubuntu-...-arch:...) to inner spread so it only schedules
jobs whose system matches the outer's arch.

locally with make all (both arches built), behaviour is unchanged --
the filter just narrows from 2 inner systems to 1.
@lczyk
lczyk merged commit db4791a into main May 25, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant