Ctrl K
ComunidadGuías…

hack/migration-seaweedfs-db-adopt.bats

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

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bats
# -----------------------------------------------------------------------------
# Unit tests for the SeaweedFS db-split hand-over shared by platform migrations
# 43 (original) and 53 (repair) — lib/seaweedfs-db-adopt.sh.
#
# The 1.5.0 split (PR #2601) moved Cluster/seaweedfs-db out of the <name>-system
# release into a new <name>-db release. The hand-over must, before <name>-system
# next renders, re-own the Cluster to <name>-db AND stamp
# helm.sh/resource-policy: keep — otherwise the <name>-system upgrade prunes the
# Cluster as a removed resource, CNPG takes its PVC with it, and the tenant's
# filer metadata (all of its S3) is gone.
#
# Three properties are pinned here, each of which shipped broken:
#
#  1. INSTANCE NAME. Migration 43 compared the owner against the literal
#     "seaweedfs-system". `SeaweedFS` is user-creatable, so an instance named
#     `foo` is owned by `foo-system` and was silently skipped. Observed live:
#     four default-named tenants carry release-name=seaweedfs-db + keep and their
#     Clusters survived; the one tenant running `foo` has no Cluster at all.
#
#  2. OWNERSHIP IS NOT SAFETY. A Cluster already owned by <name>-db can still
#     need keep: where the hand-over was skipped, <name>-system prunes it and
#     <name>-db RECREATES it under its own ownership with no keep, while
#     <name>-system's prune baseline still lists it. Live proof that the shape is
#     real: tenant-l and tenant-root are <name>-db-owned and their <name>-system
#     deployed revision still contains the Cluster — only keep saves them.
#
#  3. FAIL CLOSED. Migrations never re-run, so a swallowed error permanently
#     leaves at-risk tenants exposed. `for ns in $(kubectl ...)` does not trip
#     errexit: on failure the loop runs zero times and the script stamps the
#     version anyway. Only "the resource type is not served" (no CNPG at all) and
#     "gone between scan and read" may be treated as empty.
#
# These drive the real migration scripts end-to-end against a fake kubectl
# (hack/testdata/migration-seaweedfs-db/), mocking only the cluster boundary.
#
# SHELL. Production runs these under /bin/sh = busybox ash: the migrations image
# is FROM alpine, and run-migrations.sh execs `/migrations/<n>` BY PATH, so the
# kernel honours the `#!/bin/sh` shebang. `set -euo pipefail` and
# errexit-in-function semantics differ from bash, and pipefail is load-bearing —
# lib/cozystack-version.sh pipes the rendered manifest into `kubectl apply`, so
# without it a failed render stamps the version from empty input. Asserting
# fail-closed in a shell that never runs it would be asserting nothing.
#
# So these tests do not invoke the migrations through the runner's shell at all:
# run_migration() executes them by path inside the image's own pinned base, read
# from the migrations Dockerfile. Same base image, same interpreter, same
# invocation form — production, rather than an approximation of it. This needs a
# working docker; there is deliberately no host-shell fallback, because every
# fallback available is a shell production never uses.
#
# What that replaced was `sh "$MIG_DIR/<n>"`, which is neither production nor
# bash: on the GitHub ubuntu runner /bin/sh is dash, which has no `set -o
# pipefail` and aborts on line 1 with "set: Illegal option -o pipefail" before
# the script does any work, while on a developer box /bin/sh is often bash, which
# runs green and proves nothing about ash.
#
# CI runs this file under Bats through `make bats-unit-tests`. It also remains
# compatible with the legacy `hack/cozytest.sh` translator, whose awk parser
# recognizes only @test blocks and a bare `}` on its own line. The tests avoid
# `run`, `$status`, and setup() for that compatibility path.
#
# Run with: bats hack/migration-seaweedfs-db-adopt.bats
# -----------------------------------------------------------------------------

load test_helper

FAKEBIN="$PWD/hack/testdata/migration-seaweedfs-db"
MIG_DIR="$PWD/packages/core/platform/images/migrations/migrations"

