Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ BREAD_NET=publish spread # force port-publishing (e.g. to test the macOS path
BREAD_NET=bridge spread # force bridge IPs
```

### tar under emulation (amd64 on apple silicon)

ubuntu 26.04's patched GNU tar (`1.35+dfsg-4ubuntu0.x`) resolves extraction paths through a syscall Docker Desktop's Rosetta emulation does not implement, so in an amd64 container on apple silicon every archive entry below the top level fails with `Function not implemented`. spread hits this when it unpacks the project, reports `cannot send project content`, and after three tries gives up with `Cannot allocate ... after too many retries`. 26.10 and everything at or below 25.10 are unaffected, as are the qemu-emulated arches.

the images work around it: `/bin/tar` is a shim that probes GNU tar once per container and, if it is broken, routes *extraction* to `bsdtar` (see `hack/tar-shim.sh`). creation stays on GNU tar, which spread needs for `--sort=name` when it packs artifacts. on unaffected hosts the shim is inert.

## install spread

prefer a precompiled spread CLI over `go install`? same release ships statically-linked binaries for linux amd64 / arm64 / s390x / ppc64le:
Expand Down Expand Up @@ -93,6 +99,7 @@ spread-bread/
hash_inputs.sh # per-image input hash (drives stamp invalidation)
check_base.sh # detect upstream ubuntu base digest drift; rewrite @sha256 pins
inline_scripts.rb # splice scripts/*.sh into yaml templates
tar-shim.sh # image /bin/tar; routes extraction to bsdtar where gnu tar is broken
scripts/ # allocate / discard scripts, one pair per flavour
images/ # one Dockerfile per (flavour, ubuntu version)
templates/ # yaml templates with `source scripts/...` markers
Expand Down
69 changes: 49 additions & 20 deletions demo/spread.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ exclude:
- .git
- .github

# macOS tar packs an AppleDouble `._*` sidecar next to every file; they unpack as
# real files on the remote. `exclude:` can't drop them (tar synthesises them below
# its glob filter), so extract and repack with COPYFILE_DISABLE=1.
# macOS tar packs an AppleDouble `._*` sidecar next to every file and stores
# xattrs as pax headers; the sidecars unpack as real files on the remote and GNU
# tar warns about every xattr header. `exclude:` can't drop them (tar synthesises
# them below its glob filter), so extract and repack without either.
repack: |
if [ "$(uname -s)" = Darwin ]; then
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
tar -xf - -C "$tmp" <&3
( cd "$tmp" && shopt -s dotglob && COPYFILE_DISABLE=1 tar -cf - -- * ) >&4
( cd "$tmp" && COPYFILE_DISABLE=1 tar --no-mac-metadata --no-xattrs -cf - . ) >&4
else
cat <&3 >&4
fi
Expand Down Expand Up @@ -61,7 +62,9 @@ backends:
# 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"
# Kept out of the project tree: whatever lives there is packed up
# and shipped to every container.
export counter_file="${TMPDIR:-/tmp}/spread-bread-worker-num"
instance_num=$(
flock -x $counter_file bash -c '
[ -s $counter_file ] || echo 0 > $counter_file
Expand All @@ -84,10 +87,42 @@ backends:
-e "usr=$SPREAD_SYSTEM_USERNAME" \
-e "pass=$SPREAD_SYSTEM_PASSWORD" \
$publish_flag \
--label spread-bread \
--name "$container_name" \
-d "$image"

until docker exec "$container_name" pgrep sshd; do sleep 1; done
# sshd is up within a second or two. The bound and the liveness check are
# backstops: a container that dies on start would otherwise spin here
# until spread's own timeout, which reports nothing about the container.
sshd_up=""
for ((i = 0; i < 60; i++)); do
if docker exec "$container_name" pgrep -x sshd >/dev/null 2>&1; then
sshd_up=1
break
fi
if ! docker inspect "$container_name" --format '{{.State.Status}}' 2>/dev/null | grep -qx running; then
echo "$container_name is not running; reproduce with: docker run --rm --platform linux/$arch $image" >&2
docker logs "$container_name" >&2 2>&1 || true
exit 1
fi
sleep 1
done
if [ -z "$sshd_up" ]; then
echo "sshd did not come up in $container_name after 60s" >&2
docker logs "$container_name" >&2 2>&1 || true
exit 1
fi

# A container whose gnu tar cannot extract falls back to bsdtar (see
# hack/tar-shim.sh); say so rather than swapping the tool silently.
tar_backend=$(docker exec "$container_name" /usr/local/bin/bread-tar-shim --bread-probe 2>/dev/null || echo unknown)
if [ "$tar_backend" = bsdtar ]; then
note="note: gnu tar cannot extract in $container_name (host emulation); using bsdtar"
# spread buffers allocate output and only prints it on failure, so the
# terminal (when there is one) is the only channel a user actually reads.
echo "$note"
{ [ -w /dev/tty ] && echo "$note" > /dev/tty; } 2>/dev/null || true
fi

if [ "$mode" = publish ]; then
port=$(docker port "$container_name" 22 | head -n1 | cut -d: -f2)
Expand All @@ -109,24 +144,18 @@ backends:
container_name=""
case "$SPREAD_SYSTEM_ADDRESS" in
*:*)
# Matched on the ports column, e.g. "127.0.0.1:32768->22/tcp".
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
container_name=$(docker ps -a --filter label=spread-bread --format '{{.Names}} {{.Ports}}' |
awk -v m=":${target_port}->22/" 'index($0, m) { print $1; exit }')
not_found="No container found with published port: $target_port"
;;
*)
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
ids=$(docker ps -a --filter label=spread-bread --filter network=bridge --format '{{.ID}}')
if [ -n "$ids" ]; then
container_name=$(docker inspect $ids --format '{{.Name}} {{.NetworkSettings.Networks.bridge.IPAddress}}' |
awk -v ip="$SPREAD_SYSTEM_ADDRESS" '$2 == ip { print substr($1, 2); exit }')
fi
not_found="No container found with IP address: $SPREAD_SYSTEM_ADDRESS"
;;
esac
Expand Down
1 change: 1 addition & 0 deletions hack/hash_inputs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ case "$flavour" in
"images/Dockerfile.bread-$ver"
"hack/bread-warning.sh"
"hack/banner.txt"
"hack/tar-shim.sh"
)
;;
bread-chisel-releases)
Expand Down
79 changes: 79 additions & 0 deletions hack/tar-shim.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/bin/sh
# tar-shim: routes extraction to bsdtar on hosts where GNU tar cannot do it.
#
# Installed as /usr/bin/tar, with the real binary diverted to
# /usr/bin/tar.distrib. It has to sit on that path rather than earlier in
# PATH because spread invokes /bin/tar by absolute path.
#
# Ubuntu's patched tar (26.04 ships 1.35+dfsg-4ubuntu0.x) resolves extraction
# paths through a syscall Docker Desktop's Rosetta emulation does not
# implement, so in an amd64 container on Apple Silicon every entry below the
# top level fails with ENOSYS -- which spread reports as "cannot send project
# content", then as a failure to allocate the system. bsdtar is unaffected.
#
# Only extraction is routed: bsdtar has no --sort=name, which spread passes
# when packing artifacts. The probe runs once per container, so on unaffected
# hosts every call is plain GNU tar.

_real=/usr/bin/tar.distrib
_checked=/run/bread-tar-checked
_broken=/run/bread-tar-broken

_extracting() {
# Bare mode letters are only valid as the first argument; elsewhere an x
# is just as likely to be an option's value.
case "$1" in
(x*) return 0 ;;
esac
for _a in "$@"; do
case "$_a" in
(--) return 1 ;;
(--extract|--get) return 0 ;;
(--*) ;;
(-*x*) return 0 ;;
esac
done
return 1
}

_gnu_tar_broken() {
[ -e "$_broken" ] && return 0
[ -e "$_checked" ] && return 1

_d=$(mktemp -d) || return 1
_bad=0
mkdir -p "$_d/a/b" && : > "$_d/a/b/f" \
&& "$_real" -cf "$_d/t.tar" -C "$_d" a && rm -rf "$_d/a" \
&& "$_real" -xf "$_d/t.tar" -C "$_d" 2>/dev/null && [ -f "$_d/a/b/f" ] \
|| _bad=1
rm -rf "$_d"

# Markers are a cache; a read-only /run just means every call probes.
: > "$_checked" 2>/dev/null
[ "$_bad" = 0 ] && return 1
: > "$_broken" 2>/dev/null
return 0
}

# Reports which backend extraction would use, and warms the probe cache. The
# allocate scripts call it so the fallback shows up in the spread log instead
# of happening silently.
if [ "$1" = --bread-probe ]; then
if command -v bsdtar >/dev/null 2>&1 && _gnu_tar_broken; then
echo bsdtar
else
echo gnu
fi
exit 0
fi

if _extracting "$@" && command -v bsdtar >/dev/null 2>&1 && _gnu_tar_broken; then
# bsdtar has no bare mode letters, and spread sends with `tar xz`.
case "$1" in
(-*) ;;
(*) _mode="-$1"; shift; set -- "$_mode" "$@" ;;
esac
exec bsdtar --no-xattrs --no-mac-metadata "$@"
fi

exec "$_real" "$@"
9 changes: 8 additions & 1 deletion images/Dockerfile.bread-22.04
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ FROM docker.io/library/ubuntu:22.04@sha256:0e0a0fc6d18feda9db1590da249ac93e8d5ab
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 && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssh-server libarchive-tools && \
mkdir /var/run/sshd || true && \
echo 'root:bread' | chpasswd && \
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config && \
Expand All @@ -33,5 +33,12 @@ 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

# Extraction shim over /bin/tar, inert unless the host's emulation breaks GNU
# tar. See hack/tar-shim.sh.
COPY hack/tar-shim.sh /usr/local/bin/bread-tar-shim
RUN chmod 0755 /usr/local/bin/bread-tar-shim && \
dpkg-divert --local --divert /usr/bin/tar.distrib --rename /usr/bin/tar && \
ln -s /usr/local/bin/bread-tar-shim /usr/bin/tar

EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]
9 changes: 8 additions & 1 deletion images/Dockerfile.bread-24.04
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ FROM docker.io/library/ubuntu:24.04@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0
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 && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssh-server libarchive-tools && \
mkdir /var/run/sshd || true && \
echo 'root:bread' | chpasswd && \
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config && \
Expand All @@ -33,5 +33,12 @@ 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

# Extraction shim over /bin/tar, inert unless the host's emulation breaks GNU
# tar. See hack/tar-shim.sh.
COPY hack/tar-shim.sh /usr/local/bin/bread-tar-shim
RUN chmod 0755 /usr/local/bin/bread-tar-shim && \
dpkg-divert --local --divert /usr/bin/tar.distrib --rename /usr/bin/tar && \
ln -s /usr/local/bin/bread-tar-shim /usr/bin/tar

EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]
9 changes: 8 additions & 1 deletion images/Dockerfile.bread-25.10
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ FROM docker.io/library/ubuntu:25.10@sha256:7cc5e35f6567ee8c66d2abb4aab0fd866669e
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 && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssh-server libarchive-tools && \
mkdir /var/run/sshd || true && \
echo 'root:bread' | chpasswd && \
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config && \
Expand All @@ -33,5 +33,12 @@ 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

# Extraction shim over /bin/tar, inert unless the host's emulation breaks GNU
# tar. See hack/tar-shim.sh.
COPY hack/tar-shim.sh /usr/local/bin/bread-tar-shim
RUN chmod 0755 /usr/local/bin/bread-tar-shim && \
dpkg-divert --local --divert /usr/bin/tar.distrib --rename /usr/bin/tar && \
ln -s /usr/local/bin/bread-tar-shim /usr/bin/tar

EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]
9 changes: 8 additions & 1 deletion images/Dockerfile.bread-26.04
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ FROM docker.io/library/ubuntu:26.04@sha256:3131b4cc82a783df6c9df078f86e01819a135
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 && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssh-server libarchive-tools && \
mkdir /var/run/sshd || true && \
echo 'root:bread' | chpasswd && \
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config && \
Expand All @@ -33,5 +33,12 @@ 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

# Extraction shim over /bin/tar, inert unless the host's emulation breaks GNU
# tar. See hack/tar-shim.sh.
COPY hack/tar-shim.sh /usr/local/bin/bread-tar-shim
RUN chmod 0755 /usr/local/bin/bread-tar-shim && \
dpkg-divert --local --divert /usr/bin/tar.distrib --rename /usr/bin/tar && \
ln -s /usr/local/bin/bread-tar-shim /usr/bin/tar

EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]
9 changes: 8 additions & 1 deletion images/Dockerfile.bread-26.10
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ FROM docker.io/library/ubuntu:26.10@sha256:694b773ee7e0d0b55ca74c095ac3309055589
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 && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssh-server libarchive-tools && \
mkdir /var/run/sshd || true && \
echo 'root:bread' | chpasswd && \
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config && \
Expand All @@ -33,5 +33,12 @@ 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

# Extraction shim over /bin/tar, inert unless the host's emulation breaks GNU
# tar. See hack/tar-shim.sh.
COPY hack/tar-shim.sh /usr/local/bin/bread-tar-shim
RUN chmod 0755 /usr/local/bin/bread-tar-shim && \
dpkg-divert --local --divert /usr/bin/tar.distrib --rename /usr/bin/tar && \
ln -s /usr/local/bin/bread-tar-shim /usr/bin/tar

EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]
Loading