Files
cozystack/hack/migration-56-adopt-path.bats
Myasnikov Daniil cc942fed68 test(tests): run unit suites with Bats and retain POSIX coverage
The shell translator rewrites test bodies and loses Bats reporting.
The unit lane needs complete TAP results and JUnit reports without
changing how live-cluster suites run.

Sourced production helpers still need a dash pass, and unit test bodies
need nounset without exporting shell options into subprocesses.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 11:04:00 +03:00

490 lines
26 KiB
Bash

#!/usr/bin/env bats
# -----------------------------------------------------------------------------
# Unit tests for the ADOPTION path of platform migration 56.
#
# migration-56-adopt-values.bats pins the jq value-mapping; this file pins the
# adopt_one branch that mutates ownership of running worker objects (annotate
# helm.sh/resource-policy=keep + meta.helm.sh/release-name). The fake kubectl
# serves worker objects via FAKE_OBJS ("<res> <name> <release-name>" lines) and
# KubevirtMachineTemplate names via FAKE_KMT_NAMES, so the annotate, idempotency
# and refusal branches and the 6-hex KMT anchor are exercised directly rather
# than short-circuited on "absent". FAKE_GET_FAIL injects transient server
# errors and FAKE_WARN_STDERR injects success-path stderr warnings, pinning the
# read contract: absence (--ignore-not-found success + empty output) is
# skipped, ANY read failure aborts the run before the version stamp, and
# stderr never contaminates data.
#
# For compatibility with the legacy cozytest.sh translator, tests avoid
# run/$status and file-local setup/teardown, and keep bare `}` lines out of test
# bodies. Assertions are direct shell tests.
# Run with: bats hack/migration-56-adopt-path.bats
# -----------------------------------------------------------------------------
load test_helper
FAKEBIN="$PWD/hack/testdata/migration-56"
MIG="$PWD/packages/core/platform/images/migrations/migrations/56"
prep() {
chmod +x "$FAKEBIN/kubectl"
WORK=$(mktemp -d)
export FAKE_HR_LIST="$WORK/hrlist.json"
export FAKE_CHILD_HR="$WORK/child-hr.json"
export FAKE_CMDLOG="$WORK/cmdlog"
export FAKE_OBJS="$WORK/objs"
: > "$FAKE_CMDLOG"
export PATH="$FAKEBIN:$PATH"
export NAMESPACE=cozy-system
cat > "$FAKE_HR_LIST" <<'JSON'
{"items":[{"metadata":{"namespace":"tenant-test","name":"kubernetes-test3"},"spec":{"values":{"nodeGroups":{"md0":{"minReplicas":1,"roles":["ingress-nginx"]}}}}}]}
JSON
}
@test "adopt_one pins keep + child release-name on parent-owned worker objects, anchoring the KMT match" {
prep
# Worker objects currently owned by the parent release, plus a sibling pool's
# KMT (md0-large) the 6-hex anchor must NOT adopt while processing md0.
cat > "$FAKE_OBJS" <<'OBJS'
machinedeployment.cluster.x-k8s.io kubernetes-test3-md0 kubernetes-test3
machinehealthcheck.cluster.x-k8s.io kubernetes-test3-md0 kubernetes-test3
workloadmonitor.cozystack.io kubernetes-test3-md0 kubernetes-test3
kubevirtmachinetemplate.infrastructure.cluster.x-k8s.io kubernetes-test3-md0-abc123 kubernetes-test3
kubevirtmachinetemplate.infrastructure.cluster.x-k8s.io kubernetes-test3-md0-large-def456 kubernetes-test3
OBJS
FAKE_KMT_NAMES=$(printf '%s\n%s' kubernetes-test3-md0-abc123 kubernetes-test3-md0-large-def456)
export FAKE_KMT_NAMES
rc=0
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
cat "$WORK/out"
[ "$rc" -eq 0 ]
# MD/MHC/WM adopted onto the child release with keep.
grep -qE 'annotate machinedeployment.* kubernetes-test3-md0 .*resource-policy=keep' "$FAKE_CMDLOG"
grep -qE 'annotate machinedeployment.* meta.helm.sh/release-name=kubernetes-nodes-test3-md0' "$FAKE_CMDLOG"
grep -qE 'annotate machinehealthcheck.* kubernetes-test3-md0 ' "$FAKE_CMDLOG"
grep -qE 'annotate workloadmonitor.* kubernetes-test3-md0 ' "$FAKE_CMDLOG"
# The pool's own KMT (6-hex suffix) is adopted...
grep -qE 'annotate kubevirtmachinetemplate.* kubernetes-test3-md0-abc123 ' "$FAKE_CMDLOG"
# ...but the sibling pool md0-large's KMT is NOT (anchor stops the mis-adopt).
if grep -qE 'kubernetes-test3-md0-large-def456' "$FAKE_CMDLOG"; then echo "FAIL: the sibling pool's KubevirtMachineTemplate must not be adopted"; false; fi
rm -rf "$WORK"
}
@test "adopt_one is idempotent: objects already on the child release are skipped" {
prep
cat > "$FAKE_OBJS" <<'OBJS'
machinedeployment.cluster.x-k8s.io kubernetes-test3-md0 kubernetes-nodes-test3-md0
machinehealthcheck.cluster.x-k8s.io kubernetes-test3-md0 kubernetes-nodes-test3-md0
workloadmonitor.cozystack.io kubernetes-test3-md0 kubernetes-nodes-test3-md0
OBJS
rc=0
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
cat "$WORK/out"
[ "$rc" -eq 0 ]
# Already adopted -> no annotate issued at all.
if grep -qE 'annotate ' "$FAKE_CMDLOG"; then echo "FAIL: an already-adopted pool must not be annotated again"; false; fi
rm -rf "$WORK"
}
@test "adopt_one pins keep but does not adopt an object owned by an unexpected release (no prune, no deadlock)" {
prep
# A worker object whose meta.helm.sh/release-name was hand-edited to a third
# release (reachable via `kubectl annotate` on the tenant namespace) must NOT
# abort the whole pre-upgrade hook: exiting 1 under set -e would deadlock the
# platform upgrade for EVERY tenant on one tenant's corrupted annotation, the
# same fleet-wide deadlock the non-object / overflow / invalid-name branches
# were reshaped to avoid. But it must still be PINNED: the object is in the
# parent's old pre-split manifest, so the control-plane-only re-render prunes
# it unless resource-policy=keep is set -- Helm's prune gates on that
# annotation, not on the live release-name. Refuse to adopt THIS object
# (rewriting a foreign release-name could corrupt it), pin keep, warn, and let
# the run complete and stamp. The sibling parent-owned objects still adopt.
cat > "$FAKE_OBJS" <<'OBJS'
machinedeployment.cluster.x-k8s.io kubernetes-test3-md0 some-other-release
machinehealthcheck.cluster.x-k8s.io kubernetes-test3-md0 kubernetes-test3
workloadmonitor.cozystack.io kubernetes-test3-md0 kubernetes-test3
OBJS
rc=0
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
cat "$WORK/out"
[ "$rc" -eq 0 ]
grep -qiE 'owned by unexpected release' "$WORK/out"
# The foreign-owned MD is pinned with keep so the parent cannot prune it...
grep -qE 'annotate machinedeployment.* kubernetes-test3-md0 .*resource-policy=keep' "$FAKE_CMDLOG"
# ...but it is never re-annotated onto the child release (ownership untouched).
if grep -qE 'annotate machinedeployment.* meta.helm.sh/release-name=kubernetes-nodes-test3-md0' "$FAKE_CMDLOG"; then echo "FAIL: a foreign-owned MachineDeployment must not be re-annotated onto the child release"; false; fi
# The sibling parent-owned MHC still adopts, and the run stamps.
grep -qE 'annotate machinehealthcheck.* meta.helm.sh/release-name=kubernetes-nodes-test3-md0' "$FAKE_CMDLOG"
grep -qF -- "STAMP" "$FAKE_CMDLOG"
# The foreign-owned object is a pinned-but-unadopted case too, so it must land
# in the durable unadopted record (not only the reaped Job log): the child HR
# can stay Failed on it and an operator needs to find it after the fact.
grep -qF -- "APPLY-UNADOPTED-CM" "$FAKE_CMDLOG"
grep -qiE 'pinned prune-proof but NOT adopted' "$WORK/out"
rm -rf "$WORK"
}
@test "an RFC-1123-invalid nodeGroup key is pinned and skipped, not applied (no upgrade deadlock)" {
prep
# The old nodeGroups map key was never schema-constrained, so a parent HR can
# carry a key like 'My_Pool' that yields the RFC-1123-invalid child HR name
# kubernetes-nodes-test3-My_Pool. Without the guard the create path reaches
# `kubectl apply`, the fake apiserver rejects the name, and the migration exits
# 1 -- deadlocking every tenant's platform pre-upgrade hook. The guard must
# warn, pin the (absent) pool objects as a no-op, skip adoption, and continue.
cat > "$FAKE_HR_LIST" <<'JSON'
{"items":[{"metadata":{"namespace":"tenant-test","name":"kubernetes-test3"},"spec":{"values":{"nodeGroups":{"My_Pool":{"minReplicas":1,"roles":["ingress-nginx"]}}}}}]}
JSON
rc=0
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
cat "$WORK/out"
# Guard caught it: the run succeeds instead of deadlocking.
[ "$rc" -eq 0 ]
grep -qiE 'not a valid RFC-1123 label' "$WORK/out"
# The invalid child HelmRelease is never applied.
if grep -q 'APPLY-HR' "$FAKE_CMDLOG"; then echo "FAIL: no child HelmRelease must be applied for an invalid pool name"; false; fi
rm -rf "$WORK"
}
@test "a non-object nodeGroup value is pinned and skipped, not crashed on (no upgrade deadlock)" {
prep
# kubectl edit on the parent HR bypasses the aggregated API's schema, so a group
# value can be stored as a non-object (here a string). The value-mapping jq then
# runs `"oops" | has(...)`, which aborts with exit 5 under set -e and deadlocks the
# platform pre-upgrade hook for every tenant. The normalize-to-null guard must warn,
# pin the (absent) pool objects, skip adoption, and let the run complete.
cat > "$FAKE_HR_LIST" <<'JSON'
{"items":[{"metadata":{"namespace":"tenant-test","name":"kubernetes-test3"},"spec":{"values":{"nodeGroups":{"md0":"oops"}}}}]}
JSON
rc=0
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
cat "$WORK/out"
[ "$rc" -eq 0 ]
grep -qiE 'non-object value' "$WORK/out"
if grep -q 'APPLY-HR' "$FAKE_CMDLOG"; then echo "FAIL: no child HelmRelease must be applied for a non-object value"; false; fi
rm -rf "$WORK"
}
@test "a non-object nodeGroups map is skipped per-cluster, not crashed on (no upgrade deadlock)" {
prep
# nodeGroups itself stored as a non-object (here an array). keys[] on an array
# yields indices, then .[$idx] fails with "Cannot index array with string", exiting
# 1 under set -e and deadlocking every tenant's pre-upgrade hook. The shape guard
# must warn and skip this cluster's pool adoption, letting the run complete.
cat > "$FAKE_HR_LIST" <<'JSON'
{"items":[{"metadata":{"namespace":"tenant-test","name":"kubernetes-test3"},"spec":{"values":{"nodeGroups":["md0"]}}}]}
JSON
rc=0
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
cat "$WORK/out"
[ "$rc" -eq 0 ]
grep -qiE 'not an object' "$WORK/out"
if grep -q 'APPLY-HR' "$FAKE_CMDLOG"; then echo "FAIL: no child HelmRelease must be applied for a non-object nodeGroups"; false; fi
rm -rf "$WORK"
}
@test "a non-object nodeGroups map still pins the cluster's live worker pools before skipping (no prune)" {
prep
# nodeGroups mangled out-of-band to a non-object (array), but the cluster still
# has live worker objects from its last good render. The declared pool keys are
# unreadable, so the migration discovers the live pools from their KMTs and pins
# every worker object with resource-policy=keep -- otherwise the parent's
# control-plane-only re-render prunes the un-pinned VMs. No child HR is created
# (adoption is skipped), but the run completes and stamps. Two pools (md0, md1)
# prove discovery is not limited to the implicit md0.
cat > "$FAKE_HR_LIST" <<'JSON'
{"items":[{"metadata":{"namespace":"tenant-test","name":"kubernetes-test3"},"spec":{"values":{"nodeGroups":["md0"]}}}]}
JSON
cat > "$FAKE_OBJS" <<'OBJS'
machinedeployment.cluster.x-k8s.io kubernetes-test3-md0 kubernetes-test3
machinehealthcheck.cluster.x-k8s.io kubernetes-test3-md0 kubernetes-test3
workloadmonitor.cozystack.io kubernetes-test3-md0 kubernetes-test3
kubevirtmachinetemplate.infrastructure.cluster.x-k8s.io kubernetes-test3-md0-abc123 kubernetes-test3
machinedeployment.cluster.x-k8s.io kubernetes-test3-md1 kubernetes-test3
kubevirtmachinetemplate.infrastructure.cluster.x-k8s.io kubernetes-test3-md1-def456 kubernetes-test3
OBJS
FAKE_KMT_NAMES=$(printf '%s\n%s' kubernetes-test3-md0-abc123 kubernetes-test3-md1-def456)
export FAKE_KMT_NAMES
rc=0
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
cat "$WORK/out"
[ "$rc" -eq 0 ]
grep -qiE 'not an object' "$WORK/out"
# Both discovered pools pinned with keep, no ownership transfer (no release-name).
grep -qE 'annotate machinedeployment.* kubernetes-test3-md0 .*resource-policy=keep' "$FAKE_CMDLOG"
grep -qE 'annotate kubevirtmachinetemplate.* kubernetes-test3-md0-abc123 .*resource-policy=keep' "$FAKE_CMDLOG"
grep -qE 'annotate machinedeployment.* kubernetes-test3-md1 .*resource-policy=keep' "$FAKE_CMDLOG"
grep -qE 'annotate kubevirtmachinetemplate.* kubernetes-test3-md1-def456 .*resource-policy=keep' "$FAKE_CMDLOG"
if grep -qE 'meta.helm.sh/release-name' "$FAKE_CMDLOG"; then echo "FAIL: no ownership must be transferred"; false; fi
# Adoption skipped, run completes.
if grep -q 'APPLY-HR' "$FAKE_CMDLOG"; then echo "FAIL: no child HelmRelease must be applied"; false; fi
grep -qF -- "STAMP" "$FAKE_CMDLOG"
rm -rf "$WORK"
}
@test "a non-scalar value for a scalar nodeGroup field pins the live pool objects before skipping (no prune)" {
prep
# The group key and its map value are both well-formed, but a scalar field
# (here storageClass) is stored as an object -- reachable via kubectl edit,
# since the NodeGroup subfields were never fully schema-constrained. Copied
# verbatim into the child HR it would fail the KubernetesNodes schema and
# leave the child release stuck Failed, with the MD already re-annotated onto
# it. The bad-scalar guard must instead warn, pin the live pool objects with
# resource-policy=keep (so the parent's control-plane-only re-render cannot
# prune the running VMs), skip adoption (no child HR), and let the run stamp.
cat > "$FAKE_HR_LIST" <<'JSON'
{"items":[{"metadata":{"namespace":"tenant-test","name":"kubernetes-test3"},"spec":{"values":{"nodeGroups":{"md0":{"minReplicas":1,"storageClass":{"name":"replicated"},"roles":["ingress-nginx"]}}}}}]}
JSON
cat > "$FAKE_OBJS" <<'OBJS'
machinedeployment.cluster.x-k8s.io kubernetes-test3-md0 kubernetes-test3
machinehealthcheck.cluster.x-k8s.io kubernetes-test3-md0 kubernetes-test3
workloadmonitor.cozystack.io kubernetes-test3-md0 kubernetes-test3
kubevirtmachinetemplate.infrastructure.cluster.x-k8s.io kubernetes-test3-md0-abc123 kubernetes-test3
OBJS
FAKE_KMT_NAMES=$(printf '%s' kubernetes-test3-md0-abc123)
export FAKE_KMT_NAMES
rc=0
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
cat "$WORK/out"
[ "$rc" -eq 0 ]
grep -qiE 'non-scalar value' "$WORK/out"
# Live pool objects pinned with keep, no ownership transfer (no release-name).
grep -qE 'annotate machinedeployment.* kubernetes-test3-md0 .*resource-policy=keep' "$FAKE_CMDLOG"
grep -qE 'annotate machinehealthcheck.* kubernetes-test3-md0 .*resource-policy=keep' "$FAKE_CMDLOG"
grep -qE 'annotate workloadmonitor.* kubernetes-test3-md0 .*resource-policy=keep' "$FAKE_CMDLOG"
grep -qE 'annotate kubevirtmachinetemplate.* kubernetes-test3-md0-abc123 .*resource-policy=keep' "$FAKE_CMDLOG"
if grep -qE 'meta.helm.sh/release-name' "$FAKE_CMDLOG"; then echo "FAIL: no ownership must be transferred"; false; fi
# Adoption skipped, run completes and stamps.
if grep -q 'APPLY-HR' "$FAKE_CMDLOG"; then echo "FAIL: no child HelmRelease must be applied"; false; fi
grep -qF -- "STAMP" "$FAKE_CMDLOG"
rm -rf "$WORK"
}
@test "a child release name over 53 chars pins the live pool objects and skips, not deadlock on kubectl apply (no upgrade deadlock)" {
prep
# rest.go caps a cluster name at 42 chars, and the kubernetes-nodes- prefix
# (17 chars) plus a pool name can push the child HelmRelease name past Helm's
# 53-char limit. Here cluster 'production-eu-central-analytics01' (33) + pool
# 'md0' yields child name 'kubernetes-nodes-production-eu-central-analytics01-md0'
# (54 chars). Without the length guard the flow reaches `kubectl apply` of a
# >53-char HelmRelease name, the apiserver rejects it, and under set -e the
# run exits 1 before stamping -- deadlocking every tenant's platform upgrade.
# The overflow branch must instead warn, pin the live pool objects prune-proof,
# skip adoption (no child HR), and let the run complete and stamp.
cat > "$FAKE_HR_LIST" <<'JSON'
{"items":[{"metadata":{"namespace":"tenant-test","name":"kubernetes-production-eu-central-analytics01"},"spec":{"values":{"nodeGroups":{"md0":{"minReplicas":1,"roles":["ingress-nginx"]}}}}}]}
JSON
cat > "$FAKE_OBJS" <<'OBJS'
machinedeployment.cluster.x-k8s.io kubernetes-production-eu-central-analytics01-md0 kubernetes-production-eu-central-analytics01
machinehealthcheck.cluster.x-k8s.io kubernetes-production-eu-central-analytics01-md0 kubernetes-production-eu-central-analytics01
workloadmonitor.cozystack.io kubernetes-production-eu-central-analytics01-md0 kubernetes-production-eu-central-analytics01
kubevirtmachinetemplate.infrastructure.cluster.x-k8s.io kubernetes-production-eu-central-analytics01-md0-abc123 kubernetes-production-eu-central-analytics01
OBJS
FAKE_KMT_NAMES=$(printf '%s' kubernetes-production-eu-central-analytics01-md0-abc123)
export FAKE_KMT_NAMES
rc=0
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
cat "$WORK/out"
[ "$rc" -eq 0 ]
grep -qiE "exceeds Helm's 53-char limit" "$WORK/out"
# Live pool objects pinned with keep, no ownership transfer (no release-name).
grep -qE 'annotate machinedeployment.* kubernetes-production-eu-central-analytics01-md0 .*resource-policy=keep' "$FAKE_CMDLOG"
grep -qE 'annotate machinehealthcheck.* kubernetes-production-eu-central-analytics01-md0 .*resource-policy=keep' "$FAKE_CMDLOG"
grep -qE 'annotate workloadmonitor.* kubernetes-production-eu-central-analytics01-md0 .*resource-policy=keep' "$FAKE_CMDLOG"
grep -qE 'annotate kubevirtmachinetemplate.* kubernetes-production-eu-central-analytics01-md0-abc123 .*resource-policy=keep' "$FAKE_CMDLOG"
if grep -qE 'meta.helm.sh/release-name' "$FAKE_CMDLOG"; then echo "FAIL: no ownership must be transferred"; false; fi
# Adoption skipped (no child HR applied), run completes and stamps.
if grep -q 'APPLY-HR' "$FAKE_CMDLOG"; then echo "FAIL: no child HelmRelease must be applied"; false; fi
grep -qF -- "STAMP" "$FAKE_CMDLOG"
rm -rf "$WORK"
}
@test "an absent worker object is skipped and the run still completes and stamps" {
prep
# No FAKE_OBJS: every named get is absent -- under --ignore-not-found that is
# SUCCESS with empty output, the way real kubectl behaves. adopt_one must
# classify that as absent (skip) -- this pins the absent path so the
# fail-closed tests below prove classification, not blanket failure.
rc=0
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
cat "$WORK/out"
[ "$rc" -eq 0 ]
grep -q 'absent in tenant-test, skipping' "$WORK/out"
grep -qF -- "STAMP" "$FAKE_CMDLOG"
rm -rf "$WORK"
}
@test "a transient error reading a worker object fails the run closed, before the version stamp" {
prep
# kubectl exits 1 for NotFound and for a transient server failure alike. If a
# timeout were classified as "absent", adoption would be skipped WITHOUT the
# keep pin, the migration would complete and stamp 57 (never re-running), and
# the parent's control-plane-only upgrade would prune the live pool's
# MachineDeployment -- deleting every running worker VM. The migration must
# instead exit non-zero with no stamp, so run-migrations.sh retries it.
export FAKE_GET_FAIL="machinedeployment"
rc=0
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
cat "$WORK/out"
[ "$rc" -ne 0 ]
grep -q 'ERROR: reading machinedeployment' "$WORK/out"
if grep -qF -- "STAMP" "$FAKE_CMDLOG"; then echo "FAIL: the version must not be stamped after a read error"; false; fi
unset FAKE_GET_FAIL
rm -rf "$WORK"
}
@test "a transient error listing KubevirtMachineTemplates fails the run closed, before the version stamp" {
prep
# A failed KMT listing must not read as "no templates": zero KMTs would be
# adopted, the migration would stamp, and the parent upgrade would prune a
# preserved older-revision KMT still referenced by an in-flight MachineSet --
# permanently, since the child chart only re-emits KMTs already annotated with
# its own release name. MD/MHC/WM are present and adoptable so the run
# provably fails at the listing, not earlier.
cat > "$FAKE_OBJS" <<'OBJS'
machinedeployment.cluster.x-k8s.io kubernetes-test3-md0 kubernetes-test3
machinehealthcheck.cluster.x-k8s.io kubernetes-test3-md0 kubernetes-test3
workloadmonitor.cozystack.io kubernetes-test3-md0 kubernetes-test3
OBJS
export FAKE_GET_FAIL="kubevirtmachinetemplate"
rc=0
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
cat "$WORK/out"
[ "$rc" -ne 0 ]
grep -q 'ERROR: listing KubevirtMachineTemplates' "$WORK/out"
if grep -qF -- "STAMP" "$FAKE_CMDLOG"; then echo "FAIL: the version must not be stamped after a listing error"; false; fi
unset FAKE_GET_FAIL
rm -rf "$WORK"
}
@test "a transient error on the pin path (unadoptable pool) fails the run closed, before the version stamp" {
prep
# pin_keep guards pools that cannot be adopted (invalid/overflowing name); a
# transient read error masked as "absent" there would leave the pool un-pinned
# for the parent's prune with no child release to notice. Same contract as
# adopt_one: read failures abort the run. The key is dotted -- label-invalid,
# so the pin path fires, yet a valid object name, so objects can exist and
# the pin path genuinely reads; a key that cannot name an object at all is
# skipped before any read (covered by the slash test above).
cat > "$FAKE_HR_LIST" <<'JSON'
{"items":[{"metadata":{"namespace":"tenant-test","name":"kubernetes-test3"},"spec":{"values":{"nodeGroups":{"a.b":{"minReplicas":1}}}}}]}
JSON
export FAKE_GET_FAIL="machinedeployment"
rc=0
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
cat "$WORK/out"
[ "$rc" -ne 0 ]
grep -q 'ERROR: reading machinedeployment' "$WORK/out"
if grep -qF -- "STAMP" "$FAKE_CMDLOG"; then echo "FAIL: the version must not be stamped after a read error"; false; fi
unset FAKE_GET_FAIL
rm -rf "$WORK"
}
@test "a nodeGroup key containing a slash is skipped without a read, not deadlocked on kubectl's client-side rejection" {
prep
# A composed object name with a '/' is rejected by real kubectl client-side,
# deterministically, before any API call -- so if the pin path issued a read
# for it, the fail-closed classification would abort the run on every retry,
# deadlocking the upgrade over a pool that provably cannot exist (the old
# chart's apply of such a name would have been rejected by the apiserver).
# The pin path must skip such names statically: nothing to pin, no read made.
cat > "$FAKE_HR_LIST" <<'JSON'
{"items":[{"metadata":{"namespace":"tenant-test","name":"kubernetes-test3"},"spec":{"values":{"nodeGroups":{"pool/a":{"minReplicas":1}}}}}]}
JSON
rc=0
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
cat "$WORK/out"
[ "$rc" -eq 0 ]
grep -qiE 'not a valid RFC-1123 label' "$WORK/out"
grep -qi 'nothing to pin' "$WORK/out"
if grep -q 'APPLY-HR' "$FAKE_CMDLOG"; then echo "FAIL: no child HelmRelease must be applied"; false; fi
grep -qF -- "STAMP" "$FAKE_CMDLOG"
rm -rf "$WORK"
}
@test "a kubectl warning on stderr does not contaminate the owner read or break adoption" {
prep
# kubectl writes warnings to stderr on SUCCESSFUL reads: apiserver deprecation
# headers (deterministic, printed on every get of a deprecated GVK -- CAPI
# v1beta1 qualifies) and client-side throttling notices. If the owner read
# captured stderr into its data (2>&1), the warning would glue onto the
# annotation value, the foreign-owner guard would see an unexpected "owner",
# refuse, and exit 1 -- permanently deadlocking the pre-upgrade hook for every
# tenant on a warning that never goes away. Adoption must proceed normally.
cat > "$FAKE_OBJS" <<'OBJS'
machinedeployment.cluster.x-k8s.io kubernetes-test3-md0 kubernetes-test3
machinehealthcheck.cluster.x-k8s.io kubernetes-test3-md0 kubernetes-test3
workloadmonitor.cozystack.io kubernetes-test3-md0 kubernetes-test3
OBJS
export FAKE_WARN_STDERR=1
rc=0
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
cat "$WORK/out"
[ "$rc" -eq 0 ]
grep -qE 'annotate machinedeployment.* meta.helm.sh/release-name=kubernetes-nodes-test3-md0' "$FAKE_CMDLOG"
if grep -qi 'refusing' "$WORK/out"; then echo "FAIL: a warning on stderr must not make the owner guard refuse"; false; fi
grep -qF -- "STAMP" "$FAKE_CMDLOG"
unset FAKE_WARN_STDERR
rm -rf "$WORK"
}
@test "a transient error whose message contains 'not found' still fails the run closed" {
prep
# A conversion-webhook outage mid-upgrade fails a read with e.g.
# 'conversion webhook ... failed: service "capi-webhook-service" not found'.
# That is a transient failure, not absence: classifying it as absent would
# skip the keep pin, stamp 57, and let the parent upgrade prune the live
# pool. Only real absence (--ignore-not-found success + empty output) may be
# skipped; every read failure aborts, whatever its message says.
export FAKE_GET_FAIL="machinedeployment"
export FAKE_GET_FAIL_MSG='Error from server: conversion webhook for cluster.x-k8s.io/v1beta1, Kind=MachineDeployment failed: service "capi-webhook-service" not found'
rc=0
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
cat "$WORK/out"
[ "$rc" -ne 0 ]
if grep -qF -- "STAMP" "$FAKE_CMDLOG"; then echo "FAIL: the version must not be stamped after a transient read failure"; false; fi
unset FAKE_GET_FAIL FAKE_GET_FAIL_MSG
rm -rf "$WORK"
}
@test "a whitespace nodeGroup key reaches the RFC-1123 guard intact, not word-split into bogus pools" {
prep
# The old nodeGroups map key was never schema-constrained. An unquoted
# `for group in $(... keys[])` word-splits a key like 'my pool' into 'my' and
# 'pool' BEFORE the RFC-1123 guard, each fragment individually valid, fabricating
# two garbage child HelmReleases for a pool that never existed. Newline-safe
# iteration keeps the key intact so the guard rejects the space and pins + skips.
cat > "$FAKE_HR_LIST" <<'JSON'
{"items":[{"metadata":{"namespace":"tenant-test","name":"kubernetes-test3"},"spec":{"values":{"nodeGroups":{"my pool":{"minReplicas":1}}}}}]}
JSON
rc=0
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
cat "$WORK/out"
[ "$rc" -eq 0 ]
grep -qiE 'not a valid RFC-1123 label' "$WORK/out"
# Neither 'my' nor 'pool' is ever applied as a child release.
if grep -q 'APPLY-HR' "$FAKE_CMDLOG"; then echo "FAIL: no child HelmRelease must be applied"; false; fi
rm -rf "$WORK"
}
@test "a non-scalar scalar-field on a nodeGroup is pinned and skipped, not applied to a child that cannot validate" {
prep
# kubectl edit on the parent HR bypasses the aggregated API schema, so a field
# the child HR expects to be scalar (here storageClass) can be stored as an
# object. Copied verbatim into the child values it fails the KubernetesNodes
# schema on install, leaving the child release stuck Failed. The scalar-type
# guard must warn, pin the pool's (absent here) objects, skip adoption, and
# let the run complete and stamp instead of fabricating an unvalidatable child.
cat > "$FAKE_HR_LIST" <<'JSON'
{"items":[{"metadata":{"namespace":"tenant-test","name":"kubernetes-test3"},"spec":{"values":{"nodeGroups":{"md0":{"minReplicas":1,"storageClass":{"nested":"oops"}}}}}}]}
JSON
rc=0
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
cat "$WORK/out"
[ "$rc" -eq 0 ]
grep -qiE 'non-scalar value' "$WORK/out"
grep -q 'storageClass' "$WORK/out"
# The pool is pinned+skipped, never applied as a child release.
if grep -q 'APPLY-HR' "$FAKE_CMDLOG"; then echo "FAIL: the pinned-and-skipped pool must not be applied as a child release"; false; fi
rm -rf "$WORK"
}