# The production base image, read out of the migrations Dockerfile rather than
# repeated here, so the interpreter under test cannot drift from the one the
# migrations actually ship on when that pin is bumped.
ALPINE=$(sed -n 's/^FROM \(alpine:[^ ]*\).*$/\1/p' \
  "$PWD/packages/core/platform/images/migrations/Dockerfile" | head -1)

# run_migration <n> -- run migrations/<n> the way run-migrations.sh does.
#
# By path, not `sh <file>`: that is what makes the shebang, and therefore the
# interpreter, part of what is under test. The fake kubectl goes on PATH inside
# the container and $WORK is bind-mounted, so $FAKE_CMDLOG is the same file the
# assertions read back on the host. --network none because nothing here may
# reach a real cluster; --user keeps $WORK removable by the test afterwards.
#
# The explicit `return` is load-bearing: cozytest.sh's awk generator rewrites
# every bare `}` in column 0 into `return 0` + `}`, so a helper that falls off
# its own end returns 0 no matter what it ran, and every fail-closed assertion
# below would pass vacuously. Capture the status and return it by hand.
run_migration() {
  _run_migration_rc=0
  docker run --rm --network none \
    --user "$(id -u):$(id -g)" \
    -v "$MIG_DIR:/migrations:ro" \
    -v "$FAKEBIN:/fakebin:ro" \
    -v "$WORK:/work" \
    -e PATH=/fakebin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
    -e FAKE_CMDLOG=/work/cmdlog \
    -e NAMESPACE="${NAMESPACE-}" \
    -e FAKE_CLUSTERS="${FAKE_CLUSTERS-}" \
    -e FAKE_LIST_FAIL="${FAKE_LIST_FAIL-}" \
    -e FAKE_GET_FAIL="${FAKE_GET_FAIL-}" \
    -e FAKE_ANNOTATE_FAIL="${FAKE_ANNOTATE_FAIL-}" \
    "$ALPINE" "/migrations/$1" || _run_migration_rc=$?
  return "$_run_migration_rc"
}

# prep resets env to a clean scenario. Tests set FAKE_* afterwards.
prep() {
  # Fail here rather than at the first docker run, so the reason is legible.
  docker info >/dev/null 2>&1 || {
    echo "docker is required: these tests run the migrations inside $ALPINE," >&2
    echo "the base image of the migrations image, so that they exercise busybox" >&2
    echo "ash — the interpreter run-migrations.sh actually gives them." >&2
    return 1
  }
  chmod +x "$FAKEBIN/kubectl"
  WORK=$(mktemp -d)
  export FAKE_CMDLOG="$WORK/cmdlog"
  : > "$FAKE_CMDLOG"
  export NAMESPACE=cozy-system
  export FAKE_CLUSTERS=""
  unset FAKE_LIST_FAIL FAKE_GET_FAIL FAKE_ANNOTATE_FAIL || true
}

# --- 1. instance name -------------------------------------------------------

@test "hands over a default-named instance (seaweedfs-system -> seaweedfs-db)" {
  prep
  export FAKE_CLUSTERS="tenant-root seaweedfs-system -"
  rc=0
  run_migration 43 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  [ "$rc" -eq 0 ]
  grep -qF -- "ANNOTATE tenant-root release-name=seaweedfs-db resource-policy=keep" "$FAKE_CMDLOG"
  # Migration 43 stamps 44 — asserting the number, not a bare "STAMP": a wrong
  # version would loop run-migrations.sh forever.
  grep -qF -- "STAMP 44" "$FAKE_CMDLOG"
  rm -rf "$WORK"
}

# THE original regression. Unfixed (owner compared against the literal
# "seaweedfs-system") this namespace is skipped entirely: no ANNOTATE line, and
# the Cluster is left with no keep for the foo-system upgrade to prune.
@test "hands over a NON-default instance name (foo-system -> foo-db)" {
  prep
  export FAKE_CLUSTERS="tenant-named foo-system -"
  rc=0
  run_migration 43 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  [ "$rc" -eq 0 ]
  grep -qF -- "ANNOTATE tenant-named release-name=foo-db resource-policy=keep" "$FAKE_CMDLOG"
  rm -rf "$WORK"
}

