mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-26 21:17:32 +09:00
The shell translator rewrites test bodies and loses Bats reporting. The unit lane needs complete TAP results and JUnit reports without changing how live-cluster suites run. Sourced production helpers still need a dash pass, and unit test bodies need nounset without exporting shell options into subprocesses. Assisted-by: LLM Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
628 lines
31 KiB
Bash
628 lines
31 KiB
Bash
#!/usr/bin/env bats
|
|
# Tests for hack/stitch-multiarch.sh, which joins the amd64 build and the arm64
|
|
# leg of every first-party image into one index and repins the tree on it.
|
|
#
|
|
# The registry is a stub: skopeo and docker on PATH read and write a flat table
|
|
# of "<repo> <tag> <digest>" lines plus one file per manifest, so digests are
|
|
# real sha256 sums of the stored bytes and an index the docker stub creates
|
|
# resolves like any other manifest.
|
|
#
|
|
# Harness note: the unit lane runs this file under Bats. The bodies stay POSIX
|
|
# sh (no `run`, no [[ ]]; each @test a shell function under `set -eu`), so
|
|
# hack/cozytest.sh can still run it under dash by hand.
|
|
#
|
|
# Run with: bats hack/stitch-multiarch_test.bats
|
|
|
|
load test_helper
|
|
|
|
REG=registry.example/cozy
|
|
|
|
_stub_registry() {
|
|
MOCK_REG="$1"
|
|
export MOCK_REG
|
|
mkdir -p "$MOCK_REG/bin" "$MOCK_REG/blobs"
|
|
: >"$MOCK_REG/tags"
|
|
: >"$MOCK_REG/log"
|
|
cat >"$MOCK_REG/bin/skopeo" <<'EOF'
|
|
#!/bin/sh
|
|
set -eu
|
|
printf 'skopeo %s\n' "$*" >>"$MOCK_REG/log"
|
|
resolve() {
|
|
r="${1#docker://}"
|
|
case "$r" in
|
|
*@*) printf '%s' "${r##*@}" ;;
|
|
*) awk -v r="${r%:*}" -v t="${r##*:}" '$1 == r && $2 == t { d = $3 } END { printf "%s", d }' "$MOCK_REG/tags" ;;
|
|
esac
|
|
}
|
|
case "$1" in
|
|
inspect)
|
|
d="$(resolve "$3")"
|
|
f="$MOCK_REG/blobs/${d#sha256:}"
|
|
[ -n "$d" ] && [ -f "$f" ] || { echo "Error: reading manifest $3: manifest unknown" >&2; exit 1; }
|
|
if [ "$2" = --config ]; then jq '{architecture: .arch}' "$f"; else cat "$f"; fi
|
|
;;
|
|
list-tags)
|
|
awk -v r="${2#docker://}" '$1 == r { print $2 }' "$MOCK_REG/tags" | sort -u | jq -R . | jq -s '{Tags: .}'
|
|
;;
|
|
*) exit 2 ;;
|
|
esac
|
|
EOF
|
|
cat >"$MOCK_REG/bin/docker" <<'EOF'
|
|
#!/bin/sh
|
|
set -eu
|
|
printf 'docker %s\n' "$*" >>"$MOCK_REG/log"
|
|
[ "$1 $2 $3" = "buildx imagetools create" ] || exit 2
|
|
shift 3
|
|
# MOCK_CREATE_LIMIT=N: the (N+1)th create fails, as a registry outage would.
|
|
n=$(grep -c 'imagetools create' "$MOCK_REG/log")
|
|
if [ -n "${MOCK_CREATE_LIMIT:-}" ] && [ "$n" -gt "$MOCK_CREATE_LIMIT" ]; then
|
|
echo "ERROR: failed to push: 503 Service Unavailable" >&2
|
|
exit 1
|
|
fi
|
|
tags=""
|
|
entries=""
|
|
srcs=$(printf '%s\n' "$@" | awk 'prev != "--tag" && $0 != "--tag" { print } { prev = $0 }')
|
|
# One source that is already an index is copied as is, like the real tool.
|
|
if [ "$(printf '%s\n' "$srcs" | wc -l | tr -d ' ')" -eq 1 ] \
|
|
&& jq -e '.manifests' "$MOCK_REG/blobs/$(printf '%s' "${srcs##*@}" | sed 's/^sha256://')" >/dev/null 2>&1; then
|
|
while [ "$#" -gt 0 ]; do
|
|
if [ "$1" = --tag ]; then printf '%s %s %s\n' "${2%:*}" "${2##*:}" "${srcs##*@}" >>"$MOCK_REG/tags"; shift; fi
|
|
shift
|
|
done
|
|
exit 0
|
|
fi
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--tag) tags="$tags $2"; shift 2 ;;
|
|
*)
|
|
d="${1##*@}"
|
|
arch="$(jq -r .arch "$MOCK_REG/blobs/${d#sha256:}")"
|
|
entries="$entries${entries:+,}{\"digest\":\"$d\",\"platform\":{\"os\":\"linux\",\"architecture\":\"$arch\"}}"
|
|
shift ;;
|
|
esac
|
|
done
|
|
body="{\"mediaType\":\"application/vnd.oci.image.index.v1+json\",\"manifests\":[$entries]}"
|
|
hex="$(printf '%s' "$body" | sha256sum | cut -d' ' -f1)"
|
|
printf '%s' "$body" >"$MOCK_REG/blobs/$hex"
|
|
for t in $tags; do
|
|
printf '%s %s sha256:%s\n' "${t%:*}" "${t##*:}" "$hex" >>"$MOCK_REG/tags"
|
|
done
|
|
EOF
|
|
chmod +x "$MOCK_REG/bin/skopeo" "$MOCK_REG/bin/docker"
|
|
}
|
|
|
|
# _push <repo> <tag> <arch> [<config-media-type>] stores a single-platform
|
|
# manifest, tags it, and prints its digest.
|
|
_push() {
|
|
body="{\"mediaType\":\"application/vnd.oci.image.manifest.v1+json\",\"config\":{\"mediaType\":\"${4:-application/vnd.oci.image.config.v1+json}\"},\"arch\":\"$3\",\"name\":\"$1:$2\"}"
|
|
hex="$(printf '%s' "$body" | sha256sum | cut -d' ' -f1)"
|
|
printf '%s' "$body" >"$MOCK_REG/blobs/$hex"
|
|
printf '%s %s sha256:%s\n' "$1" "$2" "$hex" >>"$MOCK_REG/tags"
|
|
printf 'sha256:%s' "$hex"
|
|
}
|
|
|
|
_tag() {
|
|
awk -v r="$1" -v t="$2" '$1 == r && $2 == t { d = $3 } END { printf "%s", d }' "$MOCK_REG/tags"
|
|
}
|
|
|
|
# A tree stamped by an amd64 build with IMAGE_TAG=main, one image per storage
|
|
# shape and values.yaml sub-shape, each with an arm64 twin, plus the refs that
|
|
# must be left alone. Sets A_<name> to each amd64 digest.
|
|
_make_world() {
|
|
t="$1/tree"
|
|
mkdir -p "$t/system/api" "$t/system/cil" "$t/system/lin" "$t/system/ovn" \
|
|
"$t/apps/kube/images" "$t/system/multus/templates" "$t/core/testing" \
|
|
"$t/core/installer" "$t/system/capi-providers-cpprovider/images" \
|
|
"$t/system/capi-providers-cpprovider/files" "$t/system/stray/files" "$t/system/zipped/files" \
|
|
"$t/system/third" "$t/system/api/charts/up"
|
|
for n in api cil lin ovn signer multus; do
|
|
eval "A_$n=\$(_push $REG/$n main amd64)"
|
|
_push "$REG/$n" main-arm64 arm64 >/dev/null
|
|
done
|
|
# The build pushed api's component version beside the build tag, though the
|
|
# tree ref names only the build tag, as every package but kamaji stamps it;
|
|
# the pushed-tags log is what records it, and it must move with the build
|
|
# tag. Any other tag on the same bytes belongs to another build and must be
|
|
# neither read nor moved.
|
|
printf '%s v9.9.9 %s\n%s pr-1-abc %s\n%s v0.0.1-rc.1 %s\n' \
|
|
"$REG/api" "$A_api" "$REG/api" "$A_api" "$REG/api" "$A_api" >>"$MOCK_REG/tags"
|
|
PUSHED_TAGS_LOG="$1/pushed-tags"
|
|
export PUSHED_TAGS_LOG
|
|
for n in api cil lin ovn signer multus kamaji; do
|
|
printf '%s/%s:main\n' "$REG" "$n" >>"$PUSHED_TAGS_LOG"
|
|
done
|
|
printf '%s/api:v9.9.9\n' "$REG" >>"$PUSHED_TAGS_LOG"
|
|
A_sandbox=$(_push "$REG/sandbox" main amd64)
|
|
for n in kamaji stray zipped; do
|
|
eval "A_$n=\$(_push $REG/$n main amd64)"
|
|
_push "$REG/$n" main-arm64 arm64 >/dev/null
|
|
done
|
|
A_pkg=$(_push "$REG/cozystack-packages" main amd64 application/vnd.cncf.flux.config.v1+json)
|
|
_push "$REG/cozystack-packages" main-arm64 arm64 application/vnd.cncf.flux.config.v1+json >/dev/null
|
|
THIRD="sha256:$(printf 'f%.0s' $(seq 1 64))"
|
|
|
|
# values.yaml, single string
|
|
printf 'image: %s/api:main@%s\n' "$REG" "$A_api" >"$t/system/api/values.yaml"
|
|
# values.yaml, split map with a digest key
|
|
printf 'image:\n repository: %s/cil\n tag: main\n digest: %s\n' "$REG" "$A_cil" >"$t/system/cil/values.yaml"
|
|
# values.yaml, split map with the digest inside tag
|
|
printf 'image:\n repository: %s/lin\n tag: main@%s\n' "$REG" "$A_lin" >"$t/system/lin/values.yaml"
|
|
# values.yaml, chart-global registry
|
|
printf 'global:\n registry:\n address: %s\n images:\n kubeovn:\n repository: ovn\n tag: main@%s\n' "$REG" "$A_ovn" >"$t/system/ovn/values.yaml"
|
|
# images/*.tag
|
|
# a component-versioned ref: its tag was never pushed by this build, which
|
|
# pushed the image as main like every other
|
|
printf '%s/signer:v7.7.7-cozystack.0@%s\n' "$REG" "$A_signer" >"$t/apps/kube/images/signer.tag"
|
|
# the declared file; the Helm conditional keeps yq from parsing it, as in the
|
|
# real one
|
|
printf '{{- if .Values.x }}\n - image: %s/multus:main@%s\n{{- end }}\n' "$REG" "$A_multus" \
|
|
>"$t/system/multus/templates/multus-daemonset-thick.yml"
|
|
# no arm64 twin
|
|
printf 'image: %s/sandbox:main@%s\n' "$REG" "$A_sandbox" >"$t/core/testing/values.yaml"
|
|
# the chart ships this ref only inside a gzip built from a plain copy, as the
|
|
# package's image target writes it
|
|
cpp="$t/system/capi-providers-cpprovider"
|
|
printf '%s/kamaji:v0.1.0-cozystack.0@%s\n' "$REG" "$A_kamaji" >"$cpp/images/kamaji.tag"
|
|
printf 'kind: Deployment\nimage: %s/kamaji:v0.1.0-cozystack.0@%s\n' "$REG" "$A_kamaji" >"$cpp/files/control-plane-components.yaml"
|
|
gzip -nc "$cpp/files/control-plane-components.yaml" >"$cpp/files/components.gz"
|
|
# the digest also sits in a file, or a gzip, the rewrite does not reach
|
|
printf '%s/stray:main@%s\n' "$REG" "$A_stray" >"$t/apps/kube/images/stray.tag"
|
|
printf 'image: %s/stray:main@%s\n' "$REG" "$A_stray" >"$t/system/stray/files/copy.yaml"
|
|
printf '%s/zipped:main@%s\n' "$REG" "$A_zipped" >"$t/apps/kube/images/zipped.tag"
|
|
printf 'image: %s/zipped:main@%s\n' "$REG" "$A_zipped" | gzip -nc >"$t/system/zipped/files/copy.gz"
|
|
# the packages artifact, and an image the build does not own
|
|
printf 'cozystackOperator:\n platformSourceUrl: oci://%s/cozystack-packages\n platformSourceRef: digest=%s\n' "$REG" "$A_pkg" \
|
|
>"$t/core/installer/values.yaml"
|
|
printf 'image: docker.io/library/busybox:1@%s\n' "$THIRD" >"$t/system/third/values.yaml"
|
|
# a vendored chart pinning the same digest: out of the rewrite and the check
|
|
printf 'image: %s/api:main@%s\n' "$REG" "$A_api" >"$t/system/api/charts/up/values.yaml"
|
|
}
|
|
|
|
@test "every storage shape is repinned on an index of its amd64 and arm64 builds" {
|
|
tmp=$(mktemp -d)
|
|
_stub_registry "$tmp/reg"
|
|
_make_world "$tmp"
|
|
t="$tmp/tree"
|
|
|
|
PATH="$MOCK_REG/bin:$PATH" hack/stitch-multiarch.sh "$t/" "$REG" main >"$tmp/out" 2>"$tmp/err"
|
|
|
|
for n in api cil lin ovn signer multus; do
|
|
eval "a=\$A_$n"
|
|
i="$(_tag "$REG/$n" main)"
|
|
[ "$i" != "$a" ]
|
|
jq -e --arg a "$a" '.mediaType == "application/vnd.oci.image.index.v1+json" and .manifests[0].digest == $a and .manifests[0].platform.architecture == "amd64" and .manifests[1].platform.architecture == "arm64"' \
|
|
"$MOCK_REG/blobs/${i#sha256:}" >/dev/null
|
|
if grep -rq --exclude-dir=charts "$a" "$t"; then echo "FAIL: $n still pinned on its amd64 digest"; false; fi
|
|
grep -rqF "$i" "$t"
|
|
done
|
|
grep -q "^ digest: $(_tag "$REG/cil" main)\$" "$t/system/cil/values.yaml"
|
|
grep -q "^ tag: main@$(_tag "$REG/ovn" main)\$" "$t/system/ovn/values.yaml"
|
|
grep -q "multus:main@$(_tag "$REG/multus" main)\$" "$t/system/multus/templates/multus-daemonset-thick.yml"
|
|
|
|
# the component tag the build pushed moved with the build tag; tags of other
|
|
# builds on the same bytes were never read, let alone moved
|
|
[ "$(_tag "$REG/api" v9.9.9)" = "$(_tag "$REG/api" main)" ]
|
|
[ "$(_tag "$REG/api" pr-1-abc)" = "$A_api" ]
|
|
[ "$(_tag "$REG/api" v0.0.1-rc.1)" = "$A_api" ]
|
|
if grep -q 'list-tags\|api:pr-1-abc\|api:v0.0.1-rc.1' "$MOCK_REG/log"; then echo "FAIL: a tag this build did not push was read"; false; fi
|
|
# vendored charts are neither rewritten nor checked
|
|
grep -q "$A_api" "$t/system/api/charts/up/values.yaml"
|
|
|
|
# the gzip is rewritten with its plain copy, to what the image target builds
|
|
i="$(_tag "$REG/kamaji" main)"
|
|
[ "$i" != "$A_kamaji" ]
|
|
grep -q "kamaji:v0.1.0-cozystack.0@$i\$" "$cpp/images/kamaji.tag"
|
|
grep -q "kamaji:v0.1.0-cozystack.0@$i\$" "$cpp/files/control-plane-components.yaml"
|
|
gzip -dc "$cpp/files/components.gz" | grep -q "kamaji:v0.1.0-cozystack.0@$i\$"
|
|
if gzip -dc "$cpp/files/components.gz" | grep -q "$A_kamaji"; then echo "FAIL: the gzip still pins the amd64 digest"; false; fi
|
|
gzip -nc "$cpp/files/control-plane-components.yaml" | cmp - "$cpp/files/components.gz"
|
|
|
|
# skipped: no arm64 twin, and a digest pinned outside the enumerated files
|
|
[ "$(_tag "$REG/sandbox" main)" = "$A_sandbox" ]
|
|
grep -q "$A_sandbox" "$t/core/testing/values.yaml"
|
|
for n in stray zipped; do
|
|
eval "a=\$A_$n"
|
|
[ "$(_tag "$REG/$n" main)" = "$a" ]
|
|
grep -q "$a" "$t/apps/kube/images/$n.tag"
|
|
done
|
|
grep -q 'skipping sandbox: no main-arm64 image' "$tmp/err"
|
|
grep -q 'skipping stray: digest also pinned in .*/system/stray/files/copy.yaml' "$tmp/err"
|
|
grep -q 'skipping zipped: digest also pinned in .*/system/zipped/files/copy.gz' "$tmp/err"
|
|
if grep -q 'imagetools create.*\(stray\|zipped\|sandbox\|cozystack-packages\)' "$MOCK_REG/log"; then
|
|
echo "FAIL: a skipped ref was pushed"; false
|
|
fi
|
|
|
|
# the packages artifact and third-party refs are untouched
|
|
grep -q "platformSourceRef: digest=$A_pkg" "$t/core/installer/values.yaml"
|
|
grep -q "busybox:1@$THIRD" "$t/system/third/values.yaml"
|
|
|
|
[ "$(wc -l <"$tmp/out" | tr -d ' ')" -eq 1 ]
|
|
grep -q '^stitch: 7 stitched (.*kamaji.*); 3 skipped (.*stray.*zipped.*)$' "$tmp/out"
|
|
grep -q 'skipped (.*sandbox.*)$' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the rewritten gzip is byte-identical across two stitches of one tree" {
|
|
tmp=$(mktemp -d)
|
|
_stub_registry "$tmp/reg"
|
|
_make_world "$tmp"
|
|
cp -R "$tmp/tree" "$tmp/stamped"
|
|
PATH="$MOCK_REG/bin:$PATH" hack/stitch-multiarch.sh "$tmp/tree" "$REG" main >/dev/null 2>&1
|
|
cp "$tmp/tree/system/capi-providers-cpprovider/files/components.gz" "$tmp/first.gz"
|
|
# The second run finds every tag already on its index and repins on it.
|
|
rm -rf "$tmp/tree"
|
|
cp -R "$tmp/stamped" "$tmp/tree"
|
|
sleep 1
|
|
PATH="$MOCK_REG/bin:$PATH" hack/stitch-multiarch.sh "$tmp/tree" "$REG" main >/dev/null 2>&1
|
|
cmp "$tmp/first.gz" "$tmp/tree/system/capi-providers-cpprovider/files/components.gz"
|
|
if cmp -s "$tmp/first.gz" "$tmp/stamped/system/capi-providers-cpprovider/files/components.gz"; then echo "FAIL: the gzip was not rewritten"; false; fi
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a digest left inside the gzip fails the run" {
|
|
# A sed that rewrites files but passes stdin through stands in for a rewrite
|
|
# that misses the gzip payload alone; grep -I would never look inside it.
|
|
tmp=$(mktemp -d)
|
|
_stub_registry "$tmp/reg"
|
|
_make_world "$tmp"
|
|
real_sed="$(command -v sed)"
|
|
printf '#!/bin/sh\ncase "$1" in s\\|*) [ -n "${2:-}" ] || exec cat ;; esac\nexec %s "$@"\n' "$real_sed" >"$MOCK_REG/bin/sed"
|
|
chmod +x "$MOCK_REG/bin/sed"
|
|
rc=0
|
|
PATH="$MOCK_REG/bin:$PATH" hack/stitch-multiarch.sh "$tmp/tree" "$REG" main >"$tmp/out" 2>"$tmp/err" || rc=$?
|
|
[ "$rc" -ne 0 ]
|
|
grep -q "${A_kamaji} is still pinned after the rewrite, in: .*components.gz" "$tmp/err"
|
|
if grep -q "is still pinned after the rewrite, in: .*control-plane-components.yaml" "$tmp/err"; then echo "FAIL: the plain copy was not rewritten"; false; fi
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "prepare-release applies the amd64 build's tree, stitches, gates, then republishes before the commit" {
|
|
# The amd64 build pushed the artifact and chart pinned on amd64-only
|
|
# digests. The chart's values pin the operator image and the digest
|
|
# image-packages writes, so it must be packaged after that write, and the
|
|
# gate must see the stitched tree before anything is published from it.
|
|
wf=.github/workflows/tags.yaml
|
|
names=$(yq -r '.jobs.prepare-release.steps[].name' "$wf")
|
|
d=$(echo "$names" | grep -nx 'Download the amd64 build' | cut -d: -f1)
|
|
a=$(echo "$names" | grep -nx 'Apply the stamped tree' | cut -d: -f1)
|
|
st=$(echo "$names" | grep -nx 'Stitch multi-arch indexes and republish' | cut -d: -f1)
|
|
c=$(echo "$names" | grep -nx 'Commit release artifacts' | cut -d: -f1)
|
|
u=$(echo "$names" | grep -nx 'Upload assets' | cut -d: -f1)
|
|
[ -n "$d" ] && [ -n "$a" ] && [ -n "$st" ] && [ -n "$c" ] && [ -n "$u" ] \
|
|
&& [ "$d" -lt "$a" ] && [ "$a" -lt "$st" ] && [ "$st" -lt "$c" ] && [ "$c" -lt "$u" ] \
|
|
|| { echo "FAIL: prepare-release steps are out of order"; false; }
|
|
run=$(yq -r '.jobs.prepare-release.steps[] | select(.name == "Stitch multi-arch indexes and republish") | .run' "$wf")
|
|
s=$(echo "$run" | grep -n 'hack/stitch-multiarch.sh packages ' | cut -d: -f1)
|
|
v=$(echo "$run" | grep -n 'hack/verify-multiarch.sh packages$' | cut -d: -f1)
|
|
p=$(echo "$run" | grep -n 'make -C packages/core/installer image-packages chart$' | cut -d: -f1)
|
|
[ -n "$s" ] && [ -n "$v" ] && [ -n "$p" ] && [ "$s" -lt "$v" ] && [ "$v" -lt "$p" ] \
|
|
|| { echo "FAIL: stitch, gate and republish are out of order"; false; }
|
|
out=$(make -n -C packages/core/installer image-packages chart COZYSTACK_VERSION=0)
|
|
ref=$(echo "$out" | grep -n 'platformSourceRef = ' | cut -d: -f1)
|
|
pkg=$(echo "$out" | grep -n 'helm package' | cut -d: -f1)
|
|
[ -n "$ref" ] && [ -n "$pkg" ] && [ "$ref" -lt "$pkg" ]
|
|
echo "$out" | grep -q 'helm push'
|
|
}
|
|
|
|
@test "the amd64 build hands its tree, tag log and assets to prepare-release, and a missing hand-off fails" {
|
|
wf=.github/workflows/tags.yaml
|
|
up='.jobs.build-amd64.steps[] | select(.uses // "" | test("actions/upload-artifact@"))'
|
|
down='.jobs.prepare-release.steps[] | select(.uses // "" | test("actions/download-artifact@"))'
|
|
[ "$(yq -r "[$up] | length" "$wf")" -ge 2 ]
|
|
[ "$(yq -r "[$up | select(.with.\"if-no-files-found\" != \"error\")] | length" "$wf")" -eq 0 ]
|
|
# Every artifact uploaded is downloaded under the same name.
|
|
[ "$(yq -r "$up | .with.name" "$wf" | sort)" = "$(yq -r "$down | .with.name" "$wf" | sort)" ]
|
|
# The tag log the Build step writes is the file the stitch reads, and it
|
|
# travels in an uploaded artifact that lands at that same path.
|
|
build_log=$(yq -r '.jobs.build-amd64.steps[] | select(.name == "Build") | .env.PUSHED_TAGS_LOG' "$wf")
|
|
stitch_log=$(yq -r '.jobs.prepare-release.steps[] | select(.name == "Stitch multi-arch indexes and republish") | .env.PUSHED_TAGS_LOG' "$wf")
|
|
[ -n "$build_log" ] && [ "$build_log" != null ] && [ "$build_log" = "$stitch_log" ]
|
|
dir="${build_log%/*}"
|
|
yq -r "$up | .with.path" "$wf" | grep -qxF "$dir/"
|
|
yq -r "$down | .with.path" "$wf" | grep -qxF "$dir"
|
|
# The stamped tree is captured after the build and applied before the stitch.
|
|
capture=$(yq -r '.jobs.build-amd64.steps[] | select(.name == "Capture the stamped tree") | .run' "$wf")
|
|
echo "$capture" | grep -q "git diff --cached --binary HEAD >\"$dir/stamped-tree.patch\""
|
|
yq -r '.jobs.prepare-release.steps[] | select(.name == "Apply the stamped tree") | .run' "$wf" \
|
|
| grep -q "git apply --binary \"$dir/stamped-tree.patch\""
|
|
}
|
|
|
|
@test "the amd64 and arm64 builds run side by side, prepare-release waits for both, and a stable tag runs without arm64" {
|
|
wf=.github/workflows/tags.yaml
|
|
[ "$(yq -r '.jobs.build-amd64.needs // "none"' "$wf")" = none ]
|
|
[ "$(yq -r '.jobs.build-arm64.needs // "none"' "$wf")" = none ]
|
|
[ "$(yq -r '.jobs.build-arm64.if' "$wf")" = "contains(github.ref_name, '-')" ]
|
|
# The amd64 job runs for every tag: it holds the release-exists no-op and the
|
|
# hand-pushed stable tag rejection.
|
|
[ "$(yq -r '.jobs.build-amd64.if // "none"' "$wf")" = none ]
|
|
amd_steps=$(yq -r '.jobs.build-amd64.steps[].name' "$wf")
|
|
echo "$amd_steps" | grep -qx 'Check if release already exists'
|
|
echo "$amd_steps" | grep -qx 'Reject hand-pushed stable tags'
|
|
[ "$(yq -r '.jobs.build-arm64.steps[] | select(.run // "" | test("build-matrix")) | .env.IMAGE_TAG' "$wf")" = '${{ github.ref_name }}-arm64' ]
|
|
[ "$(yq -r '.jobs.prepare-release.needs | sort | join(" ")' "$wf")" = 'build-amd64 build-arm64' ]
|
|
cond=$(yq -r '.jobs.prepare-release.if' "$wf")
|
|
echo "$cond" | grep -qF '!cancelled()'
|
|
echo "$cond" | grep -qF "needs.build-amd64.result == 'success'"
|
|
echo "$cond" | grep -qF "needs.build-arm64.result == 'success'"
|
|
echo "$cond" | grep -qF "needs.build-arm64.result == 'skipped'"
|
|
if echo "$cond" | grep -qE "always\(\)|'failure'"; then echo "FAIL: a failed build must not let the release through"; false; fi
|
|
# prepare-release no-ops exactly when the amd64 job found the release.
|
|
[ "$(yq -r '.jobs.prepare-release.outputs.release_exists' "$wf")" = '${{ needs.build-amd64.outputs.release_exists }}' ]
|
|
# A stable tag skips build-arm64, and a skipped ancestor makes the implicit
|
|
# success() false for every job below it, so each one needs a status
|
|
# function of its own or it silently never runs on a stable tag.
|
|
down=prepare-release
|
|
while :; do
|
|
more=$(yq -r '.jobs | keys | .[]' "$wf" | while read -r j; do
|
|
needs=$(yq -r "[.jobs.\"$j\".needs // []] | flatten | .[]" "$wf")
|
|
for d in $down; do
|
|
if echo "$needs" | grep -qxF "$d" && ! echo " $down " | grep -qF " $j "; then echo "$j"; break; fi
|
|
done
|
|
done | sort -u)
|
|
[ -n "$more" ] || break
|
|
down="$down $(echo "$more" | tr '\n' ' ')"
|
|
done
|
|
for j in $down; do
|
|
yq -r ".jobs.\"$j\".if // \"\"" "$wf" | grep -qE '!cancelled\(\)|always\(\)' \
|
|
|| { echo "FAIL: $j runs below build-arm64 without a status function"; false; }
|
|
done
|
|
echo " $down " | grep -qF ' generate-changelog '
|
|
echo " $down " | grep -qF ' rc-e2e '
|
|
echo " $down " | grep -qF ' update-website-docs '
|
|
# A re-run of an rc whose release is already prepared rebuilds nothing.
|
|
[ "$(yq -r '.jobs.build-arm64.steps[] | select(.name == "Build arm64 images") | .if' "$wf")" = "steps.check_release.outputs.release_exists == 'false'" ]
|
|
[ "$(yq -r '.jobs.build-amd64.steps[] | select(.name == "Build") | .if' "$wf")" = "steps.check_release.outputs.release_exists == 'false'" ]
|
|
# The stitch and the gate need skopeo and mikefarah yq on the release runner.
|
|
tool=$(yq -r '.jobs.prepare-release.steps[] | select(.name == "Set up build toolchain") | .run' "$wf")
|
|
echo "$tool" | grep -q skopeo
|
|
echo "$tool" | grep -q 'yq --version | grep -q mikefarah'
|
|
# prepare-release pushes the packages artifact with flux, so it installs the
|
|
# same flux build-amd64 does rather than whatever is latest.
|
|
want=$(yq -r '.jobs.build-amd64.steps[] | select(.name == "Set up flux") | .with.version' "$wf")
|
|
[ -n "$want" ] && [ "$want" != null ]
|
|
[ "$(yq -r '.jobs.prepare-release.steps[] | select(.name == "Set up flux") | .with.version' "$wf")" = "$want" ]
|
|
}
|
|
|
|
@test "the amd64 release job rebuilds matchbox as a two-arch index before it captures the tree" {
|
|
# matchbox has no arm64 twin to stitch, so this rebuild is the only thing
|
|
# that makes the ref the gate checks an index. It needs a builder that can
|
|
# push an index while make build stays on the default docker driver.
|
|
wf=.github/workflows/tags.yaml
|
|
steps=$(yq -r '.jobs.build-amd64.steps[].name' "$wf")
|
|
pos() { echo "$steps" | grep -nxF "$1" | cut -d: -f1; }
|
|
bx=$(pos 'Set up Buildx for matchbox (docker-container driver)')
|
|
build=$(pos 'Build')
|
|
capture=$(pos 'Capture the stamped tree')
|
|
[ -n "$bx" ] && [ -n "$build" ] && [ -n "$capture" ]
|
|
[ "$bx" -lt "$build" ] && [ "$build" -lt "$capture" ]
|
|
sel='.jobs.build-amd64.steps[] | select(.id == "matchbox_buildx")'
|
|
[ "$(yq -r "$sel | .with.driver" "$wf")" = docker-container ]
|
|
[ "$(yq -r "$sel | .with.use" "$wf")" = false ]
|
|
run_=$(yq -r '.jobs.build-amd64.steps[] | select(.name == "Build") | .run' "$wf")
|
|
echo "$run_" | grep -qxE '[[:space:]]*make -C packages/core/talos image-matchbox PLATFORM=linux/amd64,linux/arm64 BUILDER="\$MATCHBOX_BUILDER"'
|
|
[ "$(yq -r '.jobs.build-amd64.steps[] | select(.name == "Build") | .env.MATCHBOX_BUILDER' "$wf")" = '${{ steps.matchbox_buildx.outputs.name }}' ]
|
|
}
|
|
|
|
@test "an arm64 tag that holds no arm64 image is skipped, not stitched" {
|
|
# The testing package pins amd64 (an x86 KVM sandbox), so a stray
|
|
# <tag>-arm64 of it holds a second amd64 image. An index of two amd64
|
|
# manifests would be wrong.
|
|
tmp=$(mktemp -d)
|
|
_stub_registry "$tmp/reg"
|
|
_make_world "$tmp"
|
|
_push "$REG/sandbox" main-arm64 amd64 >/dev/null
|
|
|
|
PATH="$MOCK_REG/bin:$PATH" hack/stitch-multiarch.sh "$tmp/tree" "$REG" main >"$tmp/out" 2>"$tmp/err"
|
|
|
|
[ "$(_tag "$REG/sandbox" main)" = "$A_sandbox" ]
|
|
grep -q "$A_sandbox" "$tmp/tree/core/testing/values.yaml"
|
|
grep -q 'skipping sandbox: main and main-arm64 are not one amd64 and one arm64 image' "$tmp/err"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a rerun after a stitch that failed partway repins every ref" {
|
|
# The stitch job starts from the amd64 build's stamped tree every time, while
|
|
# the registry keeps what the failed attempt already pushed: some tags are
|
|
# indexes by then. Those must be repinned on the existing index, not skipped,
|
|
# or the rerun goes green with those images left amd64 only.
|
|
tmp=$(mktemp -d)
|
|
_stub_registry "$tmp/reg"
|
|
_make_world "$tmp"
|
|
cp -R "$tmp/tree" "$tmp/stamped"
|
|
|
|
rc=0
|
|
PATH="$MOCK_REG/bin:$PATH" MOCK_CREATE_LIMIT=2 hack/stitch-multiarch.sh "$tmp/tree" "$REG" main >"$tmp/out" 2>"$tmp/err" || rc=$?
|
|
[ "$rc" -ne 0 ]
|
|
[ "$(grep -c 'imagetools create' "$MOCK_REG/log")" -eq 3 ]
|
|
|
|
# An attempt that died inside imagetools create may leave a tag behind on A.
|
|
printf '%s v9.9.9 %s\n' "$REG/api" "$A_api" >>"$MOCK_REG/tags"
|
|
|
|
rm -rf "$tmp/tree"
|
|
cp -R "$tmp/stamped" "$tmp/tree"
|
|
PATH="$MOCK_REG/bin:$PATH" hack/stitch-multiarch.sh "$tmp/tree" "$REG" main >"$tmp/out" 2>"$tmp/err"
|
|
[ "$(_tag "$REG/api" v9.9.9)" = "$(_tag "$REG/api" main)" ]
|
|
|
|
for n in api cil lin ovn signer multus kamaji; do
|
|
eval "a=\$A_$n"
|
|
i="$(_tag "$REG/$n" main)"
|
|
[ "$i" != "$a" ]
|
|
if grep -rq --exclude-dir=charts "$a" "$tmp/tree"; then echo "FAIL: $n still pinned on its amd64 digest"; false; fi
|
|
grep -rqF "$i" "$tmp/tree"
|
|
done
|
|
grep -q '^stitch: 7 stitched ' "$tmp/out"
|
|
if grep -q 'no longer points\|skipping \(api\|cil\|lin\|ovn\|signer\|multus\|kamaji\)' "$tmp/err"; then
|
|
echo "FAIL: the rerun skipped an image"; false
|
|
fi
|
|
|
|
# A tag moved to anything but an index of the pinned image and an arm64
|
|
# image fails: a plain other image; another build's index; an index holding
|
|
# the twin but another amd64 image, as after a newer main run moved both
|
|
# tags; an index holding the pinned image and no arm64 image.
|
|
other_amd=$(_push "$REG/api" other amd64)
|
|
other_arm=$(_push "$REG/api" other-arm64 arm64)
|
|
b_api=$(_tag "$REG/api" main-arm64)
|
|
for sources in "single" "$REG/api@$other_amd $REG/api@$other_arm" "$REG/api@$other_amd $REG/api@$b_api" "$REG/api@$A_api $REG/api@$other_amd"; do
|
|
if [ "$sources" = single ]; then
|
|
_push "$REG/api" main amd64-other >/dev/null
|
|
else
|
|
# shellcheck disable=SC2086
|
|
PATH="$MOCK_REG/bin:$PATH" docker buildx imagetools create --tag "$REG/api:main" $sources
|
|
fi
|
|
rm -rf "$tmp/tree"
|
|
cp -R "$tmp/stamped" "$tmp/tree"
|
|
rc=0
|
|
PATH="$MOCK_REG/bin:$PATH" hack/stitch-multiarch.sh "$tmp/tree" "$REG" main >"$tmp/out" 2>"$tmp/err" || rc=$?
|
|
[ "$rc" -ne 0 ] || { echo "FAIL: api:main on '$sources' was accepted"; false; }
|
|
grep -q "api:main points at .*neither the digest the tree pins" "$tmp/err"
|
|
done
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a rerun whose arm64 leg rebuilt the image repins on the index already there" {
|
|
# A rerun starts again from the amd64 build's stamped tree, but its arm64 leg
|
|
# pushes a new arm64 image unless the cache reproduces it byte for byte. The
|
|
# build tag then sits on an index of the pinned amd64 image and the previous
|
|
# arm64 half. That is a valid index of the pinned image.
|
|
tmp=$(mktemp -d)
|
|
_stub_registry "$tmp/reg"
|
|
_make_world "$tmp"
|
|
cp -R "$tmp/tree" "$tmp/stamped"
|
|
PATH="$MOCK_REG/bin:$PATH" hack/stitch-multiarch.sh "$tmp/tree" "$REG" main >/dev/null 2>&1
|
|
first="$(_tag "$REG/api" main)"
|
|
|
|
for n in api cil lin ovn signer multus kamaji; do
|
|
_push "$REG/$n" main-arm64 arm64 application/vnd.docker.container.image.v1+json >/dev/null
|
|
done
|
|
rm -rf "$tmp/tree"
|
|
cp -R "$tmp/stamped" "$tmp/tree"
|
|
PATH="$MOCK_REG/bin:$PATH" hack/stitch-multiarch.sh "$tmp/tree" "$REG" main >"$tmp/out" 2>"$tmp/err"
|
|
|
|
[ "$(_tag "$REG/api" main)" = "$first" ]
|
|
grep -q "api:main@$first\$" "$tmp/tree/system/api/values.yaml"
|
|
grep -q '^stitch: 7 stitched ' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a digest the rewrite leaves behind fails the run" {
|
|
# Scan wider than you rewrite. A sed that rewrites nothing stands in for any
|
|
# way the rewrite can miss a copy of a digest it has already re-pointed.
|
|
tmp=$(mktemp -d)
|
|
_stub_registry "$tmp/reg"
|
|
_make_world "$tmp"
|
|
real_sed="$(command -v sed)"
|
|
printf '#!/bin/sh\ncase "$1" in s\\|*) cat "$2" ;; *) exec %s "$@" ;; esac\n' "$real_sed" >"$MOCK_REG/bin/sed"
|
|
chmod +x "$MOCK_REG/bin/sed"
|
|
|
|
rc=0
|
|
PATH="$MOCK_REG/bin:$PATH" hack/stitch-multiarch.sh "$tmp/tree" "$REG" main >"$tmp/out" 2>"$tmp/err" || rc=$?
|
|
|
|
[ "$rc" -ne 0 ]
|
|
grep -q "$A_api is still pinned after the rewrite, in: .*/system/api/values.yaml" "$tmp/err"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a registry error fails the run at every lookup instead of becoming a skip" {
|
|
# Skipping on a rate limit would leave the image amd64 only while the run
|
|
# reports success. Each lookup the stitch makes for one image fails in turn,
|
|
# on every try: the arm64 twin, the build tag, the architecture of either
|
|
# half, and the ref's own tag read while collecting the tags to move. api is
|
|
# processed first, so the failure lands before anything is pushed and the
|
|
# world can be reused.
|
|
tmp=$(mktemp -d)
|
|
_stub_registry "$tmp/reg"
|
|
_make_world "$tmp"
|
|
mv "$MOCK_REG/bin/skopeo" "$MOCK_REG/bin/skopeo.real"
|
|
{
|
|
echo '#!/bin/sh'
|
|
echo 'case "$*" in'
|
|
echo ' $MOCK_FAIL) echo "received unexpected HTTP status: 429 Too Many Requests" >&2; exit 1 ;;'
|
|
echo 'esac'
|
|
echo 'exec "$MOCK_REG/bin/skopeo.real" "$@"'
|
|
} >"$MOCK_REG/bin/skopeo"
|
|
chmod +x "$MOCK_REG/bin/skopeo"
|
|
|
|
for fail in \
|
|
"inspect --raw docker://$REG/api:main-arm64" \
|
|
"inspect --raw docker://$REG/api:main" \
|
|
"inspect --raw docker://$REG/api@$A_api" \
|
|
"inspect --config docker://$REG/api@*" \
|
|
"inspect --raw docker://$REG/api:v9.9.9"; do
|
|
rc=0
|
|
PATH="$MOCK_REG/bin:$PATH" MOCK_FAIL="$fail" STITCH_RETRY_DELAY=0 hack/stitch-multiarch.sh "$tmp/tree" "$REG" main >"$tmp/out" 2>"$tmp/err" || rc=$?
|
|
[ "$rc" -ne 0 ] || { echo "FAIL: a 429 on '$fail' did not fail the run"; false; }
|
|
grep -q '429 Too Many Requests' "$tmp/err"
|
|
if grep -q 'skipping' "$tmp/err"; then echo "FAIL: a 429 on '$fail' became a skip"; false; fi
|
|
done
|
|
grep -q "$A_api" "$tmp/tree/system/api/values.yaml"
|
|
if grep -q 'imagetools create' "$MOCK_REG/log"; then echo "FAIL: pushed despite a registry error"; false; fi
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "only a rate limit or a 5xx is retried, and a missing tag log fails the run" {
|
|
tmp=$(mktemp -d)
|
|
_stub_registry "$tmp/reg"
|
|
_make_world "$tmp"
|
|
mv "$MOCK_REG/bin/skopeo" "$MOCK_REG/bin/skopeo.real"
|
|
# MOCK_ERR is answered to the first MOCK_TIMES lookups of api's arm64 twin;
|
|
# every lookup of it is counted.
|
|
{
|
|
echo '#!/bin/sh'
|
|
echo 'if [ "$*" = "inspect --raw docker://$MOCK_TWIN" ]; then'
|
|
echo ' echo x >>"$MOCK_REG/twin-calls"'
|
|
echo ' if [ "$(wc -l <"$MOCK_REG/twin-calls")" -le "$MOCK_TIMES" ]; then echo "$MOCK_ERR" >&2; exit 1; fi'
|
|
echo 'fi'
|
|
echo 'exec "$MOCK_REG/bin/skopeo.real" "$@"'
|
|
} >"$MOCK_REG/bin/skopeo"
|
|
chmod +x "$MOCK_REG/bin/skopeo"
|
|
cp -R "$tmp/tree" "$tmp/stamped"
|
|
|
|
# Deterministic: asked once, and the run fails.
|
|
rc=0
|
|
PATH="$MOCK_REG/bin:$PATH" MOCK_TWIN="$REG/api:main-arm64" MOCK_TIMES=99 STITCH_RETRY_DELAY=0 \
|
|
MOCK_ERR="Error: reading manifest main-arm64: unauthorized: authentication required" \
|
|
hack/stitch-multiarch.sh "$tmp/tree" "$REG" main >"$tmp/out" 2>"$tmp/err" || rc=$?
|
|
[ "$rc" -ne 0 ]
|
|
grep -q 'unauthorized: authentication required' "$tmp/err"
|
|
[ "$(wc -l <"$MOCK_REG/twin-calls" | tr -d ' ')" -eq 1 ]
|
|
|
|
# A 5xx that clears on the second try.
|
|
rm -f "$MOCK_REG/twin-calls"
|
|
PATH="$MOCK_REG/bin:$PATH" MOCK_TWIN="$REG/api:main-arm64" MOCK_TIMES=1 STITCH_RETRY_DELAY=0 \
|
|
MOCK_ERR="Error: reading manifest main-arm64: received unexpected HTTP status: 503 Service Unavailable" \
|
|
hack/stitch-multiarch.sh "$tmp/tree" "$REG" main >"$tmp/out" 2>"$tmp/err"
|
|
[ "$(wc -l <"$MOCK_REG/twin-calls" | tr -d ' ')" -eq 2 ]
|
|
grep -q '^stitch: 7 stitched (api, ' "$tmp/out"
|
|
|
|
# A tag log that was asked for and never written.
|
|
rm -rf "$tmp/tree"
|
|
cp -R "$tmp/stamped" "$tmp/tree"
|
|
rc=0
|
|
PATH="$MOCK_REG/bin:$PATH" PUSHED_TAGS_LOG="$tmp/missing" hack/stitch-multiarch.sh "$tmp/tree" "$REG" main >"$tmp/out" 2>"$tmp/err" || rc=$?
|
|
[ "$rc" -ne 0 ]
|
|
grep -q "PUSHED_TAGS_LOG '$tmp/missing' does not exist" "$tmp/err"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a rate limit that clears within the tries does not fail the run" {
|
|
tmp=$(mktemp -d)
|
|
_stub_registry "$tmp/reg"
|
|
_make_world "$tmp"
|
|
mv "$MOCK_REG/bin/skopeo" "$MOCK_REG/bin/skopeo.real"
|
|
# The first two lookups of api's arm64 twin are refused, the third answered.
|
|
{
|
|
echo '#!/bin/sh'
|
|
echo 'if [ "$*" = "inspect --raw docker://$MOCK_TWIN" ]; then'
|
|
echo ' n=$(cat "$MOCK_REG/refused" 2>/dev/null || echo 0)'
|
|
echo ' if [ "$n" -lt 2 ]; then echo $((n + 1)) >"$MOCK_REG/refused"; echo "toomanyrequests: rate limit" >&2; exit 1; fi'
|
|
echo 'fi'
|
|
echo 'exec "$MOCK_REG/bin/skopeo.real" "$@"'
|
|
} >"$MOCK_REG/bin/skopeo"
|
|
chmod +x "$MOCK_REG/bin/skopeo"
|
|
PATH="$MOCK_REG/bin:$PATH" MOCK_TWIN="$REG/api:main-arm64" STITCH_RETRY_DELAY=0 \
|
|
hack/stitch-multiarch.sh "$tmp/tree" "$REG" main >"$tmp/out" 2>"$tmp/err"
|
|
[ "$(cat "$MOCK_REG/refused")" -eq 2 ]
|
|
grep -q '^stitch: 7 stitched (api, ' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|