Ctrl K
ComunidadGuías…

hack/migration-57-seaweedfs-size-limit.bats

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

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bats
# -----------------------------------------------------------------------------
# Unit tests for platform migration 57 --> 58 (keep every existing SeaweedFS
# instance at a master volume size limit of 1000 unless it already names one or
# its master is not an object, before the app chart that defaults it to 400
# reaches it).
#
# Five properties are pinned here:
#
#  1. THE RIGHT INSTANCES. The fleet list uses the selector cozystack-api finds
#     SeaweedFS with, across all namespaces. The fake kubectl refuses any other
#     list instead of answering it with nothing, because an empty fleet is a
#     legitimate answer: a mistyped selector would otherwise stay green and
#     leave every production instance unpinned.
#
#  2. ONLY WHERE NOT CHOSEN, AND EXACTLY THIS PATCH. A release with no
#     spec.values (the tenant module's shape), one whose values lack master,
#     one whose master lacks the key, and one holding a null, a string, a zero,
#     a fraction or a number past 30000 are each patched, in their own
#     namespace, with exactly
#     {"spec":{"values":{"master":{"volumeSizeLimitMB":1000}}}}. The previous
#     chart ignored every one of those and the new schema refuses the last
#     four. A release naming a whole number from 1 to 30000 is left alone,
#     edges included, and named in the Job log, since the value now takes
#     effect, unless the number is 1000, which changes nothing and is what a
#     retried Job finds on the releases it already pinned. A spec.values that
#     is not an object is patched too, since helm-controller ignores it and the
#     instance runs on the chart's defaults. A master that is not an object is
#     not patched, and a valuesFrom beyond the cozystack-values Secret does not
#     stop the patch. Each of the three is named in the Job log with what it
#     holds, and the releases after it are still reached.
#
#  3. FAIL CLOSED ON THE LIST. A failed list aborts before the version stamp, so
#     the platform upgrade stops before any instance can reach the new chart.
#
#  4. FAIL CLOSED ON THE PATCH. Migrations never re-run, so a swallowed patch
#     error would stamp past an instance that then drops to 400.
#
#  5. THE RUNBOOK AGREES. The two steps of
#     docs/operations/seaweedfs-lowering-the-volume-size-limit.md that restate
#     this selection, the listing every variant runs before upgrading and the
#     pinning the default variant runs instead of this migration, found by the
#     HTML comment above each, pick the same releases on the same fleet, and
#     the pinning step stops at a failed patch before its closing line.
#
# These drive the real migration script end-to-end against a fake kubectl
# (hack/testdata/migration-57-seaweedfs-size-limit/), mocking only the cluster
# boundary.
#
# SHELL. Production runs the migration under /bin/sh = busybox ash, by path, so
# run_migration() runs it by path inside the migrations image's own pinned base
# with jq added, --network none. See hack/migration-54-redis-adopt.bats for the
# full reasoning; it applies unchanged.
#
# cozytest.sh's awk parser recognizes only @test blocks and a bare `}` on its own
# line, rewriting the latter into `return 0` + `}`. A helper whose exit status
# matters must capture it and `return` it by hand before its closing brace.
#
# Run with: hack/cozytest.sh hack/migration-57-seaweedfs-size-limit.bats
# -----------------------------------------------------------------------------

load test_helper

FAKEBIN="$PWD/hack/testdata/migration-57-seaweedfs-size-limit"
MIG_DIR="$PWD/packages/core/platform/images/migrations/migrations"
RUNBOOK="$PWD/docs/operations/seaweedfs-lowering-the-volume-size-limit.md"
SELECTOR='apps.cozystack.io/application.kind=SeaweedFS,apps.cozystack.io/application.group=apps.cozystack.io'
PIN='{"spec":{"values":{"master":{"volumeSizeLimitMB":1000}}}}'

ALPINE=$(sed -n 's/^FROM \(alpine:[^ ]*\).*$/\1/p' \
  "$PWD/packages/core/platform/images/migrations/Dockerfile" | head -1)

TESTIMG="cozystack-migration57-test:$(printf '%s' "$ALPINE" | sed 's/[^a-zA-Z0-9]/-/g')"

