diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ef38bee..eff545b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,12 +2,17 @@ name: CI run-name: CI for ${{ github.ref }} # Build + test on PR and on push to main. No publishing. +# Rock-only changes are handled by rocks-ci.yaml -- ignore them here. on: pull_request: + paths-ignore: + - 'rocks/**' push: branches: - main + paths-ignore: + - 'rocks/**' workflow_dispatch: # Cancel in-flight runs on the same ref when a newer one starts. diff --git a/.github/workflows/rocks-ci.yaml b/.github/workflows/rocks-ci.yaml new file mode 100644 index 0000000..fdf0c81 --- /dev/null +++ b/.github/workflows/rocks-ci.yaml @@ -0,0 +1,112 @@ +name: rocks CI +run-name: rocks CI for ${{ github.ref }} + +# Build + test the chiselled rocks on PR + push to main. No publishing. +# Build packs each rock per-arch (native runners); test loads that arch's rocks +# into the local daemon and runs the single top-level spread suite over the +# native-arch systems. +on: + pull_request: + paths: + - 'rocks/**' + - '.github/workflows/rocks-ci.yaml' + push: + branches: + - main + paths: + - 'rocks/**' + - '.github/workflows/rocks-ci.yaml' + workflow_dispatch: + +concurrency: + group: rocks-ci-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build: + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + dir: + - bread-24.04 + - bread-25.10 + - bread-26.04 + - bread-chisel-releases-24.04 + - bread-chisel-releases-25.10 + - bread-chisel-releases-26.04 + arch: + - amd64 + - arm64 + include: + - arch: amd64 + runner: ubuntu-24.04 + - arch: arm64 + runner: ubuntu-24.04-arm + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Sync shared hack/ + patches/ into the rock dir + run: ruby rocks/sync_shared.rb ${{ matrix.dir }} + + - name: Pack rock + uses: canonical/craft-actions/rockcraft-pack@main + with: + path: rocks/${{ matrix.dir }} + verbosity: brief + + - name: Rename to predictable artefact name + run: mv rocks/${{ matrix.dir }}/*.rock rocks/${{ matrix.dir }}/${{ matrix.dir }}.rock + + - name: Upload rock artefact + uses: actions/upload-artifact@v6 + with: + name: rock-${{ matrix.dir }}-${{ matrix.arch }} + path: rocks/${{ matrix.dir }}/${{ matrix.dir }}.rock + + test: + needs: build + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runner: ubuntu-24.04 + - arch: arm64 + runner: ubuntu-24.04-arm + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Download this arch's rocks + uses: actions/download-artifact@v6 + with: + pattern: rock-*-${{ matrix.arch }} + path: dl + + - name: Place rocks under their dirs + run: | + set -e + for d in dl/rock-*-${{ matrix.arch }}; do + base=$(basename "$d") # rock-- + dir=${base#rock-} + dir=${dir%-${{ matrix.arch }}} + cp "$d/$dir.rock" "rocks/$dir/$dir.rock" + done + + - name: Install spread + skopeo + run: | + sudo apt-get update + sudo apt-get install -y skopeo + go install github.com/canonical/spread/cmd/spread@latest + echo "$HOME/go/bin" >> "$GITHUB_PATH" + + - name: Run the top-level spread suite (${{ matrix.arch }}) + run: make -C rocks spread-only diff --git a/.gitignore b/.gitignore index ba02087..907c629 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,14 @@ .spread-reuse*.yaml .stamp/ cache/ + +# rockcraft (rocks/) build artefacts +*.rock +.rock.stamp + +# per-rock hack/ + patches/ are synced from rocks/hack + rocks/patches +# (see rocks/sync_shared.rb) -- only the shared source of truth is tracked. +rocks/bread-*/hack/ +rocks/bread-*/patches/ __pycache__/ *.pyc diff --git a/rocks/bread-24.04/rockcraft.yaml b/rocks/bread-24.04/rockcraft.yaml new file mode 100644 index 0000000..b6ec2b1 --- /dev/null +++ b/rocks/bread-24.04/rockcraft.yaml @@ -0,0 +1,117 @@ +# spellchecker: ignore rockcraft chiselled coreutils passwd nologin sshd usrmerge pebble noninteractive + +name: bread +version: "24.04" +summary: chiselled ubuntu 24.04 + sshd spread test-host rock +description: | + Throwaway spread test-host: a chiselled (base: bare) Ubuntu 24.04 rock with + just enough to act as a spread system -- bash, coreutils, openssh-server, + apt, and a pam_permit-only auth stack (accepts ANY password for root). + + INSECURE BY DESIGN. Built for ephemeral spread test containers only; never + expose to a network or use in production. + +base: bare +build-base: ubuntu@24.04 + +platforms: + amd64: + arm64: + +# sshd runs as a pebble service (pebble is pid 1 in the rock). The +# openssh-server_services slice ships systemd units, not a pebble layer, so we +# drive sshd ourselves via hack/sshd-entry.sh (host-key gen + exec sshd -D). +services: + sshd: + override: replace + command: /usr/local/bin/sshd-entry.sh + startup: enabled + +parts: + bread: + # We chisel-cut the slices ourselves rather than using stage-packages: + # rockcraft's stage-packages cut has no retry, and the chisel-releases + # archive intermittently flakes ("expected digest ..."). hack/chisel_cut.sh + # retries around that. (Pattern from not-quite-rust-rock.) + plugin: nil + build-snaps: + - chisel + build-packages: + - git + override-build: | + set -eu + + releases=$(mktemp -d) + git clone --depth 1 --branch ubuntu-24.04 \ + https://github.com/canonical/chisel-releases.git "$releases" + + # tar + gzip: needed by spread to sync the project onto the system over + # ssh (not part of coreutils). dpkg_bins: apt_apt-get only pulls + # dpkg_maintscript-support, so apt-get install can't unpack without it. + # pam: only libpam-modules_libs (carries pam_permit.so) -- NOT the + # _config / _pam-config default stacks. + bash "$CRAFT_PROJECT_DIR/hack/chisel_cut.sh" "$releases" \ + base-files_base base-files_etc base-files_var base-files_tmp \ + base-files_lib base-files_home base-files_release-info base-files_chisel \ + bash_bins coreutils_bins tar_bins gzip_bins \ + base-passwd_data login_bins passwd_bins \ + openssh-server_bins openssh-server_config ca-certificates_data \ + libpam-modules_libs \ + apt_apt-get apt_config apt_var apt_default-source-list dpkg_bins + + cd "$CRAFT_PART_INSTALL" + + # --- users: base-passwd_data gives us /etc/passwd + /etc/group (incl + # root), but not the sshd privsep user nor any /etc/shadow. add both. + grep -q '^sshd:' etc/passwd || \ + echo 'sshd:x:101:65534::/run/sshd:/usr/sbin/nologin' >> etc/passwd + # root password "bread" (matches the docker-image flavour). pam_permit + # makes the value irrelevant for auth, but root must not be locked. + hash=$(openssl passwd -6 bread) + { + printf 'root:%s:20000:0:99999:7:::\n' "$hash" + printf 'sshd:!:20000:0:99999:7:::\n' + } > etc/shadow + chmod 0640 etc/shadow + + # --- sshd config. openssh-server_config ships sshd_config.d/ but not + # /etc/ssh/sshd_config itself, so write a minimal one. + mkdir -p etc/ssh + cat > etc/ssh/sshd_config <<'EOF' + PermitRootLogin yes + PasswordAuthentication yes + UsePAM yes + Subsystem sftp /usr/lib/openssh/sftp-server + PidFile /run/sshd.pid + EOF + + # --- pam: permit-only stack (always-allow), verbatim shape from the + # docker-image flavour. + mkdir -p etc/pam.d + 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 + + # --- /etc/profile: base-files ships profile.d/ but not /etc/profile, so + # login shells wouldn't source the bread-warning banner. add a minimal one. + cat > etc/profile <<'EOF' + if [ -d /etc/profile.d ]; then + for __pf in /etc/profile.d/*.sh; do + [ -r "$__pf" ] && . "$__pf" + done + unset __pf + fi + EOF + + # --- bread bits: sshd entrypoint, lazy-apt shim, insecure-image banner. + install -D -m0755 "$CRAFT_PROJECT_DIR/hack/sshd-entry.sh" usr/local/bin/sshd-entry.sh + install -D -m0755 "$CRAFT_PROJECT_DIR/hack/lazy-apt.sh" usr/local/bin/apt-get + install -m0755 "$CRAFT_PROJECT_DIR/hack/lazy-apt.sh" usr/local/bin/apt + install -D -m0644 "$CRAFT_PROJECT_DIR/hack/banner.txt" etc/bread-banner.txt + install -D -m0644 "$CRAFT_PROJECT_DIR/hack/bread-warning.sh" etc/profile.d/00-bread-warning.sh + mkdir -p root + printf '\n# bread-warning\n[ -r /etc/profile.d/00-bread-warning.sh ] && . /etc/profile.d/00-bread-warning.sh\n' \ + >> root/.bashrc diff --git a/rocks/bread-25.10/rockcraft.yaml b/rocks/bread-25.10/rockcraft.yaml new file mode 100644 index 0000000..f680333 --- /dev/null +++ b/rocks/bread-25.10/rockcraft.yaml @@ -0,0 +1,126 @@ +# spellchecker: ignore rockcraft chiselled coreutils passwd nologin sshd usrmerge pebble noninteractive + +name: bread +version: "25.10" +summary: chiselled ubuntu 25.10 + sshd spread test-host rock +description: | + Throwaway spread test-host: a chiselled (base: bare) Ubuntu 25.10 rock with + just enough to act as a spread system -- bash, coreutils, openssh-server, + apt, and a pam_permit-only auth stack (accepts ANY password for root). + + INSECURE BY DESIGN. Built for ephemeral spread test containers only; never + expose to a network or use in production. + +base: bare +build-base: ubuntu@25.10 + +platforms: + amd64: + arm64: + +# sshd runs as a pebble service (pebble is pid 1 in the rock). The +# openssh-server_services slice ships systemd units, not a pebble layer, so we +# drive sshd ourselves via hack/sshd-entry.sh (host-key gen + exec sshd -D). +services: + sshd: + override: replace + command: /usr/local/bin/sshd-entry.sh + startup: enabled + +parts: + bread: + # We chisel-cut the slices ourselves rather than using stage-packages: + # rockcraft's stage-packages cut has no retry, and the chisel-releases + # archive intermittently flakes ("expected digest ..."). hack/chisel_cut.sh + # retries around that. (Pattern from not-quite-rust-rock.) + plugin: nil + build-snaps: + - chisel + build-packages: + - git + override-build: | + set -eu + + releases=$(mktemp -d) + git clone --depth 1 --branch ubuntu-25.10 \ + https://github.com/canonical/chisel-releases.git "$releases" + + # OpenSSH >=9.8 (this release) re-execs sshd-session + sshd-auth per + # connection; the openssh-server slice ships only /usr/sbin/sshd, so sshd + # exits 255 at startup. Add them to the cloned slice before cutting. + # (24.04's OpenSSH 9.6 predates the split and has no such binaries.) + sed -i \ + -e '\#^ /usr/sbin/sshd:#a\ /usr/lib/openssh/sshd-session:' \ + -e '\#^ /usr/sbin/sshd:#a\ /usr/lib/openssh/sshd-auth:' \ + "$releases/slices/openssh-server.yaml" + + # tar + gzip: needed by spread to sync the project onto the system over + # ssh (not part of coreutils). dpkg_bins: apt_apt-get only pulls + # dpkg_maintscript-support, so apt-get install can't unpack without it. + # pam: only libpam-modules_libs (carries pam_permit.so) -- NOT the + # _config / _pam-config default stacks. + bash "$CRAFT_PROJECT_DIR/hack/chisel_cut.sh" "$releases" \ + base-files_base base-files_etc base-files_var base-files_tmp \ + base-files_lib base-files_home base-files_release-info base-files_chisel \ + bash_bins coreutils_bins tar_bins gzip_bins \ + base-passwd_data login_bins passwd_bins \ + openssh-server_bins openssh-server_config ca-certificates_data \ + libpam-modules_libs \ + apt_apt-get apt_config apt_var apt_default-source-list dpkg_bins + + cd "$CRAFT_PART_INSTALL" + + # --- users: base-passwd_data gives us /etc/passwd + /etc/group (incl + # root), but not the sshd privsep user nor any /etc/shadow. add both. + grep -q '^sshd:' etc/passwd || \ + echo 'sshd:x:101:65534::/run/sshd:/usr/sbin/nologin' >> etc/passwd + # root password "bread" (matches the docker-image flavour). pam_permit + # makes the value irrelevant for auth, but root must not be locked. + hash=$(openssl passwd -6 bread) + { + printf 'root:%s:20000:0:99999:7:::\n' "$hash" + printf 'sshd:!:20000:0:99999:7:::\n' + } > etc/shadow + chmod 0640 etc/shadow + + # --- sshd config. openssh-server_config ships sshd_config.d/ but not + # /etc/ssh/sshd_config itself, so write a minimal one. + mkdir -p etc/ssh + cat > etc/ssh/sshd_config <<'EOF' + PermitRootLogin yes + PasswordAuthentication yes + UsePAM yes + Subsystem sftp /usr/lib/openssh/sftp-server + PidFile /run/sshd.pid + EOF + + # --- pam: permit-only stack (always-allow), verbatim shape from the + # docker-image flavour. + mkdir -p etc/pam.d + 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 + + # --- /etc/profile: base-files ships profile.d/ but not /etc/profile, so + # login shells wouldn't source the bread-warning banner. add a minimal one. + cat > etc/profile <<'EOF' + if [ -d /etc/profile.d ]; then + for __pf in /etc/profile.d/*.sh; do + [ -r "$__pf" ] && . "$__pf" + done + unset __pf + fi + EOF + + # --- bread bits: sshd entrypoint, lazy-apt shim, insecure-image banner. + install -D -m0755 "$CRAFT_PROJECT_DIR/hack/sshd-entry.sh" usr/local/bin/sshd-entry.sh + install -D -m0755 "$CRAFT_PROJECT_DIR/hack/lazy-apt.sh" usr/local/bin/apt-get + install -m0755 "$CRAFT_PROJECT_DIR/hack/lazy-apt.sh" usr/local/bin/apt + install -D -m0644 "$CRAFT_PROJECT_DIR/hack/banner.txt" etc/bread-banner.txt + install -D -m0644 "$CRAFT_PROJECT_DIR/hack/bread-warning.sh" etc/profile.d/00-bread-warning.sh + mkdir -p root + printf '\n# bread-warning\n[ -r /etc/profile.d/00-bread-warning.sh ] && . /etc/profile.d/00-bread-warning.sh\n' \ + >> root/.bashrc diff --git a/rocks/bread-26.04/rockcraft.yaml b/rocks/bread-26.04/rockcraft.yaml new file mode 100644 index 0000000..98d2a61 --- /dev/null +++ b/rocks/bread-26.04/rockcraft.yaml @@ -0,0 +1,126 @@ +# spellchecker: ignore rockcraft chiselled coreutils passwd nologin sshd usrmerge pebble noninteractive + +name: bread +version: "26.04" +summary: chiselled ubuntu 26.04 + sshd spread test-host rock +description: | + Throwaway spread test-host: a chiselled (base: bare) Ubuntu 26.04 rock with + just enough to act as a spread system -- bash, coreutils, openssh-server, + apt, and a pam_permit-only auth stack (accepts ANY password for root). + + INSECURE BY DESIGN. Built for ephemeral spread test containers only; never + expose to a network or use in production. + +base: bare +build-base: ubuntu@26.04 + +platforms: + amd64: + arm64: + +# sshd runs as a pebble service (pebble is pid 1 in the rock). The +# openssh-server_services slice ships systemd units, not a pebble layer, so we +# drive sshd ourselves via hack/sshd-entry.sh (host-key gen + exec sshd -D). +services: + sshd: + override: replace + command: /usr/local/bin/sshd-entry.sh + startup: enabled + +parts: + bread: + # We chisel-cut the slices ourselves rather than using stage-packages: + # rockcraft's stage-packages cut has no retry, and the chisel-releases + # archive intermittently flakes ("expected digest ..."). hack/chisel_cut.sh + # retries around that. (Pattern from not-quite-rust-rock.) + plugin: nil + build-snaps: + - chisel + build-packages: + - git + override-build: | + set -eu + + releases=$(mktemp -d) + git clone --depth 1 --branch ubuntu-26.04 \ + https://github.com/canonical/chisel-releases.git "$releases" + + # OpenSSH >=9.8 (this release) re-execs sshd-session + sshd-auth per + # connection; the openssh-server slice ships only /usr/sbin/sshd, so sshd + # exits 255 at startup. Add them to the cloned slice before cutting. + # (24.04's OpenSSH 9.6 predates the split and has no such binaries.) + sed -i \ + -e '\#^ /usr/sbin/sshd:#a\ /usr/lib/openssh/sshd-session:' \ + -e '\#^ /usr/sbin/sshd:#a\ /usr/lib/openssh/sshd-auth:' \ + "$releases/slices/openssh-server.yaml" + + # tar + gzip: needed by spread to sync the project onto the system over + # ssh (not part of coreutils). dpkg_bins: apt_apt-get only pulls + # dpkg_maintscript-support, so apt-get install can't unpack without it. + # pam: only libpam-modules_libs (carries pam_permit.so) -- NOT the + # _config / _pam-config default stacks. + bash "$CRAFT_PROJECT_DIR/hack/chisel_cut.sh" "$releases" \ + base-files_base base-files_etc base-files_var base-files_tmp \ + base-files_lib base-files_home base-files_release-info base-files_chisel \ + bash_bins coreutils_bins tar_bins gzip_bins \ + base-passwd_data login_bins passwd_bins \ + openssh-server_bins openssh-server_config ca-certificates_data \ + libpam-modules_libs \ + apt_apt-get apt_config apt_var apt_default-source-list dpkg_bins + + cd "$CRAFT_PART_INSTALL" + + # --- users: base-passwd_data gives us /etc/passwd + /etc/group (incl + # root), but not the sshd privsep user nor any /etc/shadow. add both. + grep -q '^sshd:' etc/passwd || \ + echo 'sshd:x:101:65534::/run/sshd:/usr/sbin/nologin' >> etc/passwd + # root password "bread" (matches the docker-image flavour). pam_permit + # makes the value irrelevant for auth, but root must not be locked. + hash=$(openssl passwd -6 bread) + { + printf 'root:%s:20000:0:99999:7:::\n' "$hash" + printf 'sshd:!:20000:0:99999:7:::\n' + } > etc/shadow + chmod 0640 etc/shadow + + # --- sshd config. openssh-server_config ships sshd_config.d/ but not + # /etc/ssh/sshd_config itself, so write a minimal one. + mkdir -p etc/ssh + cat > etc/ssh/sshd_config <<'EOF' + PermitRootLogin yes + PasswordAuthentication yes + UsePAM yes + Subsystem sftp /usr/lib/openssh/sftp-server + PidFile /run/sshd.pid + EOF + + # --- pam: permit-only stack (always-allow), verbatim shape from the + # docker-image flavour. + mkdir -p etc/pam.d + 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 + + # --- /etc/profile: base-files ships profile.d/ but not /etc/profile, so + # login shells wouldn't source the bread-warning banner. add a minimal one. + cat > etc/profile <<'EOF' + if [ -d /etc/profile.d ]; then + for __pf in /etc/profile.d/*.sh; do + [ -r "$__pf" ] && . "$__pf" + done + unset __pf + fi + EOF + + # --- bread bits: sshd entrypoint, lazy-apt shim, insecure-image banner. + install -D -m0755 "$CRAFT_PROJECT_DIR/hack/sshd-entry.sh" usr/local/bin/sshd-entry.sh + install -D -m0755 "$CRAFT_PROJECT_DIR/hack/lazy-apt.sh" usr/local/bin/apt-get + install -m0755 "$CRAFT_PROJECT_DIR/hack/lazy-apt.sh" usr/local/bin/apt + install -D -m0644 "$CRAFT_PROJECT_DIR/hack/banner.txt" etc/bread-banner.txt + install -D -m0644 "$CRAFT_PROJECT_DIR/hack/bread-warning.sh" etc/profile.d/00-bread-warning.sh + mkdir -p root + printf '\n# bread-warning\n[ -r /etc/profile.d/00-bread-warning.sh ] && . /etc/profile.d/00-bread-warning.sh\n' \ + >> root/.bashrc diff --git a/rocks/bread-chisel-releases-24.04/rockcraft.yaml b/rocks/bread-chisel-releases-24.04/rockcraft.yaml new file mode 100644 index 0000000..5cca426 --- /dev/null +++ b/rocks/bread-chisel-releases-24.04/rockcraft.yaml @@ -0,0 +1,123 @@ +# spellchecker: ignore rockcraft chiselled coreutils passwd nologin sshd pebble chisel CHISEL GOTOOLCHAIN noninteractive + +name: bread-chisel-releases +version: "24.04" +summary: chiselled ubuntu 24.04 + sshd + chisel + tooling spread test-host rock +description: | + Throwaway spread test-host for chisel-releases spread tests: a chiselled + (base: bare) Ubuntu 24.04 rock with bash, coreutils, openssh-server, apt, a + pam_permit-only auth stack, plus chisel (+ chisel-hacked), docker, and the + tooling chisel-releases tests typically need (curl, wget, git, jq, file, + sudo, ip). + + INSECURE BY DESIGN. Built for ephemeral spread test containers only; never + expose to a network or use in production. + +base: bare +build-base: ubuntu@24.04 + +platforms: + amd64: + arm64: + +services: + sshd: + override: replace + command: /usr/local/bin/sshd-entry.sh + startup: enabled + +parts: + # --- de-novo copy of the bread base (rocks can't build from rocks), with + # the chisel-releases tooling slices folded into the same single cut. ------ + bread: + plugin: nil + build-snaps: + - chisel + build-packages: + - git + override-build: | + set -eu + + releases=$(mktemp -d) + git clone --depth 1 --branch ubuntu-24.04 \ + https://github.com/canonical/chisel-releases.git "$releases" + + # bread base slices + chisel-releases tooling slices, one cut. + # holes: tree + skopeo have no slice yet -> omitted. + bash "$CRAFT_PROJECT_DIR/hack/chisel_cut.sh" "$releases" \ + base-files_base base-files_etc base-files_var base-files_tmp \ + base-files_lib base-files_home base-files_release-info base-files_chisel \ + bash_bins coreutils_bins tar_bins gzip_bins \ + base-passwd_data login_bins passwd_bins \ + openssh-server_bins openssh-server_config ca-certificates_data \ + libpam-modules_libs \ + apt_apt-get apt_config apt_var apt_default-source-list dpkg_bins \ + curl_bins wget_bins git_bins git_http-support jq_bins file_bins \ + sudo_bins sudo_config iproute2_bins + + cd "$CRAFT_PART_INSTALL" + + grep -q '^sshd:' etc/passwd || \ + echo 'sshd:x:101:65534::/run/sshd:/usr/sbin/nologin' >> etc/passwd + hash=$(openssl passwd -6 bread) + { + printf 'root:%s:20000:0:99999:7:::\n' "$hash" + printf 'sshd:!:20000:0:99999:7:::\n' + } > etc/shadow + chmod 0640 etc/shadow + + mkdir -p etc/ssh + cat > etc/ssh/sshd_config <<'EOF' + PermitRootLogin yes + PasswordAuthentication yes + UsePAM yes + Subsystem sftp /usr/lib/openssh/sftp-server + PidFile /run/sshd.pid + EOF + + mkdir -p etc/pam.d + 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 + + cat > etc/profile <<'EOF' + if [ -d /etc/profile.d ]; then + for __pf in /etc/profile.d/*.sh; do + [ -r "$__pf" ] && . "$__pf" + done + unset __pf + fi + EOF + + install -D -m0755 "$CRAFT_PROJECT_DIR/hack/sshd-entry.sh" usr/local/bin/sshd-entry.sh + install -D -m0755 "$CRAFT_PROJECT_DIR/hack/lazy-apt.sh" usr/local/bin/apt-get + install -m0755 "$CRAFT_PROJECT_DIR/hack/lazy-apt.sh" usr/local/bin/apt + install -D -m0644 "$CRAFT_PROJECT_DIR/hack/banner.txt" etc/bread-banner.txt + install -D -m0644 "$CRAFT_PROJECT_DIR/hack/bread-warning.sh" etc/profile.d/00-bread-warning.sh + mkdir -p root + printf '\n# bread-warning\n[ -r /etc/profile.d/00-bread-warning.sh ] && . /etc/profile.d/00-bread-warning.sh\n' \ + >> root/.bashrc + + # --- chisel + chisel-hacked, built from source (go is build-only) -------- + chisel: + plugin: nil + build-snaps: + - go + build-packages: + - git + build-environment: + - CHISEL_REF: v1.4.1 + override-build: bash "$CRAFT_PROJECT_DIR/hack/chisel_override_build.sh" + + # --- docker static CLI --------------------------------------------------- + docker: + plugin: nil + build-packages: + - curl + - ca-certificates + build-environment: + - DOCKER_VERSION: 29.5.2 + override-build: bash "$CRAFT_PROJECT_DIR/hack/docker_override_build.sh" diff --git a/rocks/bread-chisel-releases-25.10/rockcraft.yaml b/rocks/bread-chisel-releases-25.10/rockcraft.yaml new file mode 100644 index 0000000..292b67a --- /dev/null +++ b/rocks/bread-chisel-releases-25.10/rockcraft.yaml @@ -0,0 +1,132 @@ +# spellchecker: ignore rockcraft chiselled coreutils passwd nologin sshd pebble chisel CHISEL GOTOOLCHAIN noninteractive + +name: bread-chisel-releases +version: "25.10" +summary: chiselled ubuntu 25.10 + sshd + chisel + tooling spread test-host rock +description: | + Throwaway spread test-host for chisel-releases spread tests: a chiselled + (base: bare) Ubuntu 25.10 rock with bash, coreutils, openssh-server, apt, a + pam_permit-only auth stack, plus chisel (+ chisel-hacked), docker, and the + tooling chisel-releases tests typically need (curl, wget, git, jq, file, + sudo, ip). + + INSECURE BY DESIGN. Built for ephemeral spread test containers only; never + expose to a network or use in production. + +base: bare +build-base: ubuntu@25.10 + +platforms: + amd64: + arm64: + +services: + sshd: + override: replace + command: /usr/local/bin/sshd-entry.sh + startup: enabled + +parts: + # --- de-novo copy of the bread base (rocks can't build from rocks), with + # the chisel-releases tooling slices folded into the same single cut. ------ + bread: + plugin: nil + build-snaps: + - chisel + build-packages: + - git + override-build: | + set -eu + + releases=$(mktemp -d) + git clone --depth 1 --branch ubuntu-25.10 \ + https://github.com/canonical/chisel-releases.git "$releases" + + # OpenSSH >=9.8 (this release) re-execs sshd-session + sshd-auth per + # connection; the openssh-server slice ships only /usr/sbin/sshd, so sshd + # exits 255 at startup. Add them to the cloned slice before cutting. + # (24.04's OpenSSH 9.6 predates the split and has no such binaries.) + sed -i \ + -e '\#^ /usr/sbin/sshd:#a\ /usr/lib/openssh/sshd-session:' \ + -e '\#^ /usr/sbin/sshd:#a\ /usr/lib/openssh/sshd-auth:' \ + "$releases/slices/openssh-server.yaml" + + # bread base slices + chisel-releases tooling slices, one cut. + # holes: tree + skopeo have no slice yet -> omitted. + bash "$CRAFT_PROJECT_DIR/hack/chisel_cut.sh" "$releases" \ + base-files_base base-files_etc base-files_var base-files_tmp \ + base-files_lib base-files_home base-files_release-info base-files_chisel \ + bash_bins coreutils_bins tar_bins gzip_bins \ + base-passwd_data login_bins passwd_bins \ + openssh-server_bins openssh-server_config ca-certificates_data \ + libpam-modules_libs \ + apt_apt-get apt_config apt_var apt_default-source-list dpkg_bins \ + curl_bins wget_bins git_bins git_http-support jq_bins file_bins \ + sudo_bins sudo_config iproute2_bins + + cd "$CRAFT_PART_INSTALL" + + grep -q '^sshd:' etc/passwd || \ + echo 'sshd:x:101:65534::/run/sshd:/usr/sbin/nologin' >> etc/passwd + hash=$(openssl passwd -6 bread) + { + printf 'root:%s:20000:0:99999:7:::\n' "$hash" + printf 'sshd:!:20000:0:99999:7:::\n' + } > etc/shadow + chmod 0640 etc/shadow + + mkdir -p etc/ssh + cat > etc/ssh/sshd_config <<'EOF' + PermitRootLogin yes + PasswordAuthentication yes + UsePAM yes + Subsystem sftp /usr/lib/openssh/sftp-server + PidFile /run/sshd.pid + EOF + + mkdir -p etc/pam.d + 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 + + cat > etc/profile <<'EOF' + if [ -d /etc/profile.d ]; then + for __pf in /etc/profile.d/*.sh; do + [ -r "$__pf" ] && . "$__pf" + done + unset __pf + fi + EOF + + install -D -m0755 "$CRAFT_PROJECT_DIR/hack/sshd-entry.sh" usr/local/bin/sshd-entry.sh + install -D -m0755 "$CRAFT_PROJECT_DIR/hack/lazy-apt.sh" usr/local/bin/apt-get + install -m0755 "$CRAFT_PROJECT_DIR/hack/lazy-apt.sh" usr/local/bin/apt + install -D -m0644 "$CRAFT_PROJECT_DIR/hack/banner.txt" etc/bread-banner.txt + install -D -m0644 "$CRAFT_PROJECT_DIR/hack/bread-warning.sh" etc/profile.d/00-bread-warning.sh + mkdir -p root + printf '\n# bread-warning\n[ -r /etc/profile.d/00-bread-warning.sh ] && . /etc/profile.d/00-bread-warning.sh\n' \ + >> root/.bashrc + + # --- chisel + chisel-hacked, built from source (go is build-only) -------- + chisel: + plugin: nil + build-snaps: + - go + build-packages: + - git + build-environment: + - CHISEL_REF: v1.4.1 + override-build: bash "$CRAFT_PROJECT_DIR/hack/chisel_override_build.sh" + + # --- docker static CLI --------------------------------------------------- + docker: + plugin: nil + build-packages: + - curl + - ca-certificates + build-environment: + - DOCKER_VERSION: 29.5.2 + override-build: bash "$CRAFT_PROJECT_DIR/hack/docker_override_build.sh" diff --git a/rocks/bread-chisel-releases-26.04/rockcraft.yaml b/rocks/bread-chisel-releases-26.04/rockcraft.yaml new file mode 100644 index 0000000..741baae --- /dev/null +++ b/rocks/bread-chisel-releases-26.04/rockcraft.yaml @@ -0,0 +1,132 @@ +# spellchecker: ignore rockcraft chiselled coreutils passwd nologin sshd pebble chisel CHISEL GOTOOLCHAIN noninteractive + +name: bread-chisel-releases +version: "26.04" +summary: chiselled ubuntu 26.04 + sshd + chisel + tooling spread test-host rock +description: | + Throwaway spread test-host for chisel-releases spread tests: a chiselled + (base: bare) Ubuntu 26.04 rock with bash, coreutils, openssh-server, apt, a + pam_permit-only auth stack, plus chisel (+ chisel-hacked), docker, and the + tooling chisel-releases tests typically need (curl, wget, git, jq, file, + sudo, ip). + + INSECURE BY DESIGN. Built for ephemeral spread test containers only; never + expose to a network or use in production. + +base: bare +build-base: ubuntu@26.04 + +platforms: + amd64: + arm64: + +services: + sshd: + override: replace + command: /usr/local/bin/sshd-entry.sh + startup: enabled + +parts: + # --- de-novo copy of the bread base (rocks can't build from rocks), with + # the chisel-releases tooling slices folded into the same single cut. ------ + bread: + plugin: nil + build-snaps: + - chisel + build-packages: + - git + override-build: | + set -eu + + releases=$(mktemp -d) + git clone --depth 1 --branch ubuntu-26.04 \ + https://github.com/canonical/chisel-releases.git "$releases" + + # OpenSSH >=9.8 (this release) re-execs sshd-session + sshd-auth per + # connection; the openssh-server slice ships only /usr/sbin/sshd, so sshd + # exits 255 at startup. Add them to the cloned slice before cutting. + # (24.04's OpenSSH 9.6 predates the split and has no such binaries.) + sed -i \ + -e '\#^ /usr/sbin/sshd:#a\ /usr/lib/openssh/sshd-session:' \ + -e '\#^ /usr/sbin/sshd:#a\ /usr/lib/openssh/sshd-auth:' \ + "$releases/slices/openssh-server.yaml" + + # bread base slices + chisel-releases tooling slices, one cut. + # holes: tree + skopeo have no slice yet -> omitted. + bash "$CRAFT_PROJECT_DIR/hack/chisel_cut.sh" "$releases" \ + base-files_base base-files_etc base-files_var base-files_tmp \ + base-files_lib base-files_home base-files_release-info base-files_chisel \ + bash_bins coreutils_bins tar_bins gzip_bins \ + base-passwd_data login_bins passwd_bins \ + openssh-server_bins openssh-server_config ca-certificates_data \ + libpam-modules_libs \ + apt_apt-get apt_config apt_var apt_default-source-list dpkg_bins \ + curl_bins wget_bins git_bins git_http-support jq_bins file_bins \ + sudo_bins sudo_config iproute2_bins + + cd "$CRAFT_PART_INSTALL" + + grep -q '^sshd:' etc/passwd || \ + echo 'sshd:x:101:65534::/run/sshd:/usr/sbin/nologin' >> etc/passwd + hash=$(openssl passwd -6 bread) + { + printf 'root:%s:20000:0:99999:7:::\n' "$hash" + printf 'sshd:!:20000:0:99999:7:::\n' + } > etc/shadow + chmod 0640 etc/shadow + + mkdir -p etc/ssh + cat > etc/ssh/sshd_config <<'EOF' + PermitRootLogin yes + PasswordAuthentication yes + UsePAM yes + Subsystem sftp /usr/lib/openssh/sftp-server + PidFile /run/sshd.pid + EOF + + mkdir -p etc/pam.d + 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 + + cat > etc/profile <<'EOF' + if [ -d /etc/profile.d ]; then + for __pf in /etc/profile.d/*.sh; do + [ -r "$__pf" ] && . "$__pf" + done + unset __pf + fi + EOF + + install -D -m0755 "$CRAFT_PROJECT_DIR/hack/sshd-entry.sh" usr/local/bin/sshd-entry.sh + install -D -m0755 "$CRAFT_PROJECT_DIR/hack/lazy-apt.sh" usr/local/bin/apt-get + install -m0755 "$CRAFT_PROJECT_DIR/hack/lazy-apt.sh" usr/local/bin/apt + install -D -m0644 "$CRAFT_PROJECT_DIR/hack/banner.txt" etc/bread-banner.txt + install -D -m0644 "$CRAFT_PROJECT_DIR/hack/bread-warning.sh" etc/profile.d/00-bread-warning.sh + mkdir -p root + printf '\n# bread-warning\n[ -r /etc/profile.d/00-bread-warning.sh ] && . /etc/profile.d/00-bread-warning.sh\n' \ + >> root/.bashrc + + # --- chisel + chisel-hacked, built from source (go is build-only) -------- + chisel: + plugin: nil + build-snaps: + - go + build-packages: + - git + build-environment: + - CHISEL_REF: v1.4.1 + override-build: bash "$CRAFT_PROJECT_DIR/hack/chisel_override_build.sh" + + # --- docker static CLI --------------------------------------------------- + docker: + plugin: nil + build-packages: + - curl + - ca-certificates + build-environment: + - DOCKER_VERSION: 29.5.2 + override-build: bash "$CRAFT_PROJECT_DIR/hack/docker_override_build.sh" diff --git a/rocks/hack/chisel_cut.sh b/rocks/hack/chisel_cut.sh new file mode 100755 index 0000000..db2af91 --- /dev/null +++ b/rocks/hack/chisel_cut.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Wrapper around `chisel cut` with retry on known-flaky archive +# errors. Patterns lifted from canonical/chisel-releases' +# .github/scripts/install-slices/install_slices.py. +# +# Usage: chisel_cut.sh [ ...] +# Requires: +# - to be a chisel-releases checkout +# - $CRAFT_PART_INSTALL to be set (rockcraft env) +set -e + +N_RETRIES=10 +RETRY_DELAY=60 +PATTERNS=( + # https://github.com/canonical/chisel-releases/issues/765 + "cannot fetch from archive" + # https://github.com/canonical/chisel-releases/issues/766 + "cannot talk to archive" + # https://github.com/canonical/chisel-releases/issues/768 + "cannot find archive data" + # transient digest mismatch (chisel bug) + "expected digest" +) + +[ "$#" -ge 2 ] || { echo "usage: $0 [ ...]" >&2; exit 1; } +[ -n "${CRAFT_PART_INSTALL:-}" ] || { echo "CRAFT_PART_INSTALL unset" >&2; exit 1; } + +release_dir=$1 +shift + +for attempt in $(seq 1 "$N_RETRIES"); do + err=$(chisel cut \ + --release "$release_dir" \ + --root "$CRAFT_PART_INSTALL" \ + "$@" 2>&1) && rc=0 || rc=$? + if [ "$rc" -eq 0 ]; then + printf '%s\n' "$err" + exit 0 + fi + + matched="" + matched_line="" + for p in "${PATTERNS[@]}"; do + line=$(printf '%s\n' "$err" | grep -F "$p" | head -n1) || true + if [ -n "$line" ]; then matched=$p; matched_line=$line; break; fi + done + + if [ -n "$matched" ] && [ "$attempt" -lt "$N_RETRIES" ]; then + echo "chisel cut failed (attempt $attempt/$N_RETRIES): $matched. Retrying in ${RETRY_DELAY}s..." >&2 + echo " > $matched_line" >&2 + sleep "$RETRY_DELAY" + continue + fi + + printf '%s\n' "$err" >&2 + exit "$rc" +done diff --git a/rocks/hack/chisel_override_build.sh b/rocks/hack/chisel_override_build.sh new file mode 100755 index 0000000..d8432fb --- /dev/null +++ b/rocks/hack/chisel_override_build.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Build chisel + chisel-hacked from source (canonical/chisel @ $CHISEL_REF), +# native arch, into $CRAFT_PART_INSTALL/usr/local/bin. Mirrors the root repo's +# hack/build_binaries.sh, minus the cross-compile -- rockcraft builds natively +# per platform so GOARCH is just the host arch. +# +# Env (from the part's build-environment): +# CHISEL_REF git ref for canonical/chisel +set -euo pipefail + +: "${CHISEL_REF:?required}" + +# go.mod may demand a newer toolchain than the snap ships; let go fetch it. +export GOTOOLCHAIN=auto + +out="$CRAFT_PART_INSTALL/usr/local/bin" +mkdir -p "$out" + +src="$CRAFT_PART_BUILD/chisel-src" +git clone https://github.com/canonical/chisel "$src" +cd "$src" +git checkout "$CHISEL_REF" + +# Inject the version string the way upstream's cmd/mkversion.sh would, so +# `chisel --version` reports the pinned tag instead of "unknown". +ver=$(git describe --tags --dirty --always) +echo "==> chisel version: $ver" + +CGO_ENABLED=0 go build -trimpath \ + -ldflags "-s -w -X github.com/canonical/chisel/cmd.Version=$ver" \ + -o "$out/chisel" ./cmd/chisel + +# chisel-hacked: same source + the CHISEL_HACKS patches, -hacked version suffix. +for p in "$CRAFT_PROJECT_DIR"/patches/chisel/0001-*.patch \ + "$CRAFT_PROJECT_DIR"/patches/chisel/0002-*.patch \ + "$CRAFT_PROJECT_DIR"/patches/chisel/0003-*.patch; do + echo "==> applying $(basename "$p")" + git apply "$p" +done + +CGO_ENABLED=0 go build -trimpath \ + -ldflags "-s -w -X github.com/canonical/chisel/cmd.Version=${ver}-hacked" \ + -o "$out/chisel-hacked" ./cmd/chisel diff --git a/rocks/hack/docker_override_build.sh b/rocks/hack/docker_override_build.sh new file mode 100755 index 0000000..5e40e1f --- /dev/null +++ b/rocks/hack/docker_override_build.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Fetch the upstream docker static CLI for the build-for arch and drop it at +# /usr/local/bin/docker. Upstream's static build uses a recent go toolchain and +# so dodges the qemu emulation bug that hits ubuntu apt's docker.io (go 1.24). +# +# Env (from the part's build-environment): +# DOCKER_VERSION docker static release version +set -euo pipefail + +: "${DOCKER_VERSION:?required}" + +# docker.com static uses uname-style arch names. +case "$CRAFT_ARCH_BUILD_FOR" in + amd64) docker_arch=x86_64 ;; + arm64) docker_arch=aarch64 ;; + *) echo "unsupported arch: $CRAFT_ARCH_BUILD_FOR" >&2; exit 1 ;; +esac + +out="$CRAFT_PART_INSTALL/usr/local/bin" +mkdir -p "$out" + +url="https://download.docker.com/linux/static/stable/${docker_arch}/docker-${DOCKER_VERSION}.tgz" +echo "==> downloading docker $DOCKER_VERSION for $CRAFT_ARCH_BUILD_FOR from $url" + +tmp=$(mktemp -d) +curl -fsSL "$url" | tar -xz -C "$tmp" docker/docker +install -m0755 "$tmp/docker/docker" "$out/docker" +rm -rf "$tmp" diff --git a/rocks/hack/hash_inputs.sh b/rocks/hack/hash_inputs.sh new file mode 100755 index 0000000..a6c2a51 --- /dev/null +++ b/rocks/hack/hash_inputs.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Single sha256 over everything that affects this rock's build: the +# rockcraft.yaml plus all hack/ inputs (and patches/, if present). Drives the +# .rock.stamp short-circuit in the makefile so `make build` only repacks when a +# real input changed. +# +# Stdout: hex digest only. +set -euo pipefail + +cd "$(dirname "$0")/.." + +# shellcheck disable=SC2046 # word-splitting of find output is intended here. +sha256sum \ + rockcraft.yaml \ + $(find hack patches -type f 2>/dev/null | sort) \ + | sha256sum | cut -d' ' -f1 diff --git a/rocks/hack/sshd-entry.sh b/rocks/hack/sshd-entry.sh new file mode 100755 index 0000000..080d94c --- /dev/null +++ b/rocks/hack/sshd-entry.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# pebble service command for sshd in the bread rock. +# +# host keys are generated here on first boot rather than baked into the rock, +# so throwaway test containers don't all share the same keys. also ensures the +# privsep runtime dir exists. then execs sshd in the foreground (pebble manages +# the process). +set -e + +mkdir -p /run/sshd +ssh-keygen -A + +exec /usr/sbin/sshd -D -e diff --git a/rocks/makefile b/rocks/makefile new file mode 100644 index 0000000..80e9184 --- /dev/null +++ b/rocks/makefile @@ -0,0 +1,82 @@ +.DEFAULT_GOAL := help + +DOCKER ?= docker + +# Rock dirs = every subdir with a rockcraft.yaml (tests/ has none). +DIRS := $(patsubst %/rockcraft.yaml,%,$(wildcard */rockcraft.yaml)) + +# Native arch for the default `test` filter (uname -m -> docker/spread arch). +# Accept both linux (x86_64/aarch64) and macos/docker (amd64/arm64) spellings. +UNAME_M := $(shell uname -m) +ifeq ($(filter $(UNAME_M),x86_64 amd64),$(UNAME_M)) + NATIVE_ARCH := amd64 +else ifeq ($(filter $(UNAME_M),aarch64 arm64),$(UNAME_M)) + NATIVE_ARCH := arm64 +else + $(error unsupported host arch: $(UNAME_M)) +endif + +# Default spread filter: every rock dir on the native arch. +SPREAD_FILTER ?= $(addprefix rock:,$(addsuffix -$(NATIVE_ARCH),$(DIRS))) + +.PHONY: help +help: ## Show this help + @grep -E '^[a-zA-Z_-]+%?:.*?## .*$$' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-18s\033[0m %s\n", $$1, $$2}' + @echo "" + @echo "rock dirs: $(DIRS)" + +# Pack one rock dir, e.g. `make build-bread-24.04`. Syncs the shared +# hack/patches first, then short-circuits on a stamp of all build inputs so it +# only repacks on a real change. rockcraft names the artefact +# __.rock (underscores); the predictable .rock has none, +# so `*_*.rock` only ever matches the freshly-packed file. +.PHONY: build-% +build-%: ## Pack one rock (make build-) + @ruby sync_shared.rb $* + @cd $* && \ + new=$$(hack/hash_inputs.sh) ; cur=$$(cat .rock.stamp 2>/dev/null || true) ; \ + if [ "$$new" = "$$cur" ] && [ -f $*.rock ]; then \ + echo "==> $* up-to-date" ; \ + else \ + echo "==> packing $*" ; \ + rockcraft pack --verbosity=brief && mv *_*.rock $*.rock && echo "$$new" > .rock.stamp ; \ + fi + +.PHONY: build-all +build-all: $(addprefix build-,$(DIRS)) ## Pack every rock (narrow via DIRS="bread-24.04 ...") + +.PHONY: load +load: ## Load every built rock into the local daemon as spread-rock-:latest + @for d in $(DIRS); do \ + f="$$d/$$d.rock" ; \ + [ -f "$$f" ] || { echo "skip $$d (no $$f)"; continue; } ; \ + echo "==> load $$f -> spread-rock-$$d:latest" ; \ + skopeo --insecure-policy copy "oci-archive:$$f" "docker-daemon:spread-rock-$$d:latest" ; \ + done + +.PHONY: test +test: build-all load ## Build + load all rocks, then run the rock suite (native arch) + cd ../tests && spread $(SPREAD_FILTER) + +.PHONY: spread-only +spread-only: load ## Load prebuilt rocks + run the rock suite, no packing (for ci where rocks are downloaded) + cd ../tests && spread $(SPREAD_FILTER) + +.PHONY: size +size: ## Show packed rock sizes + @for d in $(DIRS); do [ -f $$d/$$d.rock ] && printf '%-32s %s\n' "$$d" "$$(du -h $$d/$$d.rock | cut -f1)" || echo "$$d (not built)" ; done + +.PHONY: ls-rock-% +ls-rock-%: ## List a rock's filesystem (make ls-rock-) + @tag=ls-$*-$$$$ ; \ + skopeo --insecure-policy copy oci-archive:$*/$*.rock docker-daemon:$$tag:latest > /dev/null ; \ + cid=$$($(DOCKER) create $$tag:latest) ; \ + $(DOCKER) export $$cid | tar tv ; \ + $(DOCKER) rm $$cid > /dev/null ; \ + $(DOCKER) rmi $$tag:latest > /dev/null + +.PHONY: clean +clean: ## Remove packed rocks, stamps, synced hack/patches + loaded tags + @for d in $(DIRS); do rm -f $$d/$$d.rock $$d/.rock.stamp ; rm -rf $$d/hack $$d/patches ; done + -@for d in $(DIRS); do $(DOCKER) rmi -f "spread-rock-$$d:latest" 2>/dev/null ; done diff --git a/rocks/sync_shared.rb b/rocks/sync_shared.rb new file mode 100644 index 0000000..e3cffef --- /dev/null +++ b/rocks/sync_shared.rb @@ -0,0 +1,52 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Populate a rock dir's hack/ (and patches/ for the chisel-releases flavour) +# from the shared sources, so each rock dir is hand-maintained in one place. +# +# rockcraft only copies the project dir (the one with rockcraft.yaml) into the +# build instance, so each rock needs its own physical copy of the shared files +# at pack time. The per-dir copies are gitignored and regenerated from here. +# +# Sources: +# - repo-root hack/ + patches/chisel -- shared with the docker-image build, +# sourced from there rather than duplicated. +# - rocks/hack -- rock-specific helpers (incl. a rock-flavoured hash_inputs.sh +# that differs from the root one). +# +# Usage: ruby sync_shared.rb + +require "fileutils" + +ROOT = __dir__ +REPO_ROOT = File.expand_path("..", ROOT) + +# Identical to the repo-root copies -> single source there. +ROOT_HACK = %w[banner.txt bread-warning.sh lazy-apt.sh].freeze +# Rock-specific (hash_inputs.sh here differs from the root one; the rest are +# rock-only). The chisel-releases flavour also builds chisel + docker from +# source, so it gets those two extra build scripts + the patches. +ROCK_HACK = %w[sshd-entry.sh hash_inputs.sh chisel_cut.sh].freeze +CR_ONLY = %w[chisel_override_build.sh docker_override_build.sh].freeze + +dir = ARGV[0] or abort "usage: ruby sync_shared.rb " +dest = File.join(ROOT, dir) +abort "no such rock dir: #{dir}" unless File.directory?(dest) + +cr = dir.include?("chisel-releases") + +FileUtils.mkdir_p(File.join(dest, "hack")) +ROOT_HACK.each do |f| + FileUtils.cp(File.join(REPO_ROOT, "hack", f), File.join(dest, "hack", f), preserve: true) +end +(ROCK_HACK + (cr ? CR_ONLY : [])).each do |f| + FileUtils.cp(File.join(ROOT, "hack", f), File.join(dest, "hack", f), preserve: true) +end + +if cr + chisel = File.join(dest, "patches", "chisel") + FileUtils.mkdir_p(chisel) + Dir[File.join(REPO_ROOT, "patches", "chisel", "*.patch")].each do |p| + FileUtils.cp(p, File.join(chisel, File.basename(p)), preserve: true) + end +end diff --git a/tests/contract-rocks/run/task.yaml b/tests/contract-rocks/run/task.yaml new file mode 100644 index 0000000..168f7e8 --- /dev/null +++ b/tests/contract-rocks/run/task.yaml @@ -0,0 +1,67 @@ +summary: rock contract -- version/arch + bread bits, plus chisel/tooling for the cr flavour + +execute: | + set -e + + # grep is NOT in the rock (coreutils_bins doesn't ship it), so this task + # uses only shell builtins + coreutils -- no grep/sed. + name="$SPREAD_SYSTEM" + arch="${name##*-}" + rest="${name%-*}" + expected_ver="${rest##*-}" + flavour="${rest%-*}" + + # --- ubuntu version (from base-files_release-info) matches the system name. + actual_ver=$(. /etc/os-release && echo "$VERSION_ID") + if [ "$expected_ver" != "$actual_ver" ]; then + echo "ver mismatch: expected=$expected_ver got=$actual_ver" + exit 1 + fi + + # --- arch matches (uname -m -> docker arch name). + case "$(uname -m)" in + x86_64) actual_arch=amd64 ;; + aarch64) actual_arch=arm64 ;; + *) actual_arch=$(uname -m) ;; + esac + if [ "$arch" != "$actual_arch" ]; then + echo "arch mismatch: expected=$arch got=$actual_arch" + exit 1 + fi + + # --- shell + core utils present. + command -v bash >/dev/null || { echo "missing bash"; exit 1; } + command -v ls >/dev/null || { echo "missing coreutils (ls)"; exit 1; } + + # --- bread-warning banner: files installed + the script prints when sourced. + # spread's (non-tty) session shell already sourced /etc/profile -> profile.d, + # whose silent path exports BREAD_BANNER_SHOWN=1 -- so clear it here, else the + # dedupe guard makes the re-source a no-op. + test -r /etc/profile.d/00-bread-warning.sh || { + echo "missing /etc/profile.d/00-bread-warning.sh"; exit 1; } + test -r /etc/bread-banner.txt || { echo "missing /etc/bread-banner.txt"; exit 1; } + out=$(BREAD_BANNER_SHOWN= BREAD_BANNER_FORCE=1 bash -c '. /etc/profile.d/00-bread-warning.sh' 2>&1) + case "$out" in + *"INSECURE TEST IMAGE"*) : ;; + *) echo "banner did not print when sourced"; echo "--- $out ---"; exit 1 ;; + esac + + # --- apt: update resolves, and the lazy-apt shim self-replaces with a symlink. + apt-get update + [ -L /usr/local/bin/apt-get ] || { echo "lazy-apt shim did not self-replace"; exit 1; } + + # --- chisel-releases flavour extras. + case "$flavour" in + *chisel*) + cv=$(chisel --version 2>&1) + case "$cv" in *v1.4.1*) : ;; *) echo "chisel ver wrong: $cv"; exit 1 ;; esac + chv=$(chisel-hacked --version 2>&1) + case "$chv" in *v1.4.1-hacked*) : ;; *) echo "chisel-hacked ver wrong: $chv"; exit 1 ;; esac + # tree + skopeo are holes (no slice yet) -> not checked. + for cmd in curl wget git jq file sudo ip docker; do + command -v "$cmd" >/dev/null || { echo "missing command: $cmd"; exit 1; } + done + ;; + esac + + echo "ok: $SPREAD_SYSTEM" diff --git a/tests/rock-allocate.sh b/tests/rock-allocate.sh new file mode 100755 index 0000000..20909dc --- /dev/null +++ b/tests/rock-allocate.sh @@ -0,0 +1,66 @@ +#spellchecker: ignore noninteractive + +set -e + +# System name encodes flavour, version + arch, e.g. +# bread-24.04-amd64 -> dir=bread-24.04 +# bread-chisel-releases-26.04-arm64 -> dir=bread-chisel-releases-26.04 +# Parse from the right so the hyphens in bread-chisel-releases don't trip us up. +name="$SPREAD_SYSTEM" +arch="${name##*-}" +rest="${name%-*}" +ver="${rest##*-}" +dir="${rest%-*}-$ver" + +# The rock is loaded into the local daemon (by `make load` / the ci test job) +# under this tag before spread runs -- so allocate just `docker run`s it. +tag="spread-rock-$dir:latest" +echo "system: $name -> image: $tag" + +# Unique container name per worker (flock counter pattern, cf. charmcraft). +sleep 0.$RANDOM +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="${name}-${instance_num}" +echo "container: $container_name" + +docker rm -f "$container_name" 2>/dev/null || true + +# The rock runs pebble as pid 1, which starts the enabled sshd service. +docker run \ + --rm \ + --platform "linux/$arch" \ + --name "$container_name" \ + -d "$tag" + +ip=$(docker inspect "$container_name" --format '{{.NetworkSettings.Networks.bridge.IPAddress}}') + +# Bounded wait for sshd to actually accept TCP on :22 (pgrep is not enough -- a +# process can exist without listening). On timeout, dump pebble/sshd output so +# ci shows WHY sshd isn't listening, then fail fast instead of letting spread +# spin to the kill-timeout. +ready= +for _ in $(seq 1 60); do + if timeout 1 bash -c "echo > /dev/tcp/$ip/22" 2>/dev/null; then + ready=1 + break + fi + sleep 1 +done + +if [ -z "$ready" ]; then + echo "ERROR: sshd not listening on $container_name ($ip:22) after 60s" >&2 + echo "--- docker logs $container_name ---" >&2 + docker logs "$container_name" >&2 2>&1 || true + echo "--- pebble services + sshd -t + sshd bits ---" >&2 + docker exec "$container_name" sh -c 'pgrep -a sshd; /usr/sbin/sshd -t; ls -l /usr/sbin/sshd /usr/lib/openssh /run/sshd /etc/ssh' >&2 2>&1 || true + docker rm -f "$container_name" >/dev/null 2>&1 || true + exit 1 +fi + +ADDRESS "$ip" diff --git a/tests/rock-discard.sh b/tests/rock-discard.sh new file mode 100755 index 0000000..d9342b9 --- /dev/null +++ b/tests/rock-discard.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +set -e + +echo "discarding container for system: $SPREAD_SYSTEM_ADDRESS" + +# Can't filter by IP directly; find the container by inspecting all of them. +container_name="" +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 + +if [ -n "$container_name" ]; then + echo "removing container: $container_name" + docker rm -f "$container_name" 2>/dev/null || true +else + echo "no container found with IP address: $SPREAD_SYSTEM_ADDRESS" + exit 1 +fi diff --git a/tests/spread.yaml b/tests/spread.yaml index 587b189..4ac1505 100644 --- a/tests/spread.yaml +++ b/tests/spread.yaml @@ -93,9 +93,37 @@ backends: password: bread workers: 2 + # The rock suite (contract-rocks) runs against this backend instead: it + # docker-runs the loaded rock directly (the rock is its own sshd host), no + # bread-test outer container. Coupled into this one spread.yaml but kept a + # separate backend + suite; the docker / rocks ci workflows each filter to + # their own backend (outer: vs rock:). + rock: + type: adhoc + allocate: source rock-allocate.sh + discard: source rock-discard.sh + systems: + - bread-24.04-amd64: {username: root, password: bread} + - bread-24.04-arm64: {username: root, password: bread} + - bread-25.10-amd64: {username: root, password: bread} + - bread-25.10-arm64: {username: root, password: bread} + - bread-26.04-amd64: {username: root, password: bread} + - bread-26.04-arm64: {username: root, password: bread} + - bread-chisel-releases-24.04-amd64: {username: root, password: bread} + - bread-chisel-releases-24.04-arm64: {username: root, password: bread} + - bread-chisel-releases-25.10-amd64: {username: root, password: bread} + - bread-chisel-releases-25.10-arm64: {username: root, password: bread} + - bread-chisel-releases-26.04-amd64: {username: root, password: bread} + - bread-chisel-releases-26.04-arm64: {username: root, password: bread} + suites: + # spread has no wildcards in `systems:`, so scope each suite by explicit + # name: the bread-image suites whitelist the outer systems; the rock suite + # excludes them (leaving the rock backend's systems). this keeps a bare + # `spread` from cross-running a suite on the wrong backend. contract-bread/: summary: bread image contract -- exercises inlined/bread-.yaml + systems: [ubuntu-26.04-amd64, ubuntu-26.04-arm64] # task.yaml here will run inner spread against each bread inlined yaml. # Variants below pick which version to exercise. environment: @@ -106,8 +134,13 @@ suites: contract-bread-chisel-releases/: summary: bread-chisel-releases image contract + systems: [ubuntu-26.04-amd64, ubuntu-26.04-arm64] environment: INLINED/v24_04: $REPO/inlined/bread-chisel-releases-24.04.yaml INLINED/v25_10: $REPO/inlined/bread-chisel-releases-25.10.yaml INLINED/v26_04: $REPO/inlined/bread-chisel-releases-26.04.yaml INLINED/v26_10: $REPO/inlined/bread-chisel-releases-26.10.yaml + + contract-rocks/: + summary: rock contract -- docker-runs each loaded rock + checks it + systems: [-ubuntu-26.04-amd64, -ubuntu-26.04-arm64]