@test "migration 53 repairs a non-default instance the hardcoded 43 skipped, and stamps 54" {
  prep
  export FAKE_CLUSTERS="tenant-named foo-system -"
  rc=0
  run_migration 53 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  [ "$rc" -eq 0 ]
  grep -qF -- "ANNOTATE tenant-named release-name=foo-db resource-policy=keep" "$FAKE_CMDLOG"
  grep -qF -- "STAMP 54" "$FAKE_CMDLOG"
  rm -rf "$WORK"
}

@test "handles a mixed fleet: every -system owner is handed over, in its own namespace" {
  prep
  # The shape of the upgrade stand: default-named tenants plus one `foo`.
  export FAKE_CLUSTERS="tenant-root seaweedfs-system -
tenant-dsplit seaweedfs-system -
tenant-l seaweedfs-system -
tenant-named foo-system -"
  rc=0
  run_migration 53 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  [ "$rc" -eq 0 ]
  grep -qF -- "ANNOTATE tenant-root release-name=seaweedfs-db resource-policy=keep" "$FAKE_CMDLOG"
  grep -qF -- "ANNOTATE tenant-dsplit release-name=seaweedfs-db resource-policy=keep" "$FAKE_CMDLOG"
  grep -qF -- "ANNOTATE tenant-l release-name=seaweedfs-db resource-policy=keep" "$FAKE_CMDLOG"
  grep -qF -- "ANNOTATE tenant-named release-name=foo-db resource-policy=keep" "$FAKE_CMDLOG"
  [ "$(grep -c 'ANNOTATE' "$FAKE_CMDLOG")" -eq 4 ]
  rm -rf "$WORK"
}

# --- 2. ownership is not safety --------------------------------------------

# A <name>-db-owned Cluster WITHOUT keep is exposed, not done: <name>-system's
# prune baseline may still list the Cluster (live on the stand for tenant-l and
# tenant-root), in which case its next reconcile deletes it. Skipping on
# ownership alone — the shape the previous revision of this helper shipped —
# leaves the database to be pruned.
@test "protects a <name>-db-owned Cluster that is still missing keep" {
  prep
  export FAKE_CLUSTERS="tenant-named foo-db -"
  rc=0
  run_migration 53 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  [ "$rc" -eq 0 ]
  grep -qF -- "ANNOTATE tenant-named release-name=<unset> resource-policy=keep" "$FAKE_CMDLOG"
  rm -rf "$WORK"
}

@test "protects a default-named <name>-db-owned Cluster that is still missing keep" {
  prep
  export FAKE_CLUSTERS="tenant-fresh seaweedfs-db -"
  rc=0
  run_migration 53 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  [ "$rc" -eq 0 ]
  grep -qF -- "ANNOTATE tenant-fresh release-name=<unset> resource-policy=keep" "$FAKE_CMDLOG"
  rm -rf "$WORK"
}

@test "idempotent: a Cluster already owned by <name>-db AND carrying keep is left alone" {
  prep
  export FAKE_CLUSTERS="tenant-root seaweedfs-db keep
tenant-named foo-db keep"
  rc=0
  run_migration 53 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  [ "$rc" -eq 0 ]
  if grep -q 'ANNOTATE' "$FAKE_CMDLOG"; then echo "FAIL: no Cluster must have been re-annotated"; false; fi
  grep -qF -- "STAMP 54" "$FAKE_CMDLOG"
  rm -rf "$WORK"
}

@test "leaves a Cluster with no Helm owner annotation alone, but says so" {
  prep
  # Not Helm-managed. Guessing an owner would be worse than doing nothing, but
  # an unowned SeaweedFS database must not pass silently.
  export FAKE_CLUSTERS="tenant-manual - -"
  rc=0
  run_migration 53 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  [ "$rc" -eq 0 ]
  if grep -q 'ANNOTATE' "$FAKE_CMDLOG"; then echo "FAIL: no Cluster must have been re-annotated"; false; fi
  grep -qF -- "carries no meta.helm.sh/release-name" "$WORK/out"
  rm -rf "$WORK"
}

