Ctrl K
ComunidadGuías…

hack/promote-retag_test.bats

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

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bats
# Tests for hack/promote-retag.sh — the rc->stable retag selector.
#
# Guards the regression where collect_refs scraped *every* @sha256 ref from the
# package values.yaml — including third-party images (docker.io/clastix/kubectl,
# ghcr.io/kvaps/...), bare upstream tags (kube-ovn/keycloak/kilo) and a
# "--migrate-image=..." arg string — so the first skopeo copy to a registry CI
# cannot push to aborted the whole promotion. The selector must emit only
# cozystack-owned ($REGISTRY/...) refs.
#
# Harness compatibility note: CI runs this file under Bats through
# `make bats-unit-tests`. It also remains compatible with the narrower legacy
# `hack/cozytest.sh` translator, whose awk parser recognizes only @test blocks
# and a bare `}` on its own line. The tests therefore avoid `run`, `$status`,
# `$output`, `skip`, and setup()/teardown(), and capture expected non-zero exits
# with `|| rc=$?`. mikefarah yq is assumed present (provided by the test
# toolchain, like the other yq-using Bats files here).
#
# Run with: bats hack/promote-retag_test.bats

load test_helper

_make_registry_mocks() {
  t="$1"
  mkdir -p "$t/bin"
  cat >"$t/bin/yq" <<'EOF'
#!/bin/sh
set -eu
if [ "${1:-}" = "--version" ]; then
  echo 'yq (https://github.com/mikefarah/yq/) version v4.45.1'
else
  printf '%s\n' "$MOCK_REF"
fi
EOF
  cat >"$t/bin/skopeo" <<'EOF'
#!/bin/sh
set -eu
printf '%s\n' "$*" >>"$MOCK_SKOPEO_LOG"
case "$1" in
  inspect)
    [ "$2" = "--raw" ]
    # An indeterminate failure: non-zero, no bytes, and the registry never says
    # the manifest is unknown. Must NOT read as "tag absent".
    if [ "${MOCK_TRANSIENT_ERROR:-0}" = "1" ]; then
      echo "Error: reading manifest from docker://${3:-?}: received unexpected HTTP status: 429 Too Many Requests" >&2
      exit 1
    fi
    if [ "$MOCK_MISSING_ONCE" = "1" ] && [ ! -f "$MOCK_STATE" ]; then
      # Absence as a registry actually reports it — the script requires proof,
      # not merely a non-zero exit.
      echo "Error: reading manifest from docker://${3:-?}: manifest unknown" >&2
      exit 1
    fi
    # A registry answering 200 with an empty body: exit 0, no bytes.
    if [ "${MOCK_EMPTY_MANIFEST:-0}" = "1" ]; then
      exit 0
    fi
    printf '%s' "$MOCK_MANIFEST"
    ;;
  copy)
    : >"$MOCK_STATE"
    ;;
  *)
    exit 2
    ;;
esac
EOF
  # sha256sum belongs in the stub for the same reason yq and skopeo do: the tests
  # below pin PATH to "$tmp/bin:/usr/bin:/bin", and where the hashing utility
  # lives is a property of the host, not of the thing under test. GNU coreutils
  # puts sha256sum in /usr/bin, which is inside that PATH; macOS ships shasum
  # there and sha256sum elsewhere, which is not -- so promote-retag.sh took its
  # "sha256sum is required" exit and six of the eleven tests failed on the
  # platform rather than on the code. They failed silently, too: each test
  # cleaned up from an EXIT trap, which replaces the one the bats binary installs
  # for its bookkeeping, so a failing test printed no TAP line at all and the run
  # showed five passes and no failures.
  #
  # Resolved from absolute candidates rather than through `command -v`: this stub
  # is first on PATH, so asking PATH for sha256sum finds this file and recurses.
  cat >"$t/bin/sha256sum" <<'EOF'
#!/bin/sh
set -eu
for _c in /usr/bin/sha256sum /bin/sha256sum /sbin/sha256sum /usr/local/bin/sha256sum; do
  [ -x "$_c" ] && exec "$_c" "$@"
done
for _c in /usr/bin/shasum /bin/shasum; do
  [ -x "$_c" ] && exec "$_c" -a 256 "$@"
