#!/usr/bin/env bats # ----------------------------------------------------------------------------- # Ties the kubernetes-nodes chart's default Talos image to the kubernetes-worker-image # catalog's default golden. The two live in independently edited files: # # packages/apps/kubernetes-nodes/values.yaml talos.{schematicID,version} # packages/system/kubernetes-worker-image/values.yaml images[], storageClass # # A worker pool that opts in with `osImage.builtin: {}` resolves the golden it # clones from the POOL chart's talos defaults, then requires the catalog to hold a # matching (schematicID, version) — packages/apps/kubernetes-nodes/templates/ # nodegroup.yaml fails the whole render when it does not. So a one-sided Talos bump # silently breaks every fresh install using osImage.builtin, and nothing catches it # until a ~95-minute e2e run does. # # The StorageClass is the same trap from the other direction: CDI cannot # CSI-clone across StorageClasses (it would silently fall back to a host-assisted # copy over the pod network), so the render also rejects a group whose # storageClass differs from its golden's. Bumping one file's default alone brings # that guard down on the default path. # # Both checks are cheap and exact, which is the point — they turn a slow, remote # e2e failure into a local one-second unit test. # # Compatible with both `bats` directly and the in-repo cozytest.sh runner, which # runs each @test in a fresh subshell with `set -u` and does not honor bats # setup()/teardown(). load test_helper REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)" APP_VALUES="$REPO_ROOT/packages/apps/kubernetes-nodes/values.yaml" CATALOG_VALUES="$REPO_ROOT/packages/system/kubernetes-worker-image/values.yaml" @test "worker image catalog holds a golden for the pool chart's default Talos (schematicID, version)" { sid=$(yq '.talos.schematicID' "$APP_VALUES") ver=$(yq '.talos.version' "$APP_VALUES") [ -n "$sid" ] && [ "$sid" != "null" ] [ -n "$ver" ] && [ "$ver" != "null" ] # Any entry may match — the catalog is a list of flavors, not an ordered pair. if ! yq '.images[] | .schematicID + " " + .version' "$CATALOG_VALUES" \ | grep -qxF "$sid $ver"; then echo "packages/apps/kubernetes-nodes/values.yaml defaults to Talos:" >&2 echo " schematicID: $sid" >&2 echo " version: $ver" >&2 echo "but the kubernetes-worker-image catalog has no matching images[] entry:" >&2 yq '.images[] | " - " + .schematicID + " " + .version' "$CATALOG_VALUES" >&2 echo "Every worker pool using 'osImage.builtin: {}' would fail to render." >&2 echo "Add the pair to the catalog, or bump both files together." >&2 return 1 fi } @test "worker image catalog golden shares the pool chart's default StorageClass" { sid=$(yq '.talos.schematicID' "$APP_VALUES") ver=$(yq '.talos.version' "$APP_VALUES") app_sc=$(yq '.storageClass' "$APP_VALUES") # The golden's effective class is its own override when set, else the catalog # default — mirroring how the pool chart resolves its class. entry_sc=$(SID="$sid" VER="$ver" yq \ '.images[] | select(.schematicID == strenv(SID) and .version == strenv(VER)) | .storageClass' \ "$CATALOG_VALUES") if [ -z "$entry_sc" ] || [ "$entry_sc" = "null" ]; then entry_sc=$(yq '.storageClass' "$CATALOG_VALUES") fi if [ "$app_sc" != "$entry_sc" ]; then echo "StorageClass mismatch on the default osImage.builtin path:" >&2 echo " packages/apps/kubernetes-nodes/values.yaml storageClass: $app_sc" >&2 echo " kubernetes-worker-image golden effective storageClass: $entry_sc" >&2 echo "CDI cannot CSI-clone across StorageClasses, so the tenant render" >&2 echo "rejects this outright. Keep the two defaults in step." >&2 return 1 fi } # ----------------------------------------------------------------------------- # Second family of default-drift guards on the same chart: the Talos <-> Kubernetes # support matrix. The render guard keyed only on `talos.version`, so a pool could # reach a different Talos release through `osImage.builtin.version` / # `osImage.factory.version` and boot a kubelet outside its support window with no # render failure. The table now lives once in templates/_helpers.tpl and both # paths consult it. # # The symmetry is structural, not observable from a render: the matrix ships one # row (`v1.13`) whose Kubernetes list is a superset of `values.schema.json`'s # `version` enum, so no schema-valid pairing can make either call site fail today. # A helm-unittest case would have to put an out-of-enum `version` in, and # helm-unittest validates the schema, so it cannot. These two guards cover what a # render cannot: that both call sites exist, and that the table still means # something for the defaults the chart ships. NODES_TEMPLATE="$REPO_ROOT/packages/apps/kubernetes-nodes/templates/nodegroup.yaml" NODES_SCHEMA="$REPO_ROOT/packages/apps/kubernetes-nodes/values.schema.json" @test "the support matrix is consulted for the image-resolved Talos version, not only talos.version" { calls=$(grep -c 'include "kubernetes-nodes.assertTalosSupportsKubernetes"' "$NODES_TEMPLATE" || true) if [ "$calls" -lt 2 ]; then echo "nodegroup.yaml calls the Talos support-matrix guard $calls time(s); it needs one for talos.version and one for the version osImage.builtin/osImage.factory resolves to" >&2 echo "without the second call a pool keeps a supported talos.version and still boots an unsupported Talos minor through its image override, with no render failure" >&2 return 1 fi # And the second call must be fed the RESOLVED version. Passing talos.version # twice would satisfy the count above while checking nothing new. if ! grep -q '"talosVersion" \$reqVersion' "$NODES_TEMPLATE"; then echo "the second support-matrix call does not pass \$reqVersion, so it re-checks the value the first call already checked" >&2 return 1 fi } @test "the support matrix covers every Kubernetes version the schema offers, for the default Talos" { ver=$(yq '.talos.version' "$APP_VALUES") minor=$(printf '%s\n' "$ver" | grep -oE '^v[0-9]+\.[0-9]+') [ -n "$minor" ] helpers="$REPO_ROOT/packages/apps/kubernetes-nodes/templates/_helpers.tpl" # The row is a Helm dict literal: `"v1.13" (list "v1.31" "v1.32" ...)`. row=$(grep -oE "\"${minor}\" \(list [^)]*\)" "$helpers" || true) if [ -z "$row" ]; then echo "the Talos support matrix in $helpers has no row for ${minor}, the default talos.version ($ver)" >&2 echo "an unlisted Talos minor passes the guard unchecked, so the guard means nothing on the shipped defaults" >&2 return 1 fi # Every version the schema lets an operator pick must be in that row, or the # guard rejects a combination the API advertises as valid. Capture the enum # first and refuse an empty read: iterating straight over the command # substitution cannot tell "no versions" from "that query found nothing", so a # renamed or relocated property would run the loop zero times and report # success -- the drift this test exists to catch would be the change that # disarms it. enum=$(yq -r '.properties.version.enum[]' "$NODES_SCHEMA" || true) if [ -z "$enum" ]; then echo "could not read .properties.version.enum from $NODES_SCHEMA" >&2 echo "the enum was renamed, moved, or the file relocated; this guard cannot check the matrix without it" >&2 return 1 fi for k in $enum; do case "$row" in *"\"$k\""*) ;; *) echo "values.schema.json offers version $k, but the Talos ${minor} matrix row does not list it:" >&2 echo " $row" >&2 echo "either the row is stale or the enum gained a version Talos ${minor} does not support" >&2 return 1 ;; esac done } # ----------------------------------------------------------------------------- # Third guard, and the premise the first one rests on. The catalog is keyed off # the kubernetes-nodes chart's `talos` defaults, but the parent kubernetes chart # carries its own copy of the same block since the Phase 2 split, and its own # values.yaml says to keep the two in sync. Nothing enforced that: the pool # chart's `make update` copies files/versions.yaml and images/kubectl.tag from # the parent and not the talos block, so a one-sided bump leaves the parent's # support-matrix guard reasoning about one Talos release while every worker boots # another — and the catalog check above would go on passing, because it only ever # reads one of the two files. PARENT_VALUES="$REPO_ROOT/packages/apps/kubernetes/values.yaml" @test "the parent kubernetes chart and kubernetes-nodes agree on their Talos defaults" { for key in version schematicID imageFactoryURL installerRepository registryMirrors; do parent=$(yq ".talos.${key}" "$PARENT_VALUES") pool=$(yq ".talos.${key}" "$APP_VALUES") if [ "$parent" != "$pool" ]; then echo "talos.${key} differs between the two charts:" >&2 echo " packages/apps/kubernetes/values.yaml: $parent" >&2 echo " packages/apps/kubernetes-nodes/values.yaml: $pool" >&2 echo "both are declared 'keep in sync' and a worker pool follows the second while the parent's support-matrix guard reads the first" >&2 return 1 fi done }