# run_in_image <command...> -- run a command in the test image against the fake
# kubectl, with $WORK mounted at /work.
run_in_image() {
  _run_in_image_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_HRS="${FAKE_HRS-}" \
    -e FAKE_LIST_FAIL="${FAKE_LIST_FAIL-}" \
    -e FAKE_PATCH_FAIL="${FAKE_PATCH_FAIL-}" \
    -e FAKE_VALUESFROM="${FAKE_VALUESFROM-}" \
    "$TESTIMG" "$@" || _run_in_image_rc=$?
  return "$_run_in_image_rc"
}

run_migration() {
  run_in_image "/migrations/$1"
  return $?
}

# runbook_step <marker> -- copy the sh block that follows <!-- <marker> --> in
# the runbook to $WORK/<marker>.sh, failing if there is none.
runbook_step() {
  awk -v m="<!-- $1 -->" '$0 == m { f = 1; next } f && /^```sh$/ { b = 1; next } b && /^```$/ { exit } b' \
    "$RUNBOOK" > "$WORK/$1.sh"
  grep -q 'kubectl get helmreleases' "$WORK/$1.sh"
  return $?
}

prep() {
  docker info >/dev/null 2>&1 || {
    echo "docker is required: these tests run migration 57 inside a jq-enabled" >&2
    echo "build of $ALPINE, the migrations image's base." >&2
    return 1
  }
  docker build -q -t "$TESTIMG" - >/dev/null <<DOCKERFILE
FROM $ALPINE
RUN apk add --no-cache jq
DOCKERFILE
  chmod +x "$FAKEBIN/kubectl"
  WORK=$(mktemp -d)
  export FAKE_CMDLOG="$WORK/cmdlog"
  : > "$FAKE_CMDLOG"
  export NAMESPACE=cozy-system
  export FAKE_HRS=""
  unset FAKE_LIST_FAIL FAKE_PATCH_FAIL FAKE_VALUESFROM || true
  return 0
}

# patch_for <ns> <name> -- the merge-patch body sent for that release, from its
# `PATCH <ns> <name> <json>` cmdlog line. Used only for its stdout.
patch_for() {
  grep -E "^PATCH $1 $2 " "$FAKE_CMDLOG" | head -1 | cut -d' ' -f4-
}

# listed_with_selector -- the fleet list was made once, with the exact selector.
listed_with_selector() {
  _n=$(grep -cxF -- "KUBECTL get helmreleases.helm.toolkit.fluxcd.io --all-namespaces -l $SELECTOR -o json" "$FAKE_CMDLOG")
  [ "$_n" -eq 1 ]
  return $?
}

# --- 1 and 2. the right instances, only where not chosen, exactly this patch -

@test "pins every instance that has not chosen a limit and leaves the rest alone" {
  prep
  # One row per shape. The first eight have not chosen a limit the new chart
  # accepts and must be pinned; the last six must not be touched.
  export FAKE_HRS='tenant-root seaweedfs -
tenant-api seaweedfs {"host":"s3.example.org"}
tenant-rep seaweedfs {"master":{"replicas":3}}
tenant-null seaweedfs {"master":{"volumeSizeLimitMB":null}}
tenant-str seaweedfs {"master":{"volumeSizeLimitMB":"400"}}
tenant-zero seaweedfs {"master":{"volumeSizeLimitMB":0}}
tenant-frac seaweedfs {"master":{"volumeSizeLimitMB":400.5}}
tenant-big seaweedfs {"master":{"volumeSizeLimitMB":30001}}
tenant-1 seaweedfs {"master":{"volumeSizeLimitMB":1}}
tenant-400 seaweedfs {"master":{"volumeSizeLimitMB":400}}
tenant-30000 seaweedfs {"master":{"volumeSizeLimitMB":30000}}
tenant-2000 seaweedfs {"master":{"volumeSizeLimitMB":2000}}
tenant-1000 seaweedfs {"master":{"volumeSizeLimitMB":1000}}
tenant-odd seaweedfs {"master":"unexpected"}'
  rc=0
  run_migration 57 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  [ "$rc" -eq 0 ]
  listed_with_selector

  [ "$(grep -cE '^PATCH ' "$FAKE_CMDLOG")" -eq 8 ]
  for ns in tenant-root tenant-api tenant-rep tenant-null tenant-str tenant-zero tenant-frac tenant-big; do
    [ "$(patch_for "$ns" seaweedfs)" = "$PIN" ]
  done
  for ns in tenant-1 tenant-400 tenant-30000 tenant-2000 tenant-1000 tenant-odd; do
    [ -z "$(patch_for "$ns" seaweedfs)" ]
  done
  # A patch sent without its namespace would land in the Job's own namespace.
  [ "$(grep -cE '^PATCH <none> ' "$FAKE_CMDLOG")" -eq 0 ]

  # Every kept value but 1000 is named in the Job log with the number it now
  # applies, since the previous chart ignored it; nothing else is.
  for kept in tenant-1:1 tenant-400:400 tenant-30000:30000 tenant-2000:2000; do
    grep -qF -- "Keeping SeaweedFS ${kept%%:*}/seaweedfs at master.volumeSizeLimitMB=${kept#*:}:" "$WORK/out"
  done
  [ "$(grep -c '^Keeping SeaweedFS ' "$WORK/out")" -eq 4 ]

  grep -qxF -- "STAMP 58" "$FAKE_CMDLOG"
  rm -rf "$WORK"
}

