Ctrl K
ComunidadGuías…

hack/altinity-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 Altinity (ClickHouse) backup/restore
# strategy script, extracted from the rendered cozy-default-altinity Strategy
# CR and EXECUTED against a stubbed clickhouse-backup HTTP API, with the real
# jq doing the archive selection. Which archive a restore reads back is
# decided by that selection over list/remote, so it is exercised against
# several archives rather than string-matched.
#
# The script runs with `bash`, not `sh`, for the reason given in
# hack/redis-backup-strategy.bats: it opens `set -euo pipefail`, which the
# production image's busybox ash supports and the CI runner's dash rejects.

load test_helper

CHART=packages/system/backupstrategy-controller

# Render the shipped strategy's in-Pod script into $1 and substitute the
# controller-render placeholders the way the driver fills them: $2 mode, $3
# app (release and source application), $4 the archive name recorded on the
# Backup (empty for a Backup that records none), $5 the Backup name, $6 its
# takenAt in the archive-name layout. An empty render fails early, as does a
# placeholder left unsubstituted.
render_script() {
  out=$1; mode=$2; app=$3; archive=$4; backup=$5; takenat=$6
  helm template t "$CHART" -n cozy-backup-controller \
    --set backupStorage.bucketNameOverride=test-bucket 2>/dev/null \
    | yq eval 'select(.kind == "Altinity") | .spec.template.spec.containers[0].args[0]' - \
    | sed -e "s#{{ .Mode }}#${mode}#g" \
          -e "s#{{ .Release.Name }}#${app}#g" \
          -e "s#{{ .Release.Namespace }}#tenant-x#g" \
          -e "s#{{ .Backup.ApplicationRef.Name }}#${app}#g" \
          -e "s#{{ .Backup.Name }}#${backup}#g" \
          -e "s#{{ .ArchiveName | default \"\" }}#${archive}#g" \
          -e "s#{{ .Backup.TakenAt | default \"\" }}#${takenat}#g" > "$out"
  [ -s "$out" ] || return 1
  if grep -q '{{' "$out"; then
    echo "rendered script still holds a controller placeholder" >&2
    return 1
  fi
}

# Stub the clickhouse-backup API behind curl, first on PATH. list/remote
# serves $state/remote; actions serves $state/actions plus one success row per
# operation the script has issued, and after its first read swaps in
# $state/actions.next when a test seeds one (an operation that finishes while
# the script waits). Every request is logged to $state/curl.log. sleep is
# stubbed to fail after a few calls, so a script that never sees its
# operation finish fails the test instead of hanging it.
make_stubs() {
  bin=$1; state=$2
  mkdir -p "$bin"
  : > "$state/remote"
  : > "$state/actions"
  cat > "$bin/curl" <<STUB
#!/usr/bin/env bash
set -eu
method=GET; url=""
for a in "\$@"; do
  case "\$a" in POST) method=POST ;; http://*) url="\$a" ;; esac
done
echo "\$method \$url" >> "$state/curl.log"
case "\$method \$url" in
  "GET "*/backup/status) echo '{}' ;;
  "GET "*/backup/list/remote) cat "$state/remote" ;;
  "GET "*/backup/actions)
    cat "$state/actions"
    if [ -f "$state/actions.done" ]; then cat "$state/actions.done"; fi
    if [ -f "$state/actions.next" ]; then mv "$state/actions.next" "$state/actions"; fi
    ;;
  "POST "*/backup/create_remote*)
    name=\${url#*name=}; name=\${name%%&*}
    printf '{"command":"create_remote --resume %s","status":"success","start":"2099-01-01 00:00:00"}\n' "\$name" >> "$state/actions.done"
    ;;
  "POST "*/backup/restore_remote/*)
    printf '{"command":"restore_remote %s","status":"success","start":"2099-01-01 00:00:00"}\n' "\${url##*/}" >> "$state/actions.done"
    ;;
  *) echo "unexpected request: \$method \$url" >&2; exit 22 ;;
esac
STUB
  cat > "$bin/sleep" <<STUB
