Ctrl K
ComunidadGuías…

hack/common-envs_test.bats

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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 "SBOM=1 attaches an SPDX attestation from a scanner pinned by digest, and the default builds none" {
  # An image built with an attestation is an index, not a plain manifest, which
  # the stitch and the gate have to read, so it stays off unless a build asks.
  # --sbom=true would pull the scanner by a floating tag.
  out=$(make -n -C packages/system/cozystack-controller image IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b)
  if echo "$out" | grep -q -- '--attest\|--sbom'; then echo "FAIL: an attestation without SBOM=1"; false; fi
  out=$(make -n -C packages/system/cozystack-controller image IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b SBOM=1)
  echo "$out" | grep -qE -- '--attest type=sbom,generator=[^ ]+@sha256:[0-9a-f]{64} ' || { echo "FAIL: no digest-pinned scanner"; false; }
  if echo "$out" | grep -q -- '--sbom'; then echo "FAIL: --sbom=true pulls a floating scanner tag"; false; fi
  # The environment reaches buildx as well, which is how the release workflow sets it.
  out=$(SBOM=1 make -n -C packages/system/cozystack-controller image IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b)
  echo "$out" | grep -q -- '--attest type=sbom,'
}

@test "Renovate moves the SBOM scanner pin, tag and digest together" {
  # Renovate reaches hack/common-envs.mk only through a custom manager, and a
  # reformatted SBOM_SCANNER line drops out of its regexp without a word, so the
  # pin would stay on its syft. The named groups are Renovate's: depName,
  # currentValue and currentDigest, in that order.
  re=$(jq -r '.customManagers[] | select(any(.managerFilePatterns[]; test("common-envs"))) | .matchStrings[0]' .github/renovate.json)
  [ -n "$re" ] || { echo "FAIL: no Renovate custom manager covers hack/common-envs.mk"; false; }
  ere=$(printf '%s' "$re" | sed -E 's/\(\?<[A-Za-z]+>/(/g')
  pin=$(sed -nE "s#^.*${ere}.*\$#\\1:\\2@\\3#p" hack/common-envs.mk)
  [ -n "$pin" ] || { echo "FAIL: the manager matches no SBOM_SCANNER line in hack/common-envs.mk"; false; }
  out=$(make -n -C packages/system/cozystack-controller image IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b SBOM=1)
  echo "$out" | grep -qF -- "generator=${pin} " || { echo "FAIL: the manager reads ${pin}, not the scanner the build passes"; false; }
}

@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'
}