@test "a release whose spec.values is not an object does not stop the fleet" {
  prep
  export FAKE_HRS='tenant-bad seaweedfs "oops"
tenant-root seaweedfs -
tenant-list seaweedfs ["a"]
tenant-api seaweedfs {"host":"s3.example.org"}'
  rc=0
  run_migration 57 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  [ "$rc" -eq 0 ]
  listed_with_selector
  for ns in tenant-root tenant-api; do
    [ "$(patch_for "$ns" seaweedfs)" = "$PIN" ]
  done
  for ns in tenant-bad tenant-list; do
    [ "$(patch_for "$ns" seaweedfs)" = "$PIN" ]
  done
  grep -qF -- 'WARNING: SeaweedFS tenant-bad/seaweedfs has a spec.values that is not an object' "$WORK/out"
  grep -qF -- 'It held: "oops"' "$WORK/out"
  grep -qF -- 'WARNING: SeaweedFS tenant-list/seaweedfs has a spec.values that is not an object' "$WORK/out"
  grep -qF -- 'It held: ["a"]' "$WORK/out"
  [ "$(grep -c '^WARNING: ' "$WORK/out")" -eq 2 ]
  grep -qxF -- "STAMP 58" "$FAKE_CMDLOG"
  rm -rf "$WORK"
}

@test "a master that is not an object is named and left unpinned" {
  prep
  export FAKE_HRS='tenant-odd seaweedfs {"master":"unexpected"}
tenant-root seaweedfs -
tenant-olist seaweedfs {"master":[400]}
tenant-api seaweedfs {"host":"s3.example.org"}'
  rc=0
  run_migration 57 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  [ "$rc" -eq 0 ]
  for ns in tenant-root tenant-api; do
    [ "$(patch_for "$ns" seaweedfs)" = "$PIN" ]
  done
  for ns in tenant-odd tenant-olist; do
    [ -z "$(patch_for "$ns" seaweedfs)" ]
  done
  grep -qF -- 'WARNING: SeaweedFS tenant-odd/seaweedfs is not pinned: its master is not an object' "$WORK/out"
  grep -qF -- 'master holds: "unexpected"' "$WORK/out"
  grep -qF -- 'WARNING: SeaweedFS tenant-olist/seaweedfs is not pinned: its master is not an object' "$WORK/out"
  grep -qF -- 'master holds: [400]' "$WORK/out"
  [ "$(grep -c '^WARNING: ' "$WORK/out")" -eq 2 ]
  grep -qxF -- "STAMP 58" "$FAKE_CMDLOG"
  rm -rf "$WORK"
}