done
echo "no sha256 implementation found outside the stub dir" >&2
exit 127
EOF
  chmod +x "$t/bin/yq" "$t/bin/skopeo" "$t/bin/sha256sum"
}

@test "dry-run over the real tree retags only cozystack-owned refs" {
  tmp=$(mktemp -d)

  # `env -u REGISTRY`: the CI workflow exports REGISTRY=<OCIR build registry>
  # for every job (.github/workflows/pull-requests.yaml), but the committed
  # tree vendors its digests under the script's default ghcr.io/cozystack/
  # cozystack. Inheriting the ambient REGISTRY makes the selector filter for the
  # wrong registry, match nothing, and abort — so strip it and exercise the
  # default, the registry the refs below actually live under.
  #
  # An exit-0 is the assertion; on any non-zero, surface the script's own
  # stdout/stderr (collect_refs swallows yq errors, so its stderr is the only
  # breadcrumb) and the yq build, so a CI failure is self-diagnosing.
  rc=0
  env -u REGISTRY hack/promote-retag.sh v9.9.9 --dry-run \
    >"$tmp/out" 2>"$tmp/err" || rc=$?
  if [ "$rc" -ne 0 ]; then
    echo "promote-retag.sh exited $rc; yq: $(yq --version 2>&1)" >&2
    echo "--- script stderr ---" >&2; cat "$tmp/err" >&2
    echo "--- script stdout ---" >&2; cat "$tmp/out" >&2
    return "$rc"
  fi

  # At least one cozystack-owned image is selected.
  grep -q 'docker://ghcr.io/cozystack/cozystack/' "$tmp/out"

  # Images whose digest is embedded in `tag` ({repository, tag: <t>@sha256:<d>})
  # must be selected too. The "at least one owned ref" check above cannot catch
  # their absence — it is satisfied by the shapes that already worked, which is
  # why eight images across six packages were silently skipped while this suite
  # stayed green. Naming concrete packages is deliberate: a count or a generic
  # pattern would drift back to proving nothing. linstor-csi and piraeus-server
  # are the two whose absence from GHCR broke the nightly e2e at image pre-pull.
  for owned in linstor-csi piraeus-server kamaji redis-operator; do
    grep -q "docker://ghcr.io/cozystack/cozystack/${owned}@sha256:" "$tmp/out"
  done

  # Both images are built and pushed to $REGISTRY by cozystack and carry the
  # digest in `tag`, and neither has ever received a 1.x release tag (on GHCR
  # keycloak-operator has only `latest`, and kubeovn's newest cozystack-versioned
  # tag predates 1.0), so they get one only if the ownership filter counts them
  # as ours. kubeovn keeps its host in global.registry.address, apart from
  # `repository`.
  for owned in keycloak-operator kubeovn; do
    grep -q "docker://ghcr.io/cozystack/cozystack/${owned}@sha256:" "$tmp/out"
  done

  # Every docker:// ref in the copy plan is under the cozystack registry — no
  # third-party repos and no malformed arg-string refs leak through.
  bad=$(grep -oE 'docker://[^ ]+' "$tmp/out" | sed 's|docker://||' \
        | grep -vE '^ghcr\.io/cozystack/cozystack/' || true)
  [ -z "$bad" ]
  rm -rf "$tmp"
}

@test "default leaves :latest unmoved" {
  tmp=$(mktemp -d)

  # :latest belongs to promotion, and only when the promoted version is the
  # newest published stable. Without MOVE_LATEST the plan retags the stable tag
  # but must NOT repoint :latest — otherwise a patch on an older line would drag
  # :latest backwards.
  rc=0
  env -u REGISTRY hack/promote-retag.sh v9.9.9 --dry-run \
    >"$tmp/out" 2>"$tmp/err" || rc=$?
  if [ "$rc" -ne 0 ]; then
    echo "promote-retag.sh exited $rc" >&2
    echo "--- script stderr ---" >&2; cat "$tmp/err" >&2
    return "$rc"
  fi

  # The stable tag is in the copy plan...
  grep -qE 'docker://ghcr\.io/cozystack/cozystack/[^ ]*:v9\.9\.9' "$tmp/out"
  # ...but nothing moves :latest.
  if grep -qE 'docker://[^ ]+:latest' "$tmp/out"; then echo "FAIL: no :latest tag must have been planned"; false; fi
  rm -rf "$tmp"
}