#!/usr/bin/env bash
n=\$(cat "$state/sleeps" 2>/dev/null || echo 0)
echo \$((n + 1)) > "$state/sleeps"
if [ "\$n" -ge 3 ]; then echo "sleep budget spent: the operation never finished" >&2; exit 1; fi
STUB
  chmod 0755 "$bin/curl" "$bin/sleep"
  export API_USERNAME=u API_PASSWORD=p
}

# One list/remote row per argument, "name" or "name|desc"; created mirrors
# the name's timestamp, which is the order the pre-fix selection sorted by.
remote_rows() {
  for row in "$@"; do
    name=${row%%|*}; desc="tar, regular"
    if [ "$row" != "$name" ]; then desc=${row#*|}; fi
    ts=${name##*-}
    printf '{"name":"%s","created":"%s","location":"remote","required":"","desc":"%s"}\n' "$name" "$ts" "$desc"
  done
}

@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" restore myapp "" bkp 20260304T050622Z 2>/dev/null; then
    echo "FAIL: render_script reported success with no chart"
    false
  fi
  rm -rf "$tmp"
}

@test "restore reads back the archive the Backup recorded, not the newest one" {
  tmp=$(mktemp -d)
  make_stubs "$tmp/bin" "$tmp"
  render_script "$tmp/s.sh" restore myapp clickhouse-myapp-20260304T050607Z bkp-a 20260304T050622Z
  remote_rows clickhouse-myapp-20260304T040000Z clickhouse-myapp-20260304T050607Z \
    clickhouse-myapp-20260304T060000Z clickhouse-myapp-20260304T070000Z > "$tmp/remote"
  PATH="$tmp/bin:$PATH" bash "$tmp/s.sh"
  grep -qxF 'POST http://chi-clickhouse-myapp-clickhouse-0-0.tenant-x.svc:7171/backup/restore_remote/clickhouse-myapp-20260304T050607Z' "$tmp/curl.log"
  [ "$(grep -c '/backup/restore_remote/' "$tmp/curl.log")" -eq 1 ]
  rm -rf "$tmp"
}

@test "restore fails naming the recorded archive when remote storage lacks it" {
  tmp=$(mktemp -d)
  make_stubs "$tmp/bin" "$tmp"
  render_script "$tmp/s.sh" restore myapp clickhouse-myapp-20260304T050607Z bkp-a 20260304T050622Z
  remote_rows clickhouse-myapp-20260304T040000Z clickhouse-myapp-20260304T060000Z > "$tmp/remote"
  out=""
  if out=$(PATH="$tmp/bin:$PATH" bash "$tmp/s.sh" 2>&1); then
    echo "FAIL: restore succeeded without the recorded archive: $out"
    false
  fi
  printf '%s\n' "$out" | grep -qF 'archive clickhouse-myapp-20260304T050607Z recorded on Backup bkp-a is not in remote storage'
  if grep -qF '/backup/restore_remote/' "$tmp/curl.log"; then
    echo "FAIL: restored another archive: $(cat "$tmp/curl.log")"
    false
  fi
  rm -rf "$tmp"
}

@test "restore of a Backup without a recorded archive takes the newest one stamped by its takenAt, and warns" {
  tmp=$(mktemp -d)
  make_stubs "$tmp/bin" "$tmp"
  render_script "$tmp/s.sh" restore myapp "" bkp-a 20260304T050800Z
  # Each decoy sorts after the expected archive and is ruled out by one
  # filter: broken, not a bare timestamp after the prefix, or stamped after
  # takenAt.
  remote_rows clickhouse-myapp-20260304T040000Z clickhouse-myapp-20260304T050607Z \
    "clickhouse-myapp-20260304T050700Z|broken (can't stat metadata.json)" \
    clickhouse-myapp-20260304T050710Z-copy \
    clickhouse-myapp-20260304T060000Z > "$tmp/remote"
  out=$(PATH="$tmp/bin:$PATH" bash "$tmp/s.sh" 2>&1)
  grep -qxF 'POST http://chi-clickhouse-myapp-clickhouse-0-0.tenant-x.svc:7171/backup/restore_remote/clickhouse-myapp-20260304T050607Z' "$tmp/curl.log"
  [ "$(grep -c '/backup/restore_remote/' "$tmp/curl.log")" -eq 1 ]
  printf '%s\n' "$out" | grep -qF 'WARNING: Backup bkp-a records no archive name'
  rm -rf "$tmp"
}

@test "restore of a Backup without a recorded archive fails when no archive is stamped by its takenAt" {
  tmp=$(mktemp -d)
  make_stubs "$tmp/bin" "$tmp"
  render_script "$tmp/s.sh" restore myapp "" bkp-a 20260304T050800Z
  remote_rows clickhouse-myapp-20260304T060000Z clickhouse-myapp-20260304T070000Z > "$tmp/remote"
  out=""
  if out=$(PATH="$tmp/bin:$PATH" bash "$tmp/s.sh" 2>&1); then
    echo "FAIL: restore succeeded with every archive newer than takenAt: $out"
    false
  fi
  printf '%s\n' "$out" | grep -qF 'no archive of myapp stamped no later than 20260304T050800Z'
  if grep -qF '/backup/restore_remote/' "$tmp/curl.log"; then
    echo "FAIL: restored an archive newer than takenAt: $(cat "$tmp/curl.log")"
    false
  fi
  rm -rf "$tmp"
}

@test "backup writes the archive under the name the controller chose" {
  tmp=$(mktemp -d)
  make_stubs "$tmp/bin" "$tmp"
  render_script "$tmp/s.sh" backup myapp clickhouse-myapp-20260304T050607Z "" ""
  PATH="$tmp/bin:$PATH" bash "$tmp/s.sh"
  grep -qxF 'POST http://chi-clickhouse-myapp-clickhouse-0-0.tenant-x.svc:7171/backup/create_remote?name=clickhouse-myapp-20260304T050607Z&resume=true' "$tmp/curl.log"
  rm -rf "$tmp"
}

@test "a retried backup waits on the attempt the sidecar is still running" {
  tmp=$(mktemp -d)
  make_stubs "$tmp/bin" "$tmp"
  render_script "$tmp/s.sh" backup myapp clickhouse-myapp-20260304T050607Z "" ""
  printf '{"command":"create_remote --resume clickhouse-myapp-20260304T050607Z","status":"in progress","start":"2026-03-04 05:06:08"}\n' > "$tmp/actions"
  printf '{"command":"create_remote --resume clickhouse-myapp-20260304T050607Z","status":"success","start":"2026-03-04 05:06:08"}\n' > "$tmp/actions.next"
  PATH="$tmp/bin:$PATH" bash "$tmp/s.sh"
  # The sidecar answers a second operation with 423 while one runs.
  if grep -qF 'POST ' "$tmp/curl.log"; then
    echo "FAIL: started another operation over the running one: $(cat "$tmp/curl.log")"
    false
  fi
  rm -rf "$tmp"
}

@test "a retried backup after a failed attempt resumes the same archive" {
  tmp=$(mktemp -d)
  make_stubs "$tmp/bin" "$tmp"
  render_script "$tmp/s.sh" backup myapp clickhouse-myapp-20260304T050607Z "" ""
  printf '{"command":"create_remote clickhouse-myapp-20260304T050607Z","status":"error","start":"2026-03-04 05:06:08","error":"S3 PutObject 403"}\n' > "$tmp/actions"
  PATH="$tmp/bin:$PATH" bash "$tmp/s.sh"
  grep -qxF 'POST http://chi-clickhouse-myapp-clickhouse-0-0.tenant-x.svc:7171/backup/create_remote?name=clickhouse-myapp-20260304T050607Z&resume=true' "$tmp/curl.log"
  rm -rf "$tmp"
}

@test "backup falls back to a timestamped name when the controller supplies none" {
  tmp=$(mktemp -d)
  make_stubs "$tmp/bin" "$tmp"
  render_script "$tmp/s.sh" backup myapp "" "" ""
  PATH="$tmp/bin:$PATH" bash "$tmp/s.sh"
  grep -qE '^POST http://[^ ]*/backup/create_remote\?name=clickhouse-myapp-[0-9]{8}T[0-9]{6}Z&resume=true$' "$tmp/curl.log"
  rm -rf "$tmp"
}