Files
cozystack/hack/testdata/migration-56/kubectl
IvanHunters 3d1a83f419 fix(migrations): make the unadopted-pools record publish best-effort
The durable-record ConfigMap write sat under set -euo pipefail right before the
version stamp, so a permanent failure (e.g. the record exceeding etcd's object
size limit at a very large skip count) would abort the migration and deadlock
the fleet upgrade with no path out -- even though every skipped pool was already
pinned prune-proof earlier in the sweep, so the workers are safe regardless of
whether the record lands. The record is a convenience artifact, not a safety
mechanism, so swallow its failure with a warning instead of failing closed.
Covered by a bats case that fails the publish and asserts the run still stamps.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-08-25 12:38:28 +03:00

124 lines
5.6 KiB
Bash
Executable File

#!/bin/sh
# Fake kubectl for the migration-56 unit test (hack/migration-56-adopt-values.bats).
# Mocks only the cluster boundary. Env:
# FAKE_HR_LIST path to the `get helmrelease -o json` list fixture
# FAKE_CHILD_HR path to write the child HelmRelease that `apply -f -` receives
# FAKE_CMDLOG path to append a call log (APPLY-HR / STAMP / annotate / label)
# FAKE_GET_FAIL substring: any `get` whose args match it fails with a
# transient server error (message overridable via
# FAKE_GET_FAIL_MSG, e.g. a conversion-webhook failure whose
# text happens to contain "not found"), so the migration's
# absence-vs-failure classification is exercised
# FAKE_WARN_STDERR when set, every successful get prints a deprecation
# warning to stderr first, the way real kubectl does -- data
# captured with 2>&1 would be contaminated by it
# Absence honors --ignore-not-found the way real kubectl does: with the flag a
# missing object is SUCCESS with empty output; without it, "Error from server
# (NotFound)" on stderr and exit 1.
log() { [ -n "${FAKE_CMDLOG:-}" ] && echo "$*" >> "$FAKE_CMDLOG"; return 0; }
notfound() { echo "Error from server (NotFound): $1 not found" >&2; exit 1; }
absent() {
case "$args" in
*--ignore-not-found*) exit 0 ;;
esac
notfound "$1"
}
verb=${1:-}
shift 2>/dev/null || true
args="$*"
case "$verb" in
get)
if [ -n "${FAKE_GET_FAIL:-}" ]; then
case "$args" in
*"${FAKE_GET_FAIL}"*)
echo "${FAKE_GET_FAIL_MSG:-Error from server: etcdserver: request timed out}" >&2; exit 1 ;;
esac
fi
if [ -n "${FAKE_WARN_STDERR:-}" ]; then
echo "Warning: cluster.x-k8s.io/v1beta1 MachineDeployment is deprecated" >&2
fi
case "$args" in
*helmrelease*--all-namespaces*)
cat "$FAKE_HR_LIST"; exit 0 ;;
*helmrelease*kubernetes-nodes-*)
notfound "helmreleases.helm.toolkit.fluxcd.io" ;; # child HR absent -> create path
esac
# tokenize: $1=resource, $2=object-name (or a flag for list queries)
# shellcheck disable=SC2086
set -- $args
res=$1; name=$2
# Real kubectl rejects a name containing '/' client-side, before any API
# call, deterministically -- reproduce that so the migration provably never
# issues a read for a composed name that cannot exist.
case "$name" in
*/*)
echo "error: there is no need to specify a resource type as a separate argument when passing arguments in resource/name form" >&2
exit 1 ;;
esac
case "$res" in
kubevirtmachinetemplate*)
case "$name" in
-*|"") # KMT range/list query
[ -n "${FAKE_KMT_NAMES:-}" ] && printf '%s\n' "$FAKE_KMT_NAMES"
exit 0 ;;
esac ;;
esac
# Named-object get. When FAKE_OBJS is set (adoption-path tests), look the
# object up there: a matching line "<res> <name> <release-name>" means present.
# Without FAKE_OBJS every named get is "absent" (value-mapping tests rely on
# adopt_one short-circuiting on absent).
if [ -n "${FAKE_OBJS:-}" ] && [ -f "$FAKE_OBJS" ]; then
line=$(grep -E "^${res}[[:space:]]+${name}([[:space:]]|$)" "$FAKE_OBJS" || true)
[ -z "$line" ] && absent "${res} \"${name}\"" # absent
rel=$(printf '%s\n' "$line" | awk '{print $3}')
case "$args" in
*jsonpath*release-name*) printf '%s' "$rel"; exit 0 ;;
*" -o json"*)
printf '{"metadata":{"annotations":{"meta.helm.sh/release-name":"%s"}}}' "$rel"; exit 0 ;;
*" -o name"*) printf '%s/%s\n' "$res" "$name"; exit 0 ;;
*) exit 0 ;; # existence check
esac
fi
absent "${res} \"${name}\"" ;; # default: absent
apply)
payload=$(cat)
if printf '%s' "$payload" | grep -q 'HelmRelease'; then
# Reject an RFC-1123-invalid metadata.name the way the real apiserver would,
# so the migration's name-validity guard is exercised: without it the script
# would reach this apply and exit non-zero, deadlocking the upgrade.
hrname=$(printf '%s' "$payload" | jq -r '.metadata.name // ""' 2>/dev/null)
if ! printf '%s' "$hrname" | grep -qE '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'; then
echo "The HelmRelease \"$hrname\" is invalid: metadata.name: Invalid value: \"$hrname\": a lowercase RFC 1123 label is required" >&2
exit 1
fi
printf '%s' "$payload" > "$FAKE_CHILD_HR"; log "APPLY-HR"
elif printf '%s' "$payload" | grep -q 'ConfigMap'; then
if printf '%s' "$payload" | grep -q 'cozystack-migration-56-unadopted'; then
# FAKE_UNADOPTED_CM_FAIL exercises the best-effort path: a permanent
# failure publishing the record must NOT abort the migration.
if [ -n "${FAKE_UNADOPTED_CM_FAIL:-}" ]; then
echo "Error from server: ConfigMap \"cozystack-migration-56-unadopted\" is invalid: exceeds object size limit" >&2
exit 1
fi
log "APPLY-UNADOPTED-CM"
else
log "STAMP"
fi
else
log "APPLY-OTHER"
fi
exit 0 ;;
annotate) log "annotate $args"; exit 0 ;;
label) log "label $args"; exit 0 ;;
create)
# Only `create configmap cozystack-migration-56-unadopted --from-file ...
# --dry-run=client -o yaml` is used; emit a minimal ConfigMap manifest so the
# piped `apply --filename -` receives it and logs APPLY-UNADOPTED-CM.
case "$args" in
*configmap*cozystack-migration-56-unadopted*)
printf 'apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: cozystack-migration-56-unadopted\n' ;;
esac
exit 0 ;;
*) exit 0 ;;
esac