@test "MOVE_LATEST=1 also repoints :latest" {
  tmp=$(mktemp -d)

  rc=0
  env -u REGISTRY MOVE_LATEST=1 hack/promote-retag.sh v9.9.9 --dry-run \
    >"$tmp/out" 2>"$tmp/err" || rc=$?
  if [ "$rc" -ne 0 ]; then
    echo "promote-retag.sh exited $rc" >&2
    echo "--- script stderr ---" >&2; cat "$tmp/err" >&2
    return "$rc"
  fi

  # Every promoted repo also gets a :latest copy in the plan.
  grep -qE 'docker://ghcr\.io/cozystack/cozystack/[^ ]*:latest' "$tmp/out"
  rm -rf "$tmp"
}

@test "REGISTRY override scopes the selection" {
  tmp=$(mktemp -d)

  # No cozystack images live under example.com/nope, so the selector finds
  # nothing and exits non-zero rather than silently promoting the wrong set.
  # Capture the exit status without tripping the harness's `set -e`.
  rc=0
  REGISTRY="example.com/nope" hack/promote-retag.sh v9.9.9 --dry-run \
    >"$tmp/out" 2>"$tmp/err" || rc=$?

  [ "$rc" -ne 0 ]
  # The diagnostic is written to stderr.
  grep -q 'No cozystack-owned digest-pinned image refs found' "$tmp/err"
  rm -rf "$tmp"
}

@test "retags images whose ref lives outside a values.yaml" {
  # Until the file enumeration moved to hack/lib/image-refs.sh this scanned the
  # depth-2 values.yaml alone, so every ref held in an images/*.tag file or
  # stamped into a template was skipped — the promotion reported success while
  # never creating those images' :<version> tags. Twelve images were affected
  # (30 refs selected before, 42 after). Because the retag stays inside one
  # registry the digests still resolved, so nothing failed at pull time and the
  # gap went unnoticed until a release shipped reading as a release candidate.
  tmp=$(mktemp -d)

  rc=0
  env -u REGISTRY hack/promote-retag.sh v9.9.9 --dry-run \
    >"$tmp/out" 2>"$tmp/err" || rc=$?
  if [ "$rc" -ne 0 ]; then
    echo "promote-retag.sh exited $rc" >&2
    echo "--- script stderr ---" >&2; cat "$tmp/err" >&2
    return "$rc"
  fi

  # grafana-dashboards lives in
  # packages/system/grafana-operator/images/grafana-dashboards.tag, and
  # multus-cni is sed'd into packages/system/multus/templates/*.yml. Neither is
  # reachable from any values.yaml.
  grep -q 'cozystack/grafana-dashboards:v9.9.9' "$tmp/out"
  grep -q 'cozystack/multus-cni:v9.9.9' "$tmp/out"
  rm -rf "$tmp"
}

@test "raw manifest digest resolves for an OCI artifact" {
  tmp=$(mktemp -d)
  _make_registry_mocks "$tmp"

  manifest='{"schemaVersion":2,"config":{"mediaType":"application/vnd.cncf.flux.config.v1+json","digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},"layers":[]}'
  digest="sha256:$(printf '%s' "$manifest" | sha256sum | cut -d' ' -f1)"
  ref="example.com/cozystack/cozystack-packages:v1.6.0-rc.1@${digest}"

  rc=0
  REGISTRY="example.com/cozystack" MOCK_REF="$ref" MOCK_MANIFEST="$manifest" \
    MOCK_MISSING_ONCE=0 MOCK_STATE="$tmp/state" MOCK_SKOPEO_LOG="$tmp/skopeo.log" \
    PATH="$tmp/bin:/usr/bin:/bin" hack/promote-retag.sh v1.6.0 \
    >"$tmp/out" 2>"$tmp/err" || rc=$?
  if [ "$rc" -ne 0 ]; then
    echo "promote-retag.sh exited $rc" >&2
    echo "--- stderr ---" >&2; cat "$tmp/err" >&2
    return "$rc"
  fi

  grep -q "already at ${digest}; skipping stable copy" "$tmp/out"
  [ "$(grep -c '^inspect --raw ' "$tmp/skopeo.log")" -eq 2 ]
  if grep -q '^copy ' "$tmp/skopeo.log"; then echo "FAIL: no image must have been copied"; false; fi
  rm -rf "$tmp"
}

