Ctrl K
ComunidadGuías…

hack/check-talosconfigtemplate-name-agreement.bats

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bats
# -----------------------------------------------------------------------------
# The TalosConfigTemplate is named after a content hash of the worker machine
# config, and three places in kubernetes-nodes have to arrive at that name:
#
#   * talos-reconcile-job.yaml creates the object, passing the name to the Job
#     as TCT_NAME;
#   * the same file's Role pins the Job's get/patch/update and delete rights on
#     TalosConfigTemplates to it by resourceNames;
#   * nodegroup.yaml points MachineDeployment.spec.template.spec.bootstrap
#     .configRef.name at it.
#
# If the Job and the MachineDeployment disagree, the MachineDeployment
# references a TalosConfigTemplate that is never created. If the Role disagrees,
# the Job's server-side apply -- a PATCH on the name, even for a create -- is
# Forbidden and the template is never created either. CAPI does not fail in
# either case: it blocks on "templates do not exist" and no worker is ever
# built, so the divergence surfaces as a cluster that quietly never scales
# rather than as a render error.
#
# The chart's helm-unittest suites pin both names, but each pins its own
# literal in its own file. That catches a divergence, yet the output only says
# one literal moved -- which reads as a stale fixture, and re-pasting the new
# value is then the obvious and wrong fix. This guard renders the chart ONCE and
# compares the two values against each other, so the failure states the
# invariant instead of a number.
#
# Rendered with explicit resources rather than an instanceType: an
# instanceType-sized pool resolves its kubelet reservations through a live
# `lookup` of the VirtualMachineClusterInstancetype, which is empty outside a
# cluster. The invariant under test does not depend on which sizing source is
# used -- both consumers read the same assembled group either way.
#
# Compatible with both `bats` directly and the in-repo cozytest.sh runner, which
# runs each @test in a fresh subshell under `set -u` and honors no setup() or
# teardown(), so TMP is provisioned inline per test and cleaned without an EXIT
# trap (docs/agents/e2e-testing.md bans those here).

load test_helper

REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
CHART="$REPO_ROOT/packages/apps/kubernetes-nodes"

render_pool() {
  # $1: output file, remaining args: extra --set flags for the pool shape
  out="$1"; shift
  helm template kubernetes-nodes-myk8s-md0 "$CHART" \
    --namespace tenant-test \
    --set cluster=myk8s \
    --set _cluster.cluster-domain=cozy.local \
    --set version=v1.35 \
    --set minReplicas=0 --set maxReplicas=3 \
    --set instanceType="" \
    --set diskSize=20Gi --set storageClass=replicated \
    --set 'roles[0]=ingress-nginx' \
    --set resources.cpu=2 --set resources.memory=4Gi \
    "$@" > "$out" 2>"$out.err"
  [ -s "$out" ] || { cat "$out.err" >&2; return 1; }
}

job_tct_name() {
  awk '/name: TCT_NAME/ { getline; sub(/^[[:space:]]*value:[[:space:]]*/, ""); gsub(/"/, ""); print; exit }' "$1"
}

role_tct_names() {
  # One line per Role rule on talosconfigtemplates that carries resourceNames.
  awk '/resources: \["talosconfigtemplates"\]/ { getline; if ($0 ~ /resourceNames:/) { sub(/.*resourceNames:[[:space:]]*\[/, ""); sub(/\].*/, ""); gsub(/"/, ""); print } }' "$1"
}

md_config_ref_name() {
  awk '/configRef:/ { f=1; next } f && /name:/ { sub(/^[[:space:]]*name:[[:space:]]*/, ""); print; exit }' "$1"
}

