mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-10 06:24:09 +09:00
Image overrides let a tenant run an arbitrary container in the
management cluster, including helpers with access to the Talos CA
secrets and the tenant cluster's API credentials. Drop those overrides
and run the images the platform ships.
The Kubernetes and KubernetesNodes helpers (the wait-for-kubeconfig
and Proxmox cloud-config init containers, the bootstrap-token and OIDC
RBAC Jobs, the talos-csr-signer sidecar, the Talos reconcile and unpin
Jobs) take the bundled image references through cozy-lib.image, so
they still follow the configured registry. OpenSearch no longer sets
an image and leaves it to the operator, which follows the version the
chart's version map selects; an explicit version: null now fails the
render instead of falling back to v2. The Talos installer settings for
tenant worker VMs stay.
FoundationDB replaces the free-form cluster.version with version, v7.3
or v7.4, mapped to 7.3.63 and 7.4.1; the 7.1 line is no longer
offered. The chart refuses a render where cluster.version and version
name different release lines, which also covers variants without
platform migrations. Migration 60 carries an existing cluster.version
over to its release line and leaves the legacy value in place while
the old chart can still render it. It records what needs attention in
the cozystack.io/migration-60-needs-attention annotation on the
cozystack-version ConfigMap: a FoundationDB release it cannot carry
over or whose patch changes, a release that still sets one of the
removed image overrides, an OpenSearch release storing version: null.
A dry run reports them without patching, annotating or advancing the
stamp. Migration 56 stops carrying the removed kubectl override into
adopted node pools.
OpenSearch and FoundationDB get version-map update scripts that keep
the current default while it is still supported.
Stored image overrides are still accepted and the charts ignore them.
On the first reconcile OpenSearch nodes move to the image of their
selected version and Kubernetes components whose image changed
restart. Air-gapped installations need docker.io and ghcr.io mirrored
on the management-cluster nodes.
Assisted-by: LLM
(cherry picked from commit 749d401899)
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
311 lines
16 KiB
Bash
311 lines
16 KiB
Bash
#!/usr/bin/env bats
|
|
# -----------------------------------------------------------------------------
|
|
# Unit tests for platform migration 56 (adopt pre-split worker pools into
|
|
# per-pool kubernetes-nodes HelmReleases).
|
|
#
|
|
# These pin the value-mapping contract that guarantees "byte-identical child
|
|
# re-render, zero worker-VM churn". The migration builds the child HelmRelease's
|
|
# spec.values from the parent kubernetes HR values with a jq helper:
|
|
#
|
|
# def pick($o; ks): reduce ks[] as $k ({}; if ($o | has($k)) ...);
|
|
# ... + pick(.; ["version","talos"])
|
|
#
|
|
# A subtle regression is to write `def pick(o; ks)` with a filter-parameter:
|
|
# inside `reduce ks[] as $k ({}; ...)` the `.` context is the accumulator, so
|
|
# `o | has($k)` tests the accumulator (not the source object) and pick(.; ...)
|
|
# returns {} for EVERY input. The child HR is then created WITHOUT the tenant's
|
|
# talos/version, Helm renders the chart defaults, talos.version changes, the
|
|
# content-hashed KubevirtMachineTemplate is renamed, and the MachineDeployment
|
|
# rolls every live worker VM on upgrade — the exact churn this migration exists
|
|
# to prevent. Static review missed it; only e2e on a real cluster caught it.
|
|
#
|
|
# The @test below drives the REAL migration script against a fake kubectl and
|
|
# asserts the captured child HelmRelease carries the tenant's non-default
|
|
# talos/version (plus the group fields and storageClass fallback). It
|
|
# fails against the buggy `pick(o; ...)` and passes against `pick($o; ...)`.
|
|
#
|
|
# CI runs this file under Bats through `make bats-unit-tests`. It also remains
|
|
# compatible with the legacy `hack/cozytest.sh` translator, whose awk parser
|
|
# recognizes only @test blocks and a bare `}` on its own line. The tests avoid
|
|
# `run`, `$status`, setup(), and teardown() for that compatibility path.
|
|
#
|
|
# Run with: bats hack/migration-56-adopt-values.bats
|
|
# -----------------------------------------------------------------------------
|
|
|
|
load test_helper
|
|
|
|
FAKEBIN="$PWD/hack/testdata/migration-56"
|
|
MIG="$PWD/packages/core/platform/images/migrations/migrations/56"
|
|
|
|
# prep resets PATH/env to a clean scenario: one tenant Kubernetes HR (test3)
|
|
# with a single pool md0 and NON-default talos/version so the assertions
|
|
# distinguish "copied the tenant value" from "fell back to the chart default",
|
|
# plus a leftover images map that must not reach the pool values.
|
|
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"
|
|
: > "$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,"maxReplicas":3,"roles":["ingress-nginx"],"logSerialConsole":true}},
|
|
"talos":{"version":"v1.13.0","schematicID":"deadbeef","imageFactoryURL":"https://factory.talos.dev","installerRepository":"factory.talos.dev/installer"},
|
|
"version":"v1.32",
|
|
"storageClass":"replicated",
|
|
"images":{"kubectl":"example.io/kubectl:v1.32","waitForKubeconfig":"drop-me","talosCsrSigner":"drop-me"}}}}
|
|
]}
|
|
JSON
|
|
}
|
|
|
|
@test "child HR carries the tenant's talos/version (not chart defaults) so the KMT hash is preserved" {
|
|
prep
|
|
rc=0
|
|
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
|
|
cat "$WORK/out"
|
|
[ "$rc" -eq 0 ]
|
|
|
|
# The create path ran and captured a child HelmRelease.
|
|
grep -qF -- "APPLY-HR" "$FAKE_CMDLOG"
|
|
[ -s "$FAKE_CHILD_HR" ]
|
|
|
|
# Shape sanity: it is the pool's child HR.
|
|
[ "$(jq -r '.kind' "$FAKE_CHILD_HR")" = "HelmRelease" ]
|
|
[ "$(jq -r '.metadata.name' "$FAKE_CHILD_HR")" = "kubernetes-nodes-test3-md0" ]
|
|
[ "$(jq -r '.spec.values.cluster' "$FAKE_CHILD_HR")" = "test3" ]
|
|
|
|
# THE REGRESSION: cluster-level inputs the worker templates consume must be
|
|
# copied from the tenant, NOT dropped (which would let the chart default win).
|
|
[ "$(jq -r '.spec.values.talos.version' "$FAKE_CHILD_HR")" = "v1.13.0" ]
|
|
[ "$(jq -r '.spec.values.talos.schematicID' "$FAKE_CHILD_HR")" = "deadbeef" ]
|
|
[ "$(jq -r '.spec.values.version' "$FAKE_CHILD_HR")" = "v1.32" ]
|
|
# Neither chart reads an images map any more, so none is carried over.
|
|
[ "$(jq -r '.spec.values | has("images")' "$FAKE_CHILD_HR")" = "false" ]
|
|
|
|
# Group fields and the storageClass cluster-level fallback carry through.
|
|
[ "$(jq -r '.spec.values.minReplicas' "$FAKE_CHILD_HR")" = "1" ]
|
|
[ "$(jq -r '.spec.values.roles[0]' "$FAKE_CHILD_HR")" = "ingress-nginx" ]
|
|
# logSerialConsole feeds the content-hashed KMT (nodegroup.yaml define), so
|
|
# dropping it on adoption would rename the KMT and roll every worker VM of the
|
|
# pool (and silently disable the setting); it must carry through.
|
|
[ "$(jq -r '.spec.values.logSerialConsole' "$FAKE_CHILD_HR")" = "true" ]
|
|
[ "$(jq -r '.spec.values.storageClass' "$FAKE_CHILD_HR")" = "replicated" ]
|
|
rm -rf "$WORK"
|
|
}
|
|
|
|
@test "positive control: a clean run reaches the version stamp" {
|
|
prep
|
|
rc=0
|
|
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
|
|
cat "$WORK/out"
|
|
[ "$rc" -eq 0 ]
|
|
# The stamp ran (so "values present" above is a real signal, not a no-op run).
|
|
grep -qF -- "STAMP" "$FAKE_CMDLOG"
|
|
rm -rf "$WORK"
|
|
}
|
|
|
|
@test "implicit md0: an empty nodeGroups materialises the default ingress-nginx pool" {
|
|
prep
|
|
# A parent cluster whose spec.values carries no nodeGroups relied on the chart's
|
|
# implicit md0 default. Migration 56 must materialise DEFAULT_MD0 and create the
|
|
# child HR kubernetes-nodes-<cluster>-md0 with minReplicas 0 and the
|
|
# ingress-nginx role, so a cluster that never set nodeGroups keeps its pool.
|
|
cat > "$FAKE_HR_LIST" <<'JSON'
|
|
{"items":[{"metadata":{"namespace":"tenant-test","name":"kubernetes-test3"},"spec":{"values":{}}}]}
|
|
JSON
|
|
rc=0
|
|
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
|
|
cat "$WORK/out"
|
|
[ "$rc" -eq 0 ]
|
|
grep -qF -- "materialising implicit md0 default" "$WORK/out"
|
|
grep -qF -- "APPLY-HR" "$FAKE_CMDLOG"
|
|
[ "$(jq -r '.metadata.name' "$FAKE_CHILD_HR")" = "kubernetes-nodes-test3-md0" ]
|
|
[ "$(jq -r '.spec.values.minReplicas' "$FAKE_CHILD_HR")" = "0" ]
|
|
[ "$(jq -r '.spec.values.roles[0]' "$FAKE_CHILD_HR")" = "ingress-nginx" ]
|
|
# DEFAULT_MD0 is the sole source of the implicit pool's shape, and the fields
|
|
# below feed the content-hashed KubevirtMachineTemplate (kubernetes-nodes
|
|
# nodegroup.yaml). An edit to any of them re-hashes the KMT and silently rolls
|
|
# every implicit-md0 pool in the fleet on the next upgrade; no other assertion
|
|
# here catches it (name/minReplicas/role do not enter the hash). Pin the
|
|
# pre-split md0 values so such an edit reds this case instead of shipping.
|
|
[ "$(jq -r '.spec.values.instanceType' "$FAKE_CHILD_HR")" = "u1.medium" ]
|
|
[ "$(jq -r '.spec.values.diskSize' "$FAKE_CHILD_HR")" = "20Gi" ]
|
|
[ "$(jq -c '.spec.values.resources' "$FAKE_CHILD_HR")" = "{}" ]
|
|
[ "$(jq -c '.spec.values.gpus' "$FAKE_CHILD_HR")" = "[]" ]
|
|
[ "$(jq -c '.spec.values.kubelet' "$FAKE_CHILD_HR")" = "{}" ]
|
|
# storageClass "" in DEFAULT_MD0 falls through to the (absent here) cluster-
|
|
# level value, so the child carries no storageClass key — the pre-split md0
|
|
# shape, where md0 rendered with the chart's empty-string default.
|
|
[ "$(jq -r '.spec.values.storageClass // "ABSENT"' "$FAKE_CHILD_HR")" = "ABSENT" ]
|
|
rm -rf "$WORK"
|
|
}
|
|
|
|
@test "adoption carries podCpuLimit/podCpuRequest and the cluster-wide MHC timeouts so a tuned pool is neither rolled nor reset" {
|
|
prep
|
|
# A pre-split parent that tuned worker sizing and MachineHealthCheck timeouts.
|
|
# podCpuLimit/podCpuRequest feed the content-hashed KubevirtMachineTemplate, so
|
|
# dropping them on adoption renames the KMT and rolls every worker VM of the
|
|
# pool. readyUnknownTimeout / readyFalseTimeout / maxNodeProvisionTime feed the
|
|
# pool's MachineHealthCheck and the autoscaler annotation, so dropping them
|
|
# silently resets the tuning to the chart default. All must survive the
|
|
# parent -> child value mapping (these fields arrived with the main merge that
|
|
# made the pool chart configurable; the pick list has to keep pace with it).
|
|
cat > "$FAKE_HR_LIST" <<'JSON'
|
|
{"items":[{"metadata":{"namespace":"tenant-test","name":"kubernetes-test3"},"spec":{"values":{
|
|
"nodeGroups":{"md0":{"minReplicas":1,"maxReplicas":3,"resources":{"cpu":"4","memory":"8Gi"},"podCpuLimit":"3","podCpuRequest":"2"}},
|
|
"nodeHealthCheck":{"maxUnhealthy":"30%","nodeStartupTimeout":"25m","readyUnknownTimeout":"90s","readyFalseTimeout":"7m"},
|
|
"maxNodeProvisionTime":"45m",
|
|
"talos":{"version":"v1.13.0","schematicID":"deadbeef","imageFactoryURL":"https://factory.talos.dev","installerRepository":"factory.talos.dev/installer"},
|
|
"version":"v1.32"}}}]}
|
|
JSON
|
|
rc=0
|
|
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
|
|
cat "$WORK/out"
|
|
[ "$rc" -eq 0 ]
|
|
grep -qF -- "APPLY-HR" "$FAKE_CMDLOG"
|
|
[ "$(jq -r '.metadata.name' "$FAKE_CHILD_HR")" = "kubernetes-nodes-test3-md0" ]
|
|
|
|
# KMT-hash inputs: carried verbatim from the group, or the pool rolls on adoption.
|
|
[ "$(jq -r '.spec.values.podCpuLimit' "$FAKE_CHILD_HR")" = "3" ]
|
|
[ "$(jq -r '.spec.values.podCpuRequest' "$FAKE_CHILD_HR")" = "2" ]
|
|
|
|
# cluster-wide MHC / autoscaler tuning: flattened onto the pool, not reset.
|
|
[ "$(jq -r '.spec.values.maxUnhealthy' "$FAKE_CHILD_HR")" = "30%" ]
|
|
[ "$(jq -r '.spec.values.nodeStartupTimeout' "$FAKE_CHILD_HR")" = "25m" ]
|
|
[ "$(jq -r '.spec.values.readyUnknownTimeout' "$FAKE_CHILD_HR")" = "90s" ]
|
|
[ "$(jq -r '.spec.values.readyFalseTimeout' "$FAKE_CHILD_HR")" = "7m" ]
|
|
[ "$(jq -r '.spec.values.maxNodeProvisionTime' "$FAKE_CHILD_HR")" = "45m" ]
|
|
rm -rf "$WORK"
|
|
}
|
|
|
|
@test "an unlabelled parent HR (kubernetes chartRef, no application.kind label) is restamped before the sweep" {
|
|
prep
|
|
# A parent kubernetes HelmRelease predating the apps.cozystack.io/application.kind
|
|
# label (fdca49838, ~v1.2), or one restored/created by hand, is identified by its
|
|
# chartRef rather than the label. The migration must relabel it BEFORE the
|
|
# label-selected sweep, otherwise the sweep skips it, stamps, and the parent's
|
|
# control-plane-only re-render prunes its un-pinned workers. Fixture carries the
|
|
# parent chartRef and NO application labels; assert all three labels are written.
|
|
cat > "$FAKE_HR_LIST" <<'JSON'
|
|
{"items":[{"metadata":{"namespace":"tenant-legacy","name":"kubernetes-old"},"spec":{"chartRef":{"name":"cozystack-kubernetes-application-kubevirt-kubernetes"},"values":{"nodeGroups":{"md0":{"minReplicas":0,"roles":["ingress-nginx"]}}}}}]}
|
|
JSON
|
|
rc=0
|
|
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
|
|
cat "$WORK/out"
|
|
[ "$rc" -eq 0 ]
|
|
grep -qF -- "restamping application labels on unlabelled parent tenant-legacy/kubernetes-old" "$WORK/out"
|
|
grep -qE 'label helmrelease kubernetes-old .*apps.cozystack.io/application.kind=Kubernetes' "$FAKE_CMDLOG"
|
|
grep -qE 'label helmrelease kubernetes-old .*apps.cozystack.io/application.group=apps.cozystack.io' "$FAKE_CMDLOG"
|
|
grep -qE 'label helmrelease kubernetes-old .*apps.cozystack.io/application.name=old' "$FAKE_CMDLOG"
|
|
rm -rf "$WORK"
|
|
}
|
|
|
|
@test "a labelled parent HR (chartRef + application.kind label) is NOT restamped (no redundant label write)" {
|
|
prep
|
|
# A parent that carries BOTH the kubernetes chartRef AND the application.kind
|
|
# label is already correctly labelled: the restamp's label-absence guard
|
|
# (select .kind != "Kubernetes") must exclude it so it is not re-labelled on
|
|
# every upgrade. A fixture WITHOUT the chartRef would be dropped by the chartRef
|
|
# filter regardless and leave this guard untested, so seed the chartRef too and
|
|
# exercise exactly the label guard. Assert no `label helmrelease` is issued
|
|
# (the sweep's own `label machinedeployment ... managed-by` does not match).
|
|
cat > "$FAKE_HR_LIST" <<'JSON'
|
|
{"items":[{"metadata":{"namespace":"tenant-test","name":"kubernetes-test3","labels":{"apps.cozystack.io/application.kind":"Kubernetes"}},"spec":{"chartRef":{"name":"cozystack-kubernetes-application-kubevirt-kubernetes"},"values":{"nodeGroups":{"md0":{"minReplicas":1,"roles":["ingress-nginx"]}}}}}]}
|
|
JSON
|
|
rc=0
|
|
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
|
|
[ "$rc" -eq 0 ]
|
|
if grep -qE 'label helmrelease' "$FAKE_CMDLOG"; then echo "FAIL: no HelmRelease must be labelled"; false; fi
|
|
rm -rf "$WORK"
|
|
}
|
|
|
|
@test "pinned-but-unadopted pools are published to the unadopted ConfigMap before the stamp" {
|
|
prep
|
|
# A pool the migration pins prune-proof but cannot adopt (here: child release
|
|
# name over 53 chars) must leave a durable record — the Job-log warnings are
|
|
# reaped by the next upgrade's before-hook-creation policy and migration 56 does
|
|
# not re-run. Assert the unadopted ConfigMap is applied AND the run still stamps.
|
|
cat > "$FAKE_HR_LIST" <<'JSON'
|
|
{"items":[{"metadata":{"namespace":"tenant-test","name":"kubernetes-verylongclustername-thirty"},"spec":{"values":{"nodeGroups":{"poolnamethatislong":{"minReplicas":0,"roles":["ingress-nginx"]}}}}}]}
|
|
JSON
|
|
rc=0
|
|
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
|
|
cat "$WORK/out"
|
|
[ "$rc" -eq 0 ]
|
|
grep -qF -- "pinned prune-proof but NOT adopted" "$WORK/out"
|
|
grep -qF -- "APPLY-UNADOPTED-CM" "$FAKE_CMDLOG"
|
|
grep -qF -- "STAMP" "$FAKE_CMDLOG"
|
|
rm -rf "$WORK"
|
|
}
|
|
|
|
@test "a clean run writes NO unadopted ConfigMap (empty skip record)" {
|
|
prep
|
|
# The default fixture adopts cleanly (no skip branch), so no unadopted ConfigMap
|
|
# must be applied — only the version STAMP.
|
|
rc=0
|
|
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
|
|
[ "$rc" -eq 0 ]
|
|
if grep -qF -- "APPLY-UNADOPTED-CM" "$FAKE_CMDLOG"; then echo "FAIL: no unadopted ConfigMap must be applied"; false; fi
|
|
grep -qF -- "STAMP" "$FAKE_CMDLOG"
|
|
rm -rf "$WORK"
|
|
}
|
|
|
|
@test "a failed unadopted-ConfigMap publish does NOT abort the migration (best-effort record)" {
|
|
prep
|
|
# The record is a convenience artifact, not a safety mechanism: every skipped
|
|
# pool was already pinned prune-proof earlier in the sweep. A permanent publish
|
|
# failure (e.g. exceeding etcd's object-size limit) must NOT fail the migration
|
|
# closed and deadlock the fleet upgrade — it must warn and still stamp.
|
|
export FAKE_UNADOPTED_CM_FAIL=1
|
|
cat > "$FAKE_HR_LIST" <<'JSON'
|
|
{"items":[{"metadata":{"namespace":"tenant-test","name":"kubernetes-verylongclustername-thirty"},"spec":{"values":{"nodeGroups":{"poolnamethatislong":{"minReplicas":0,"roles":["ingress-nginx"]}}}}}]}
|
|
JSON
|
|
rc=0
|
|
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
|
|
cat "$WORK/out"
|
|
[ "$rc" -eq 0 ]
|
|
grep -qF -- "could not publish the unadopted-pools record" "$WORK/out"
|
|
grep -qF -- "STAMP" "$FAKE_CMDLOG"
|
|
unset FAKE_UNADOPTED_CM_FAIL
|
|
rm -rf "$WORK"
|
|
}
|
|
|
|
@test "an unlabelled parent HR on the OLD inline spec.chart shape is restamped too" {
|
|
prep
|
|
# The application.kind label predates spec.chartRef, so the real unlabelled
|
|
# population is the pre-chartRef inline shape: spec.chart.spec.chart="kubernetes"
|
|
# from the cozystack-apps HelmRepository, no chartRef, no label. Matching only
|
|
# chartRef would miss every one of these and let their workers be pruned. Assert
|
|
# the restamp discovers the inline shape and writes the labels.
|
|
cat > "$FAKE_HR_LIST" <<'JSON'
|
|
{"items":[{"metadata":{"namespace":"tenant-legacy","name":"kubernetes-old"},"spec":{"chart":{"spec":{"chart":"kubernetes","sourceRef":{"kind":"HelmRepository","name":"cozystack-apps"}}},"values":{"nodeGroups":{"md0":{"minReplicas":0,"roles":["ingress-nginx"]}}}}}]}
|
|
JSON
|
|
rc=0
|
|
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
|
|
cat "$WORK/out"
|
|
[ "$rc" -eq 0 ]
|
|
grep -qF -- "restamping application labels on unlabelled parent tenant-legacy/kubernetes-old" "$WORK/out"
|
|
grep -qE 'label helmrelease kubernetes-old .*apps.cozystack.io/application.kind=Kubernetes' "$FAKE_CMDLOG"
|
|
grep -qE 'label helmrelease kubernetes-old .*apps.cozystack.io/application.name=old' "$FAKE_CMDLOG"
|
|
rm -rf "$WORK"
|
|
}
|
|
|
|
@test "a non-kubernetes app on the inline shape is NOT restamped (chart-name exact match)" {
|
|
prep
|
|
# A different app on the old inline shape (chart != "kubernetes") must not be
|
|
# mislabelled as a Kubernetes application by the restamp.
|
|
cat > "$FAKE_HR_LIST" <<'JSON'
|
|
{"items":[{"metadata":{"namespace":"tenant-legacy","name":"kubernetes-decoy"},"spec":{"chart":{"spec":{"chart":"postgres","sourceRef":{"kind":"HelmRepository","name":"cozystack-apps"}}},"values":{}}}]}
|
|
JSON
|
|
rc=0
|
|
bash "$MIG" >"$WORK/out" 2>&1 || rc=$?
|
|
[ "$rc" -eq 0 ]
|
|
if grep -qE 'label helmrelease kubernetes-decoy' "$FAKE_CMDLOG"; then echo "FAIL: the decoy HelmRelease must not be labelled"; false; fi
|
|
rm -rf "$WORK"
|
|
}
|