@test "post-copy raw manifest digest mismatch fails verification" {
  tmp=$(mktemp -d)
  _make_registry_mocks "$tmp"

  manifest='{"schemaVersion":2,"config":{"mediaType":"application/vnd.cncf.flux.config.v1+json","digest":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"},"layers":[]}'
  expected_manifest='{"schemaVersion":2,"config":{"mediaType":"application/vnd.cncf.flux.config.v1+json","digest":"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"},"layers":[]}'
  actual="sha256:$(printf '%s' "$manifest" | sha256sum | cut -d' ' -f1)"
  expected="sha256:$(printf '%s' "$expected_manifest" | sha256sum | cut -d' ' -f1)"
  ref="example.com/cozystack/cozystack-packages:v1.6.0-rc.1@${expected}"

  rc=0
  REGISTRY="example.com/cozystack" MOCK_REF="$ref" MOCK_MANIFEST="$manifest" \
    MOCK_MISSING_ONCE=1 MOCK_STATE="$tmp/state" MOCK_SKOPEO_LOG="$tmp/skopeo.log" \
    PATH="$tmp/bin:/usr/bin:/bin" hack/promote-retag.sh v1.6.0 \
    >"$tmp/out" 2>"$tmp/err" || rc=$?

  [ "$rc" -ne 0 ]
  grep -q "resolved to '${actual}', expected '${expected}'" "$tmp/err"
  grep -q '^copy --multi-arch all ' "$tmp/skopeo.log"
  [ "$(grep -c '^inspect --raw ' "$tmp/skopeo.log")" -eq 2 ]
  rm -rf "$tmp"
}

@test "missing stable tag remains an empty digest and proceeds to copy" {
  tmp=$(mktemp -d)
  _make_registry_mocks "$tmp"

  manifest='{"schemaVersion":2,"config":{"mediaType":"application/vnd.cncf.flux.config.v1+json","digest":"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"},"layers":[]}'
  digest="sha256:$(printf '%s' "$manifest" | sha256sum | cut -d' ' -f1)"
  ref="example.com/cozystack/cozystack-packages:v1.6.0-rc.1@${digest}"

  rc=0
  REGISTRY="example.com/cozystack" MOCK_REF="$ref" MOCK_MANIFEST="$manifest" \
    MOCK_MISSING_ONCE=1 MOCK_STATE="$tmp/state" MOCK_SKOPEO_LOG="$tmp/skopeo.log" \
    PATH="$tmp/bin:/usr/bin:/bin" hack/promote-retag.sh v1.6.0 \
    >"$tmp/out" 2>"$tmp/err" || rc=$?
  if [ "$rc" -ne 0 ]; then
    echo "promote-retag.sh exited $rc" >&2
    echo "--- stderr ---" >&2; cat "$tmp/err" >&2
    return "$rc"
  fi

  grep -q '^copy --multi-arch all ' "$tmp/skopeo.log"
  [ "$(grep -c '^inspect --raw ' "$tmp/skopeo.log")" -eq 2 ]
  grep -q 'Retagged image refs to v1.6.0' "$tmp/out"
  rm -rf "$tmp"
}

@test "existing stable tag at a different digest is refused, not moved" {
  tmp=$(mktemp -d)
  _make_registry_mocks "$tmp"

  # The stable tag already exists (MOCK_MISSING_ONCE=0) but resolves to a
  # manifest other than the rc's: released bytes must never be overwritten.
  published='{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:1111111111111111111111111111111111111111111111111111111111111111"},"layers":[]}'
  rc_manifest='{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:2222222222222222222222222222222222222222222222222222222222222222"},"layers":[]}'
  published_digest="sha256:$(printf '%s' "$published" | sha256sum | cut -d' ' -f1)"
  rc_digest="sha256:$(printf '%s' "$rc_manifest" | sha256sum | cut -d' ' -f1)"
  ref="example.com/cozystack/cozystack-packages:v1.6.0-rc.1@${rc_digest}"

  rc=0
  REGISTRY="example.com/cozystack" MOCK_REF="$ref" MOCK_MANIFEST="$published" \
    MOCK_MISSING_ONCE=0 MOCK_STATE="$tmp/state" MOCK_SKOPEO_LOG="$tmp/skopeo.log" \
    PATH="$tmp/bin:/usr/bin:/bin" hack/promote-retag.sh v1.6.0 \
    >"$tmp/out" 2>"$tmp/err" || rc=$?

  [ "$rc" -ne 0 ]
  grep -q "already exists at '${published_digest}'; refusing to move it to '${rc_digest}'" "$tmp/err"
  # The refusal must happen BEFORE any write.
  if grep -q '^copy ' "$tmp/skopeo.log"; then echo "FAIL: no image must have been copied"; false; fi
  [ "$(grep -c '^inspect --raw ' "$tmp/skopeo.log")" -eq 1 ]
  rm -rf "$tmp"
}

