Files
cozystack/hack/common-envs_test.bats
Myasnikov Daniil cc942fed68 test(tests): run unit suites with Bats and retain POSIX coverage
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>
2026-10-07 11:04:00 +03:00

226 lines
15 KiB
Bash

#!/usr/bin/env bats
# Unit tests for the OCI_EXPORT_DIR export mode wired through hack/common-envs.mk
# (#3257). A fork PR's unprivileged `pull_request` build carries no registry
# credentials, so it must export every image to a per-image OCI archive instead
# of pushing; a privileged workflow_run (e2e-fork.yaml) later pushes those
# archives to OCIR by digest. A build unit that pushes anyway dies on a fork with
# an anonymous-push `denied`, so these tests dry-run (`make -n`) a representative
# set of the root `build:` units with OCI_EXPORT_DIR set and assert that every
# image is exported as an archive and no (ungated) `docker://` push survives.
#
# Coverage includes the two units the fork split originally broke — talos (a
# skopeo copy that bypasses the image-tags macro) and capi-providers-cpprovider
# (a two-`--tag` buildx call that put two manifests in one OCI archive) — plus a
# plain image-tags package (cozystack-controller) as the happy path.
#
# Run from the repo root through `make bats-unit-tests`; the relative
# `make -C packages/...` calls resolve against that cwd. The file also remains
# compatible with the legacy `hack/cozytest.sh` translator, so it avoids
# run/$status/$output/setup() and uses plain $(...) capture plus grep and the
# build-matrix_test.bats `if grep -q …; then echo FAIL; false; fi`
# negation idiom (cozytest runs each @test under `set -e`, which suppresses a
# bare `!`-negated pipeline, so a regression would silently pass a `! grep`).
load test_helper
@test "cozystack-controller exports one OCI archive and never pushes under OCI_EXPORT_DIR" {
out=$(make -n -C packages/system/cozystack-controller image OCI_EXPORT_DIR=/tmp/ocitest IMAGE_TAG=pr-1-abc BUILDER=b)
# Exactly one per-image OCI archive export for the single built image.
# `grep -o | wc -l` counts OCCURRENCES: the macro expands onto a single line,
# so `grep -c` would count that one line and report 1 however many flags survive.
n=$(echo "$out" | grep -o -- '--output type=oci,dest=' | wc -l)
[ "$n" -eq 1 ]
echo "$out" | grep -q -- '--output type=oci,dest=/tmp/ocitest/cozystack-controller.oci.tar'
# A plain image-tags package routes every tag through the macro, so under
# export there is no docker:// ref at all.
if echo "$out" | grep -q 'docker://'; then echo "FAIL: cozystack-controller must not push under OCI_EXPORT_DIR"; false; fi
}
@test "capi-providers-cpprovider exports one OCI archive with a single tag under OCI_EXPORT_DIR" {
out=$(make -n -C packages/system/capi-providers-cpprovider image OCI_EXPORT_DIR=/tmp/ocitest IMAGE_TAG=pr-1-abc BUILDER=b)
# Exactly one per-image OCI archive export.
n=$(echo "$out" | grep -o -- '--output type=oci,dest=' | wc -l)
[ "$n" -eq 1 ]
echo "$out" | grep -q -- '--output type=oci,dest=/tmp/ocitest/cluster-api-control-plane-provider-kamaji.oci.tar'
# The two-tag regression: under `--output type=oci` both --tag refs land in
# index.json → two manifests → `skopeo copy oci-archive:…` fails with "more
# than one image in oci". Exactly one --tag must survive under export.
tags=$(echo "$out" | grep -o -- '--tag' | wc -l)
[ "$tags" -eq 1 ]
if echo "$out" | grep -q 'docker://'; then echo "FAIL: capi-providers-cpprovider must not push under OCI_EXPORT_DIR"; false; fi
}
@test "talos exports matchbox + talos archives and never pushes an ungated image under OCI_EXPORT_DIR" {
out=$(make -n -C packages/core/talos image OCI_EXPORT_DIR=/tmp/ocitest IMAGE_TAG=pr-1-abc BUILDER=b)
# matchbox is a buildx call through image-tags → one --output type=oci archive.
echo "$out" | grep -q -- '--output type=oci,dest=/tmp/ocitest/matchbox.oci.tar'
# talos is a skopeo copy that bypasses image-tags; under export it must target
# an oci-archive whose basename is `talos` (the OCIR repo e2e-fork.yaml pushes
# to by basename), NOT a docker:// registry (an anonymous push on a fork dies
# with `denied` — #3257).
echo "$out" | grep -q 'oci-archive:/tmp/ocitest/talos.oci.tar'
# No push survives export except the PUBLISH_*=1-gated release copies, which
# are inert here: a fork build never sets PUBLISH_*, so the shell gate expands
# to `[ "0" = "1" ]`. Anything else pushing to docker:// is the regression.
pushes=$(echo "$out" | grep 'docker://' | grep -v '"0" = "1"' || true)
if [ -n "$pushes" ]; then echo "FAIL: talos pushes an ungated image under OCI_EXPORT_DIR: $pushes"; false; fi
}
@test "OCI_EXPORT_DIR suppresses release tags/pushes even with PUBLISH_*=1 (#3262 hardening)" {
# The B2-class trap is latent: PUBLISH_* are 0 on fork PRs (the only export
# case today), so force them on to prove the OCI_EXPORT_DIR gate holds anyway.
# image-tags package: only the pr IMAGE_TAG survives — a versioned + :latest
# multi-tag OCI archive holds >1 manifest and `skopeo copy oci-archive:` refuses it.
out=$(make -n -C packages/system/cozystack-controller image OCI_EXPORT_DIR=/tmp/ocitest PUBLISH_VERSIONED=1 PUBLISH_FLOATING=1 IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b)
tags=$(echo "$out" | grep -o -- '--tag' | wc -l)
[ "$tags" -eq 1 ]
if echo "$out" | grep -q -- ':latest'; then echo "FAIL: image-tags emits :latest under OCI_EXPORT_DIR"; false; fi
# talos: its versioned/floating skopeo copies are additionally gated on an
# empty OCI_EXPORT_DIR (`[ -z … ]`), so no ungated docker:// push survives export.
tout=$(make -n -C packages/core/talos image OCI_EXPORT_DIR=/tmp/ocitest PUBLISH_VERSIONED=1 PUBLISH_FLOATING=1 IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b)
pushes=$(echo "$tout" | grep 'docker://' | grep -vF '[ -z ' || true)
if [ -n "$pushes" ]; then echo "FAIL: talos pushes an ungated image under OCI_EXPORT_DIR with PUBLISH_*=1: $pushes"; false; fi
}
@test "default path (OCI_EXPORT_DIR unset) still pushes and keeps release tags (#3262)" {
# Every other test here sets OCI_EXPORT_DIR, so nothing covered the OFF path —
# the one every same-repo PR, main and release build still takes. Without this
# case a refactor of the export wiring could stop pushing altogether, or strip
# the release tags, and the suite would stay green.
out=$(make -n -C packages/system/cozystack-controller image PUBLISH_VERSIONED=1 PUBLISH_FLOATING=1 IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b)
# buildx pushes, and no per-image archive is written.
echo "$out" | grep -q -- '--push=1'
if echo "$out" | grep -q -- '--output type=oci'; then echo "FAIL: OCI archive export leaked into the default path"; false; fi
# image-tags expands all three release tags when not exporting: the
# build-unique IMAGE_TAG, the versioned tag and :latest.
tags=$(echo "$out" | grep -o -- '--tag' | wc -l)
[ "$tags" -eq 3 ]
echo "$out" | grep -q -- ':latest'
# talos bypasses image-tags. Its skopeo copies must target the registry, and
# the `[ -z "$(OCI_EXPORT_DIR)" ]` release gates must be INERT off-export
# (expanding to `[ -z "" ]`) so a release run still pushes versioned+floating.
tout=$(make -n -C packages/core/talos image PUBLISH_VERSIONED=1 PUBLISH_FLOATING=1 IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b)
echo "$tout" | grep -q 'docker://.*/talos:pr-1-abc'
echo "$tout" | grep -q 'docker://.*/talos:latest'
if echo "$tout" | grep -q 'oci-archive:'; then echo "FAIL: talos exports an archive without OCI_EXPORT_DIR"; false; fi
}
@test "a published image is built for amd64 and arm64 unless PLATFORM says otherwise" {
# A push with no PLATFORM builds both, so an arm64 workstation cannot publish
# an arm64-only digest that amd64 nodes then fail to pull.
out=$(make -n -C packages/system/cozystack-controller image IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b)
platforms=$(echo "$out" | grep -o -- '--platform=[^ ]*' | tr '\n' ' ')
[ "$platforms" = "--platform=linux/amd64,linux/arm64 " ]
# An explicit PLATFORM replaces the default rather than adding to it.
out=$(make -n -C packages/system/cozystack-controller image IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b PLATFORM=linux/arm64)
platforms=$(echo "$out" | grep -o -- '--platform=[^ ]*' | tr '\n' ' ')
[ "$platforms" = "--platform=linux/arm64 " ]
}
@test "LOAD=1 builds for the host only, since the docker image store cannot load an index" {
out=$(make -n -C packages/system/cozystack-controller image IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b LOAD=1 PUSH=0)
echo "$out" | grep -q -- '--load=1'
if echo "$out" | grep -q -- '--platform'; then echo "FAIL: LOAD=1 passes a platform, so buildx builds an index it cannot load"; false; fi
}
@test "LOAD and PUSH from the environment reach buildx, and LOAD=1 alone does not push" {
out=$(LOAD=1 PUSH=0 make -n -C packages/system/cozystack-controller image IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b)
echo "$out" | grep -q -- '--push=0 --load=1' || { echo "FAIL: environment LOAD=1 PUSH=0 ignored"; false; }
if echo "$out" | grep -q -- '--platform'; then echo "FAIL: environment LOAD=1 still builds a multi-arch index"; false; fi
out=$(LOAD=1 make -n -C packages/system/cozystack-controller image IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b)
echo "$out" | grep -q -- '--push=0 --load=1' || { echo "FAIL: LOAD=1 alone pushes"; false; }
# An explicit PUSH still wins over the LOAD-derived default.
out=$(LOAD=1 PUSH=1 make -n -C packages/system/cozystack-controller image IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b)
echo "$out" | grep -q -- '--push=1 --load=1'
# The fork export must keep beating an environment PUSH=1.
out=$(PUSH=1 make -n -C packages/system/cozystack-controller image IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b OCI_EXPORT_DIR=/tmp/ocitest)
echo "$out" | grep -q -- '--push=0 --load=0'
}
@test "an empty PUSH or LOAD in the environment falls back to the default" {
out=$(PUSH= LOAD= make -n -C packages/system/cozystack-controller image IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b)
echo "$out" | grep -q -- '--push=1 --load=0' || { echo "FAIL: empty PUSH/LOAD reach buildx as empty flags"; false; }
out=$(PUSH= LOAD=1 make -n -C packages/system/cozystack-controller image IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b)
echo "$out" | grep -q -- '--push=0 --load=1'
}
@test "talos publishes nothing to the registry with PUSH=0" {
out=$(make -n -B -C packages/core/talos image-talos PUSH=0 PUBLISH_VERSIONED=1 PUBLISH_FLOATING=1 IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0)
echo "$out" | grep -q 'docker://'
pushes=$(echo "$out" | grep 'docker://' | grep -vF '[ "0" = "1" ]' || true)
if [ -n "$pushes" ]; then echo "FAIL: talos pushes with PUSH=0: $pushes"; false; fi
}
@test "CACHE_TAG moves the cache ref of every image" {
# The arm64 leg writes its own mode=max cache. Sharing the amd64 ref would
# make every write from one leg evict the other's layers.
out=$(make -n -C packages/system/cozystack-controller image IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b WRITE_CACHE=1)
echo "$out" | grep -q -- '--cache-from type=registry,ref=[^ ]*/cozystack-controller:buildcache '
echo "$out" | grep -q -- '--cache-to type=registry,ref=[^ ]*/cozystack-controller:buildcache,'
out=$(make -n -C packages/system/cozystack-controller image IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b WRITE_CACHE=1 CACHE_TAG=buildcache-arm64)
echo "$out" | grep -q -- '--cache-from type=registry,ref=[^ ]*/cozystack-controller:buildcache-arm64 '
echo "$out" | grep -q -- '--cache-to type=registry,ref=[^ ]*/cozystack-controller:buildcache-arm64,'
# No per-call tag may bypass CACHE_TAG, or both legs write one cache ref.
tmp=$(mktemp -d)
printf 'include %s/hack/common-envs.mk\nprobe:\n\t@echo $(call cache-args,img,percall)\n' "$(pwd)" > "$tmp/Makefile"
out=$(make -s -C "$tmp" probe COZYSTACK_VERSION=0 WRITE_CACHE=1 CACHE_TAG=buildcache-arm64)
rm -rf "$tmp"
echo "$out" | grep -q -- '--cache-from type=registry,ref=[^ ]*/img:buildcache-arm64 '
echo "$out" | grep -q -- '--cache-to type=registry,ref=[^ ]*/img:buildcache-arm64,'
}
@test "the kamaji provider image is pushed under the build's IMAGE_TAG like every other image" {
# An IMAGE_TAG assigned in a package Makefile beats the value CI puts in the
# environment, so every PR, main and line build would push the same
# component-version tag and overwrite each other's.
pkg=packages/system/capi-providers-cpprovider
out=$(IMAGE_TAG=pr-1-abc make -n -C "$pkg" image COZYSTACK_VERSION=0 BUILDER=b)
tags=$(echo "$out" | grep -o -- '--tag [^ ]*' | sed 's/^--tag //')
[ "$(echo "$tags" | sed 's/.*://')" = pr-1-abc ] || { echo "FAIL: pushes $tags"; false; }
# The stamped refs keep the component version, the tag a release publishes.
echo "$out" | grep -qE 'IMG="[^"]*/cluster-api-control-plane-provider-kamaji:v[0-9][^"@]*-cozystack\.[0-9]+@'
# The component tag is published only when a release asks for versioned tags.
out=$(IMAGE_TAG=pr-1-abc PUBLISH_VERSIONED=1 make -n -C "$pkg" image COZYSTACK_VERSION=0 BUILDER=b)
tags=$(echo "$out" | grep -o -- '--tag [^ ]*' | sed 's/.*://')
[ "$(echo "$tags" | head -1)" = pr-1-abc ]
echo "$tags" | grep -qE '^v[0-9].*-cozystack\.[0-9]+$'
[ "$(echo "$tags" | wc -l | tr -d ' ')" -eq 2 ]
if echo "$tags" | grep -q -- '-$'; then echo "FAIL: a tag ends in '-': $tags"; false; fi
}
@test "image-tags records every tag it pushes in PUSHED_TAGS_LOG, and nothing when unset, exporting or not pushing" {
# The stitch moves exactly these tags onto the multi-arch index. A component
# version pushed under PUBLISH_VERSIONED=1 is named by no ref in the tree, so
# a tag missing here would stay on the amd64-only image.
tmp=$(mktemp -d)
out=$(make -n -C packages/system/cozystack-controller image IMAGE_TAG=v9.9.9-rc.1 PUBLISH_VERSIONED=1 BUILDER=b \
REGISTRY=reg.example/c PUSHED_TAGS_LOG="$tmp/log")
echo "$out" | grep -o -- '--tag [^ ]*' | awk '{ print $2 }' | sort >"$tmp/pushed"
[ "$(wc -l <"$tmp/pushed" | tr -d ' ')" -eq 2 ]
sort "$tmp/log" | diff "$tmp/pushed" -
make -n -C packages/system/cozystack-controller image IMAGE_TAG=v9.9.9-rc.1 PUBLISH_VERSIONED=1 BUILDER=b >/dev/null
make -n -C packages/system/cozystack-controller image IMAGE_TAG=pr-1-abc BUILDER=b \
OCI_EXPORT_DIR=/tmp/ocitest PUSHED_TAGS_LOG="$tmp/export-log" >/dev/null
[ ! -e "$tmp/export-log" ]
# A build that does not push must not hand its tags to the stitch.
LOAD=1 make -n -C packages/system/cozystack-controller image IMAGE_TAG=pr-1-abc BUILDER=b \
PUSHED_TAGS_LOG="$tmp/load-log" >/dev/null
if [ -s "$tmp/load-log" ]; then echo "FAIL: LOAD=1 logs tags it never pushed: $(cat "$tmp/load-log")"; false; fi
rm -rf "$tmp"
}
@test "talos pushes one installer index for PLATFORM and matchbox boots both arches" {
# -B prints every asset build whether or not a local _out/assets has it.
out=$(make -n -B -C packages/core/talos image IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b)
echo "$out" | grep -q 'for a in amd64 arm64; do skopeo copy'
echo "$out" | grep -q 'hack/installer-index.sh [^ ]* amd64 arm64;'
echo "$out" | grep -q 'skopeo copy --all "$SRC" docker://[^ ]*/talos:pr-1-abc'
# The installer follows PLATFORM, but matchbox serves machines of either arch
# wherever it runs, so it still needs the arm64 kernel and initramfs.
out=$(make -n -B -C packages/core/talos image IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b PLATFORM=linux/amd64)
if echo "$out" | grep -q 'installer-arm64'; then echo "FAIL: an amd64 build builds the arm64 installer"; false; fi
echo "$out" | grep -q 'profiles/kernel-arm64.yaml'
echo "$out" | grep -q 'profiles/initramfs-arm64.yaml'
}