#!/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 (" " 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" }