@test "a zero-exit empty manifest refuses to decide and never writes" {
  tmp=$(mktemp -d)
  _make_registry_mocks "$tmp"

  manifest='{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:3333333333333333333333333333333333333333333333333333333333333333"},"layers":[]}'
  digest="sha256:$(printf '%s' "$manifest" | sha256sum | cut -d' ' -f1)"
  ref="example.com/cozystack/cozystack-packages:v1.6.0-rc.1@${digest}"

  rc=0
  REGISTRY="example.com/cozystack" MOCK_REF="$ref" MOCK_MANIFEST="$manifest" \
    MOCK_EMPTY_MANIFEST=1 MOCK_MISSING_ONCE=0 MOCK_STATE="$tmp/state" \
    MOCK_SKOPEO_LOG="$tmp/skopeo.log" \
    PATH="$tmp/bin:/usr/bin:/bin" hack/promote-retag.sh v1.6.0 \
    >"$tmp/out" 2>"$tmp/err" || rc=$?

  # A 200 with an empty body proves nothing: the tag may hold released bytes this
  # promotion must not overwrite. So the script must abort BEFORE any copy —
  # reading "no bytes" as "not published" is what would turn a proxy hiccup into
  # an overwrite of a published stable tag.
  [ "$rc" -ne 0 ]
  grep -q 'returned no manifest bytes' "$tmp/err"
  if grep -q '^copy ' "$tmp/skopeo.log"; then echo "FAIL: no image must have been copied"; false; fi
  [ "$(grep -c '^inspect --raw ' "$tmp/skopeo.log")" -eq 1 ]
  # The empty-input hash must never appear: that is the guard being absent.
  if grep -q 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' "$tmp/err"; then echo "FAIL: the empty-manifest digest must not reach the diagnostic"; false; fi
  rm -rf "$tmp"
}

@test "a transient registry failure is not read as an unpublished tag" {
  tmp=$(mktemp -d)
  _make_registry_mocks "$tmp"

  manifest='{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:4444444444444444444444444444444444444444444444444444444444444444"},"layers":[]}'
  digest="sha256:$(printf '%s' "$manifest" | sha256sum | cut -d' ' -f1)"
  ref="example.com/cozystack/cozystack-packages:v1.6.0-rc.1@${digest}"

  # 429 during finalize: non-zero exit, no bytes, no "manifest unknown". The old
  # shape of this helper made that indistinguishable from an absent tag and
  # proceeded to copy over it; finalize retags ~42 refs with no retry wrapper, so
  # a single rate-limit was enough to reach that path.
  rc=0
  REGISTRY="example.com/cozystack" MOCK_REF="$ref" MOCK_MANIFEST="$manifest" \
    MOCK_TRANSIENT_ERROR=1 MOCK_MISSING_ONCE=0 MOCK_STATE="$tmp/state" \
    MOCK_SKOPEO_LOG="$tmp/skopeo.log" \
    PATH="$tmp/bin:/usr/bin:/bin" hack/promote-retag.sh v1.6.0 \
    >"$tmp/out" 2>"$tmp/err" || rc=$?

  [ "$rc" -ne 0 ]
  grep -q 'cannot be treated as unpublished' "$tmp/err"
  grep -q '429' "$tmp/err"
  if grep -q '^copy ' "$tmp/skopeo.log"; then echo "FAIL: no image must have been copied"; false; fi
  rm -rf "$tmp"
}