Ctrl K
ComunidadGuías…

hack/redis-backup-strategy.bats

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

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bats
# Behavioural tests for the shipped Redis backup/restore strategy script,
# extracted from the rendered cozy-default-redis Strategy CR and EXECUTED with
# stubbed redis-cli / curl. helm-unittest (tests/strategy_redis_test.yaml) pins
# the chart's render-time decisions (volumes, env, deadline); the script's own
# behaviour — which mode reaches which command — is a contract between the
# running Pod, Redis and S3, so it is exercised here rather than string-matched.
#
# The load-bearing case is cleanup: a deleted Backup renders MODE=cleanup, and
# it must delete the object and NEVER reach the restore path's FLUSHALL against
# the live master. Blanking the object key or breaking the rfs-redis- sentinel
# prefix is caught the same way — by what the stubs are actually called with.
#
# Run from the repo root with make bats-unit-tests. To retain compatibility
# with cozytest, assertions precede inline cleanup: its translator would
# replace a final assertion's status with return 0 at a lone closing brace.
#
# The rendered script is run with `bash`, not `sh`: it opens `set -euo
# pipefail`, which the production image's busybox ash supports but the CI
# runner's /bin/sh (dash) rejects at line 1. bash matches ash's pipefail
# semantics; same reason hack/migration-50-etcd-adopt.bats runs its script
# with bash.

load test_helper

CHART=packages/system/backupstrategy-controller

# Render the shipped strategy's in-Pod script into $1, substitute the
# controller-render placeholders (Mode, Release.Name, ObjectKey, Release.Prefix)
# with the given values ($2 mode, $3 app, $4 object key, $6 chart prefix,
# redis- by default), and rebind every absolute path it touches into $5 so it
# runs unprivileged. An empty render is an early failure, as is a placeholder
# left unsubstituted, and no real /etc or /tmp path may survive the rebinding —
# these tests EXECUTE the script.
render_script() {
  out=$1; mode=$2; app=$3; key=$4; sandbox=$5; prefix=${6:-redis-}
  mkdir -p "$sandbox/redis-ca" "$sandbox/s3-ca" "$sandbox/restore"
  helm template t "$CHART" -n cozy-backup-controller \
    --set backupStorage.bucketNameOverride=test-bucket 2>/dev/null \
    | yq eval 'select(.kind == "Redis") | .spec.template.spec.containers[0].args[0]' - \
    | sed -e "s#{{ .Mode }}#${mode}#g" \
          -e "s#{{ .Release.Prefix | default \"redis-\" }}#${prefix}#g" \
          -e "s#{{ .Release.Name }}#${app}#g" \
          -e "s#{{ .ObjectKey }}#${key}#g" \
          -e "s#/etc/redis-ca#${sandbox}/redis-ca#g" \
          -e "s#/etc/s3-ca#${sandbox}/s3-ca#g" \
          -e "s#/tmp/restore#${sandbox}/restore#g" \
          -e "s#/tmp/dump.rdb#${sandbox}/dump.rdb#g" > "$out"
  [ -s "$out" ] || return 1
  if grep -q '{{' "$out"; then
    echo "rendered script still holds a controller placeholder" >&2
    return 1
  fi
  # The exact absolute paths the rebinding above replaces; a survivor means the
  # script grew a new one and the test would touch the real filesystem. Matched
  # literally (not a bare /tmp/, which the sandbox itself lives under in CI).
  if grep -qE '/etc/redis-ca|/etc/s3-ca|/tmp/dump\.rdb|/tmp/restore' "$out"; then
    echo "rendered script still references a real /etc or /tmp path after rebinding" >&2
    return 1
  fi
}

# redis-cli / curl stubs that log every invocation, put first on PATH. The
# sentinel query answers with a master addr so backup/restore proceed; nothing
# actually talks to Redis or S3.
make_stubs() {
  bin=$1; rclog=$2; curllog=$3
  mkdir -p "$bin"
  cat > "$bin/redis-cli" <<STUB
#!/bin/sh
echo "\$*" >> "$rclog"
case "\$*" in
  *"get-master-addr-by-name"*) printf '127.0.0.1\n6379\n' ;;
  *"INFO persistence"*) printf 'aof_rewrite_in_progress:0\naof_last_bgrewrite_status:ok\n' ;;
esac
exit 0
STUB
  cat > "$bin/curl" <<STUB
#!/bin/sh
echo "\$*" >> "$curllog"
# Cleanup asks for the HTTP status with -w; a 204 is a successful delete.
case "\$*" in *"-w %{http_code}"*|*"-w"*) echo 204 ;; esac
exit 0
STUB
  cat > "$bin/redis-server" <<'STUB'