@test "a valuesFrom beyond the cozystack-values Secret is named and does not stop the pin" {
  prep
  export FAKE_HRS='tenant-vf seaweedfs -
tenant-vfkeep seaweedfs {"master":{"volumeSizeLimitMB":400}}
tenant-std seaweedfs -
tenant-nofrom seaweedfs -'
  export FAKE_VALUESFROM='tenant-vf [{"kind":"Secret","name":"cozystack-values"},{"kind":"ConfigMap","name":"limits","valuesKey":"limit","targetPath":"master.volumeSizeLimitMB"}]
tenant-vfkeep [{"kind":"ConfigMap","name":"limits"}]
tenant-nofrom -'
  rc=0
  run_migration 57 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  [ "$rc" -eq 0 ]
  for ns in tenant-vf tenant-std tenant-nofrom; do
    [ "$(patch_for "$ns" seaweedfs)" = "$PIN" ]
  done
  [ -z "$(patch_for tenant-vfkeep seaweedfs)" ]
  # Only the entries besides the platform's own Secret are listed.
  grep -qxF -- 'WARNING: SeaweedFS tenant-vf/seaweedfs also takes values from [{"kind":"ConfigMap","name":"limits","valuesKey":"limit","targetPath":"master.volumeSizeLimitMB"}]. spec.values is merged over them, so a master.volumeSizeLimitMB set there without targetPath does not apply, and one set through targetPath can, depending on the entries before it' "$WORK/out"
  grep -qF -- 'WARNING: SeaweedFS tenant-vfkeep/seaweedfs also takes values from [{"kind":"ConfigMap","name":"limits"}].' "$WORK/out"
  [ "$(grep -c '^WARNING: ' "$WORK/out")" -eq 2 ]
  grep -qxF -- "STAMP 58" "$FAKE_CMDLOG"
  rm -rf "$WORK"
}

@test "an empty fleet stamps 58 without patching anything" {
  prep
  export FAKE_HRS=""
  rc=0
  run_migration 57 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  [ "$rc" -eq 0 ]
  # The list was really made, with the right selector: an empty answer to a
  # list that never happened would look the same from here.
  listed_with_selector
  [ "$(grep -cE '^PATCH ' "$FAKE_CMDLOG")" -eq 0 ]
  grep -qxF -- "STAMP 58" "$FAKE_CMDLOG"
  rm -rf "$WORK"
}

# --- 3. fail closed on the list ----------------------------------------------

@test "a failing fleet list aborts before stamping" {
  prep
  export FAKE_HRS='tenant-root seaweedfs -'
  export FAKE_LIST_FAIL=1
  rc=0
  run_migration 57 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  [ "$rc" -ne 0 ]
  [ "$(grep -cE '^PATCH ' "$FAKE_CMDLOG")" -eq 0 ]
  [ "$(grep -cE '^STAMP ' "$FAKE_CMDLOG")" -eq 0 ]
  rm -rf "$WORK"
}

# --- 4. fail closed on the patch ---------------------------------------------

@test "a failed patch aborts before stamping" {
  prep
  export FAKE_HRS='tenant-root seaweedfs -
tenant-api seaweedfs {"host":"s3.example.org"}'
  export FAKE_PATCH_FAIL=1
  rc=0
  run_migration 57 >"$WORK/out" 2>&1 || rc=$?
  cat "$WORK/out"; cat "$FAKE_CMDLOG"
  [ "$rc" -ne 0 ]
  # The abort is the patch's: it was attempted, and nothing after it was.
  [ "$(grep -c ' patch helmreleases' "$FAKE_CMDLOG")" -eq 1 ]
  grep -qF -- "KUBECTL -n tenant-root patch helmreleases.helm.toolkit.fluxcd.io seaweedfs " "$FAKE_CMDLOG"
  [ "$(grep -cE '^STAMP ' "$FAKE_CMDLOG")" -eq 0 ]
  rm -rf "$WORK"
}

# --- 5. the runbook agrees ----------------------------------------------------

# One row per shape the selection tells apart.
RUNBOOK_FLEET='tenant-root seaweedfs -
tenant-bad seaweedfs "oops"
tenant-list seaweedfs ["a"]
tenant-api seaweedfs {"host":"s3.example.org"}
tenant-null seaweedfs {"master":{"volumeSizeLimitMB":null}}
tenant-str seaweedfs {"master":{"volumeSizeLimitMB":"400"}}
tenant-frac seaweedfs {"master":{"volumeSizeLimitMB":400.5}}
tenant-big seaweedfs {"master":{"volumeSizeLimitMB":30001}}
tenant-400 seaweedfs {"master":{"volumeSizeLimitMB":400}}
tenant-2000 seaweedfs {"master":{"volumeSizeLimitMB":2000}}
tenant-1000 seaweedfs {"master":{"volumeSizeLimitMB":1000}}
tenant-odd seaweedfs {"master":"unexpected"}
tenant-vf seaweedfs -
tenant-vfkeep seaweedfs {"master":{"volumeSizeLimitMB":400}}'
RUNBOOK_VALUESFROM='tenant-vf [{"kind":"Secret","name":"cozystack-values"},{"kind":"ConfigMap","name":"limits"}]
tenant-vfkeep [{"kind":"ConfigMap","name":"limits"}]'