assert_agreement() {
  tmp="$1"; shape="$2"
  job=$(job_tct_name "$tmp/r.yaml")
  md=$(md_config_ref_name "$tmp/r.yaml")

  # An empty capture means the render stopped emitting one of the two fields,
  # which would make a naive string comparison pass on "" == "".
  if [ -z "$job" ] || [ -z "$md" ]; then
    echo "FAIL: could not read both names out of the $shape render."
    echo "  Job TCT_NAME:            '${job:-<missing>}'"
    echo "  MachineDeployment ref:   '${md:-<missing>}'"
    echo "Either the field moved or the render is incomplete; this guard cannot"
    echo "compare what it cannot find, so it fails rather than pass vacuously."
    return 1
  fi

  if [ "$job" != "$md" ]; then
    echo "FAIL: the two TalosConfigTemplate names must be equal, and are not ($shape pool)."
    echo "  talos-reconcile-job.yaml creates: $job"
    echo "  nodegroup.yaml references:        $md"
    echo "The MachineDeployment now points at a TalosConfigTemplate nothing creates."
    echo "CAPI blocks on \"templates do not exist\" and builds no worker, without"
    echo "failing the release. Both names come from a hash of the group assembled"
    echo "by kubernetes-nodes.group in templates/_helpers.tpl: they diverge when a"
    echo "call site stops feeding that helper, or when a key it carries is dropped."
    echo "Fix the input they share; do not re-pin either literal."
    return 1
  fi

  # Two named rules: get/patch/update, and delete. Fewer means one went unnamed
  # or disappeared, and the per-rule comparison below would pass on nothing.
  roles=$(role_tct_names "$tmp/r.yaml")
  if [ "$(printf '%s\n' "$roles" | grep -c .)" -ne 2 ]; then
    echo "FAIL: expected two named TalosConfigTemplate rules in the talos-reconcile Role ($shape pool), found:"
    printf '  %s\n' "${roles:-<none>}"
    return 1
  fi
  for r in $roles; do
    if [ "$r" != "$md" ]; then
      echo "FAIL: the talos-reconcile Role names a TalosConfigTemplate the Job does not apply ($shape pool)."
      echo "  Role resourceNames:        $r"
      echo "  nodegroup.yaml references: $md"
      echo "The Job's server-side apply is a PATCH on the template's name even when"
      echo "it creates it, so it is Forbidden, and the pool never gets a template"
      echo "to scale onto. Name the rule after the same content-hash name."
      return 1
    fi
  done
}

@test "Job and MachineDeployment agree on the TalosConfigTemplate name" {
  TMP=$(mktemp -d)
  render_pool "$TMP/r.yaml"
  assert_agreement "$TMP" "default"
  rm -rf "$TMP"
}

@test "they still agree once the hashed spec changes (GPU pool)" {
  # A GPU pool renders extra nodeLabels into the machine config, so the hash --
  # and therefore both names -- must move together. Pinning the agreement on a
  # second shape is what catches a call site that reads a subset of the group:
  # the default shape renders gpus [], kubelet {} and instanceType "" as falsy,
  # so a consumer missing those keys is indistinguishable from one carrying them.
  TMP=$(mktemp -d)
  render_pool "$TMP/r.yaml" --set 'gpus[0].name=nvidia.com/GH200'
  assert_agreement "$TMP" "GPU"
  rm -rf "$TMP"
}

@test "they still agree with an explicit kernelModules list" {
  # kernelModules has no values.yaml default, so the default and GPU shapes both
  # leave it unset and cannot tell a call site that carries it from one that
  # does not. Only a set value moves the hash on one side alone.
  TMP=$(mktemp -d)
  render_pool "$TMP/r.yaml" --set 'kernelModules[0].name=br_netfilter'
  assert_agreement "$TMP" "kernelModules"
  rm -rf "$TMP"
}

@test "the two shapes hash differently" {
  # Guards the guard: if both renders collapsed to the same name, the agreement
  # assertions above would hold trivially and stop proving anything.
  TMP=$(mktemp -d)
  render_pool "$TMP/plain.yaml"
  render_pool "$TMP/gpu.yaml" --set 'gpus[0].name=nvidia.com/GH200'
  plain=$(job_tct_name "$TMP/plain.yaml")
  gpu=$(job_tct_name "$TMP/gpu.yaml")
  if [ "$plain" = "$gpu" ]; then
    echo "FAIL: the plain and GPU pools hashed to the same name ($plain)."
    echo "The content hash is supposed to track the rendered spec, and a GPU pool"
    echo "renders different nodeLabels. Equal names mean the hash input no longer"
    echo "covers the field that changed."
    false
  fi
  rm -rf "$TMP"
}