- Log .status.history regardless of content so a silently empty result (Flux field rename) shows up in CI logs, and treat empty history on a Ready HelmRelease as a distinct failure. A Ready HR by definition has at least one snapshot; anything else is a shape-drift signal. - Replace the unquoted heredoc in remediation-guard.sh with a printf | grep pipeline. printf %s treats statuses as literal payload (no $ expansion surprises for future callers), grep --quiet --extended-regexp returns exit status the caller can forward directly. - Share the etcd-absent values file between both invariant tests (packages/apps/kubernetes/tests/values-ci-no-etcd.yaml) instead of duplicating the --set block. - Fix typo "override applied" -> "override is applied" in the Kubernetes ApplicationDefinition. - Add a coupling comment in the ApplicationDefinition annotation that points at the wait-for-kubeconfig init deadline in _helpers.tpl, so a future operator raising the HR timeout updates the init deadline too. - Clarify the per-annotation timeout comment in rest.go so it stops implying the feature is Kubernetes-only (it is not - only today's one user is). Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
145 lines
5.7 KiB
Bash
145 lines
5.7 KiB
Bash
#!/usr/bin/env bats
|
|
# -----------------------------------------------------------------------------
|
|
# Chart-wide invariant for packages/apps/kubernetes:
|
|
#
|
|
# Every Deployment in this chart that mounts <release>-admin-kubeconfig as a
|
|
# Secret volume MUST:
|
|
# - declare that volume optional: true (so kubelet does not FailedMount
|
|
# while Kamaji is still provisioning the Secret), AND
|
|
# - include the wait-for-kubeconfig init container (so the pod becomes
|
|
# Ready only after Kamaji publishes the Secret).
|
|
#
|
|
# The per-template unittests in packages/apps/kubernetes/tests/ lock in
|
|
# today's three Deployments (cluster-autoscaler, kccm, csi controller) by
|
|
# name. This invariant is stricter: any future Deployment added to this
|
|
# chart that mounts the same Secret but forgets the guard will fail here.
|
|
#
|
|
# Requires: helm, yq (mikefarah v4+), jq. All three are available on the
|
|
# project's CI runners and on the maintainer workstation.
|
|
# -----------------------------------------------------------------------------
|
|
|
|
@test "every Deployment mounting admin-kubeconfig has optional:true and wait-for-kubeconfig init" {
|
|
values_file="packages/apps/kubernetes/tests/values-ci.yaml"
|
|
[ -f "$values_file" ]
|
|
|
|
tmp=$(mktemp -d)
|
|
trap 'rm -rf "$tmp"' EXIT
|
|
|
|
helm template invariant packages/apps/kubernetes \
|
|
--namespace tenant-root \
|
|
--values "$values_file" \
|
|
2>/dev/null > "$tmp/rendered.yaml"
|
|
|
|
# yq streams one JSON object per input document. jq -s slurps the stream
|
|
# into an array so we can treat all Deployments as a single collection.
|
|
yq --output-format=json eval-all '.' "$tmp/rendered.yaml" \
|
|
| jq -s --raw-output '
|
|
map(select(.kind == "Deployment")) |
|
|
map({
|
|
name: .metadata.name,
|
|
volumes: (.spec.template.spec.volumes // []),
|
|
initNames: ((.spec.template.spec.initContainers // []) | map(.name)),
|
|
}) |
|
|
map(
|
|
.name as $n |
|
|
.initNames as $ins |
|
|
(.volumes[] | select(.secret.secretName | test("-admin-kubeconfig$")?))
|
|
| {
|
|
name: $n,
|
|
optional: (.secret.optional == true),
|
|
hasInit: ($ins | index("wait-for-kubeconfig") != null),
|
|
}
|
|
)
|
|
' > "$tmp/summary.json"
|
|
|
|
# At least one Deployment must match; if a refactor removes every
|
|
# admin-kubeconfig volume from this chart, the test must be updated
|
|
# deliberately rather than silently passing.
|
|
matched=$(jq 'length' "$tmp/summary.json")
|
|
[ "$matched" -ge 1 ]
|
|
|
|
offenders=$(jq --raw-output '.[] | select(.optional != true or .hasInit != true) | .name' "$tmp/summary.json")
|
|
|
|
if [ -n "$offenders" ]; then
|
|
echo "Deployments mounting *-admin-kubeconfig without optional:true + wait-for-kubeconfig init:" >&2
|
|
echo "$offenders" >&2
|
|
echo "Full summary:" >&2
|
|
cat "$tmp/summary.json" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Invariant holds for $matched Deployment(s)"
|
|
}
|
|
|
|
@test "chart emits zero admin-kubeconfig Deployments when tenant has no etcd DataStore" {
|
|
# Without a DataStore (parent Tenant has not populated _namespace.etcd yet)
|
|
# the control-plane-side Deployments must NOT render at all. If they did,
|
|
# the wait-for-kubeconfig init would CrashLoopBackOff indefinitely - there
|
|
# would be no KamajiControlPlane to provision the Secret - consuming the
|
|
# HelmRelease wait budget and triggering exactly the remediation cycle the
|
|
# rest of this chart tries to avoid. This test renders the whole chart
|
|
# with etcd empty and asserts no Deployment references the admin-kubeconfig
|
|
# Secret.
|
|
tmp=$(mktemp -d)
|
|
trap 'rm -rf "$tmp"' EXIT
|
|
|
|
helm template invariant packages/apps/kubernetes \
|
|
--namespace tenant-root \
|
|
--values packages/apps/kubernetes/tests/values-ci-no-etcd.yaml \
|
|
2>/dev/null > "$tmp/rendered.yaml"
|
|
|
|
matched=$(
|
|
yq --output-format=json eval-all '.' "$tmp/rendered.yaml" \
|
|
| jq -s '
|
|
map(select(.kind == "Deployment")) |
|
|
map(select(
|
|
(.spec.template.spec.volumes // [])
|
|
| any(.secret.secretName | test("-admin-kubeconfig$")?)
|
|
)) |
|
|
length
|
|
'
|
|
)
|
|
|
|
if [ "$matched" -ne 0 ]; then
|
|
echo "Expected zero Deployments mounting *-admin-kubeconfig when etcd is empty, got $matched:" >&2
|
|
yq --output-format=json eval-all '.' "$tmp/rendered.yaml" \
|
|
| jq -s 'map(select(.kind == "Deployment") | .metadata.name)' >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "No admin-kubeconfig Deployments rendered for empty etcd (as expected)"
|
|
}
|
|
|
|
@test "chart emits zero admin-kubeconfig HelmReleases when tenant has no etcd DataStore" {
|
|
# Same principle as the Deployment variant above, extended to every child
|
|
# HelmRelease (cilium, coredns, csi, cert-manager, ...). They reference
|
|
# *-admin-kubeconfig via kubeConfig.secretRef and would otherwise sit in
|
|
# NotReady forever on an etcd-less tenant, polluting the HelmRelease list
|
|
# the operator sees and contradicting the "awaiting-etcd beacon only"
|
|
# contract of the soft-skip path.
|
|
tmp=$(mktemp -d)
|
|
trap 'rm -rf "$tmp"' EXIT
|
|
|
|
helm template invariant packages/apps/kubernetes \
|
|
--namespace tenant-root \
|
|
--values packages/apps/kubernetes/tests/values-ci-no-etcd.yaml \
|
|
2>/dev/null > "$tmp/rendered.yaml"
|
|
|
|
matched=$(
|
|
yq --output-format=json eval-all '.' "$tmp/rendered.yaml" \
|
|
| jq -s '
|
|
map(select(.kind == "HelmRelease")) |
|
|
map(select(.spec.kubeConfig.secretRef.name | test("-admin-kubeconfig$")?)) |
|
|
length
|
|
'
|
|
)
|
|
|
|
if [ "$matched" -ne 0 ]; then
|
|
echo "Expected zero HelmReleases referencing *-admin-kubeconfig when etcd is empty, got $matched:" >&2
|
|
yq --output-format=json eval-all '.' "$tmp/rendered.yaml" \
|
|
| jq -s 'map(select(.kind == "HelmRelease") | .metadata.name)' >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "No admin-kubeconfig HelmReleases rendered for empty etcd (as expected)"
|
|
}
|