# releases_in <file> <sed-expression> -- the sorted "<ns> <name>" pairs the
# expression prints from the file.
releases_in() {
  sed -n "$2" "$1" | sort
}

@test "the runbook's steps select the releases migration 57 does" {
  prep
  export FAKE_HRS="$RUNBOOK_FLEET" FAKE_VALUESFROM="$RUNBOOK_VALUESFROM"
  runbook_step migration-57-listing
  runbook_step migration-57-pin
  run_migration 57 >"$WORK/mig.out" 2>&1
  cp "$FAKE_CMDLOG" "$WORK/mig.log"; : > "$FAKE_CMDLOG"
  # stdout only: the xtrace of this runner goes to stderr, and the closing line
  # is checked by position.
  run_in_image sh /work/migration-57-listing.sh >"$WORK/listing.out" 2>"$WORK/listing.err"
  run_in_image sh /work/migration-57-pin.sh >"$WORK/pin.out" 2>"$WORK/pin.err"
  cp "$FAKE_CMDLOG" "$WORK/pin.log"
  cat "$WORK/mig.out" "$WORK/listing.out" "$WORK/pin.out"

  releases_in "$WORK/mig.log" 's/^PATCH \([^ ]*\) \([^ ]*\) .*/\1 \2/p' > "$WORK/a"
  releases_in "$WORK/pin.log" 's/^PATCH \([^ ]*\) \([^ ]*\) .*/\1 \2/p' > "$WORK/b"
  [ "$(wc -l < "$WORK/a")" -eq 9 ]
  cmp "$WORK/a" "$WORK/b"

  releases_in "$WORK/mig.out" 's/^Keeping SeaweedFS \([^/]*\)\/\([^ ]*\) at master.volumeSizeLimitMB=\([0-9]*\):.*/\1 \2 \3/p' > "$WORK/a"
  releases_in "$WORK/listing.out" 's/^changes //p' > "$WORK/b"
  [ "$(wc -l < "$WORK/a")" -eq 3 ]
  cmp "$WORK/a" "$WORK/b"

  for what in unreadable:'has a spec.values' unrenderable:'is not pinned' valuesfrom:'also takes values'; do
    releases_in "$WORK/mig.out" "s/^WARNING: SeaweedFS \\([^/]*\\)\\/\\([^ ]*\\) ${what#*:}.*/\\1 \\2/p" > "$WORK/a"
    releases_in "$WORK/listing.out" "s/^${what%%:*} \\([^ ]*\\) \\([^ ]*\\) .*/\\1 \\2/p" > "$WORK/b"
    releases_in "$WORK/pin.out" "s/^${what%%:*} \\([^ ]*\\) \\([^ ]*\\) .*/\\1 \\2/p" > "$WORK/c"
    [ -s "$WORK/a" ]
    cmp "$WORK/a" "$WORK/b"
    cmp "$WORK/a" "$WORK/c"
  done

  for out in listing pin; do
    [ "$(head -1 "$WORK/$out.out")" = "instances 14" ]
    [ "$(tail -1 "$WORK/$out.out")" = "complete 14" ]
  done
  rm -rf "$WORK"
}

@test "the runbook's pinning step stops at a failed patch without its closing line" {
  prep
  export FAKE_HRS="$RUNBOOK_FLEET" FAKE_VALUESFROM="$RUNBOOK_VALUESFROM"
  export FAKE_PATCH_FAIL=1
  runbook_step migration-57-pin
  run_in_image sh /work/migration-57-pin.sh >"$WORK/pin.out" 2>"$WORK/pin.err"
  cat "$WORK/pin.out" "$FAKE_CMDLOG"
  [ "$(grep -c ' patch helmreleases' "$FAKE_CMDLOG")" -eq 1 ]
  grep -qxF -- "stopped: patching tenant-root/seaweedfs failed" "$WORK/pin.out"
  [ "$(grep -c '^complete' "$WORK/pin.out")" -eq 0 ]
  rm -rf "$WORK"
}