#!/bin/sh
exit 0
STUB
  chmod 0755 "$bin/redis-cli" "$bin/curl" "$bin/redis-server"
}

s3_env() {
  export S3_ENDPOINT=s3.example.com S3_BUCKET=bkt S3_REGION=us-east-1
  export AWS_ACCESS_KEY_ID=ak AWS_SECRET_ACCESS_KEY=sk S3_INSECURE_SKIP_VERIFY=false
}

@test "an empty render fails the fixture instead of passing as a no-op" {
  tmp=$(mktemp -d)
  CHART=$tmp/no-such-chart
  if render_script "$tmp/s.sh" cleanup app key "$tmp/sb" 2>/dev/null; then
    echo "FAIL: render_script reported success with no chart"
    false
  fi
}

@test "cleanup deletes the object and never FLUSHALLs the live master" {
  tmp=$(mktemp -d)
  make_stubs "$tmp/bin" "$tmp/rc.log" "$tmp/curl.log"
  render_script "$tmp/s.sh" cleanup myapp "tenant-x/myapp/bkp-42.rdb" "$tmp/sb"
  s3_env
  PATH="$tmp/bin:$PATH" bash "$tmp/s.sh"
  # A DELETE against exactly this backup's object.
  grep -q -- '-X DELETE' "$tmp/curl.log"
  grep -q 'tenant-x/myapp/bkp-42.rdb' "$tmp/curl.log"
  # The destructive branch must be unreachable in cleanup: no FLUSHALL, no
  # --pipe, and in fact no Redis contact at all. This is the assertion the
  # round's cleanup arm exists to hold.
  if grep -qi 'FLUSHALL' "$tmp/rc.log"; then echo "FAIL: cleanup must not flush the master"; false; fi
  if grep -q -- '--pipe' "$tmp/rc.log"; then echo "FAIL: cleanup must not pipe a restore into the master"; false; fi
  [ ! -s "$tmp/rc.log" ]
}

@test "backup resolves rfs-redis-<app> and uploads under the recorded object key" {
  tmp=$(mktemp -d)
  make_stubs "$tmp/bin" "$tmp/rc.log" "$tmp/curl.log"
  render_script "$tmp/s.sh" backup myapp "tenant-x/myapp/bkp-42.rdb" "$tmp/sb"
  s3_env
  PATH="$tmp/bin:$PATH" bash "$tmp/s.sh"
  # Sentinel host carries the rfs-redis- prefix owned by the redis chart.
  grep -q -- '-h rfs-redis-myapp -p 26379' "$tmp/rc.log"
  grep -q 'get-master-addr-by-name mymaster' "$tmp/rc.log"
  # The RDB is uploaded to exactly the recorded object key, so a Plan keeps one
  # object per run.
  grep -q -- '-X PUT' "$tmp/curl.log"
  grep -q 'tenant-x/myapp/bkp-42.rdb' "$tmp/curl.log"
  # Backup must never wipe the source.
  if grep -qi 'FLUSHALL' "$tmp/rc.log"; then echo "FAIL: backup must not flush the source"; false; fi
}

@test "a Valkey backup resolves rfs-valkey-<app>, the valkey chart's sentinel" {
  tmp=$(mktemp -d)
  make_stubs "$tmp/bin" "$tmp/rc.log" "$tmp/curl.log"
  render_script "$tmp/s.sh" backup myapp "tenant-x/myapp/bkp-42.rdb" "$tmp/sb" valkey-
  s3_env
  PATH="$tmp/bin:$PATH" bash "$tmp/s.sh"
  grep -q -- '-h rfs-valkey-myapp -p 26379' "$tmp/rc.log"
  grep -q -- '-X PUT' "$tmp/curl.log"
  if grep -q 'rfs-redis-' "$tmp/rc.log"; then echo "FAIL: a Valkey backup must not query the redis sentinel"; false; fi
}

@test "an unrecognised mode exits non-zero without touching the master" {
  tmp=$(mktemp -d)
  make_stubs "$tmp/bin" "$tmp/rc.log" "$tmp/curl.log"
  render_script "$tmp/s.sh" bogus myapp "tenant-x/myapp/bkp-42.rdb" "$tmp/sb"
  s3_env
  # No bats `run` helper under hack/cozytest.sh, so capture the status directly.
  if PATH="$tmp/bin:$PATH" bash "$tmp/s.sh" 2>/dev/null; then
    echo "FAIL: an unrecognised mode must exit non-zero"
    false
  fi
  # The unknown mode must fail closed, never reaching the restore FLUSHALL.
  if grep -qi 'FLUSHALL' "$tmp/rc.log"; then echo "FAIL: an unrecognised mode must not reach the restore flush"; false; fi
  [ -f "$tmp/rc.log" ]
}