@test "leaves a Cluster owned by an unrelated release alone" {
  prep
  export FAKE_CLUSTERS="tenant-x some-other-release -"
  rc=0
  run_migration 53 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  [ "$rc" -eq 0 ]
  if grep -q 'ANNOTATE' "$FAKE_CMDLOG"; then echo "FAIL: no Cluster must have been re-annotated"; false; fi
  grep -qF -- "owned by unrelated release" "$WORK/out"
  rm -rf "$WORK"
}

@test "refuses a release literally named -system rather than annotating owner -db" {
  prep
  # "${current%-system}" would be empty, yielding release-name=-db, which no
  # release will ever claim: the Cluster would be orphaned by the very step
  # meant to protect it.
  export FAKE_CLUSTERS="tenant-weird -system -"
  rc=0
  run_migration 53 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  [ "$rc" -eq 0 ]
  if grep -q 'ANNOTATE' "$FAKE_CMDLOG"; then echo "FAIL: no Cluster must have been re-annotated"; false; fi
  grep -qF -- "no instance name" "$WORK/out"
  rm -rf "$WORK"
}

# --- 3. fail closed ---------------------------------------------------------

@test "a failing fleet scan aborts the migration instead of stamping past it" {
  prep
  export FAKE_CLUSTERS="tenant-named foo-system -"
  export FAKE_LIST_FAIL="Error from server (Timeout): the server was unable to return a response in the time allotted"
  rc=0
  run_migration 53 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  # Must propagate: the Job retries rather than advancing the version.
  [ "$rc" -ne 0 ]
  grep -qF -- "refusing to stamp past an unverified fleet" "$WORK/out"
  if grep -q 'STAMP' "$FAKE_CMDLOG"; then echo "FAIL: no version must have been stamped"; false; fi
  rm -rf "$WORK"
}

@test "an unreadable owner annotation aborts rather than being read as not-Helm-managed" {
  prep
  export FAKE_CLUSTERS="tenant-named foo-system -"
  export FAKE_GET_FAIL="Error from server (Forbidden): clusters.postgresql.cnpg.io \"seaweedfs-db\" is forbidden"
  rc=0
  run_migration 53 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  [ "$rc" -ne 0 ]
  grep -qF -- "cannot read the Helm owner" "$WORK/out"
  if grep -q 'STAMP' "$FAKE_CMDLOG"; then echo "FAIL: no version must have been stamped"; false; fi
  rm -rf "$WORK"
}

@test "a failed hand-over aborts rather than stamping a half-migrated fleet" {
  prep
  export FAKE_CLUSTERS="tenant-named foo-system -"
  export FAKE_ANNOTATE_FAIL="Error from server (Conflict): the object has been modified"
  rc=0
  run_migration 53 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  [ "$rc" -ne 0 ]
  if grep -q 'STAMP' "$FAKE_CMDLOG"; then echo "FAIL: no version must have been stamped"; false; fi
  rm -rf "$WORK"
}

# The fail-open that IS load-bearing: a cluster with no CNPG at all must not be
# blocked from upgrading. "The server doesn't have a resource type" is the only
# list failure allowed to mean "nothing to do".
@test "a cluster with no CNPG resource type stamps cleanly without annotating" {
  prep
  export FAKE_LIST_FAIL="error: the server doesn't have a resource type \"cluster\" in group \"postgresql.cnpg.io\""
  rc=0
  run_migration 53 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  [ "$rc" -eq 0 ]
  grep -qF -- "resource type is not served" "$WORK/out"
  if grep -q 'ANNOTATE' "$FAKE_CMDLOG"; then echo "FAIL: no Cluster must have been re-annotated"; false; fi
  grep -qF -- "STAMP 54" "$FAKE_CMDLOG"
  rm -rf "$WORK"
}

@test "an empty fleet stamps without annotating" {
  prep
  export FAKE_CLUSTERS=""
  rc=0
  run_migration 53 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  [ "$rc" -eq 0 ]
  if grep -q 'ANNOTATE' "$FAKE_CMDLOG"; then echo "FAIL: no Cluster must have been re-annotated"; false; fi
  grep -qF -- "STAMP 54" "$FAKE_CMDLOG"
  rm -rf "$WORK"
}