Files
cozystack/hack/run-kubernetes-oidc_test.bats
T
Myasnikov Daniil cc942fed68 test(tests): run unit suites with Bats and retain POSIX coverage
The shell translator rewrites test bodies and loses Bats reporting.
The unit lane needs complete TAP results and JUnit reports without
changing how live-cluster suites run.

Sourced production helpers still need a dash pass, and unit test bodies
need nounset without exporting shell options into subprocesses.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 11:04:00 +03:00

311 lines
14 KiB
Bash

#!/usr/bin/env bats
# Unit coverage for the OIDC lifecycle folded into kubernetes-latest.
# Sourcing run-kubernetes.sh only defines functions; every kubectl interaction
# below is replaced with a shell stub, so this file needs no cluster.
load test_helper
@test "HelmRelease upgrade wait rejects stale Ready and stale observedGeneration" {
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
state_file=$(mktemp)
printf '0\n' > "$state_file"
kubectl() {
count=$(sed -n '1p' "$state_file")
count=$(( count + 1 ))
printf '%s\n' "$count" > "$state_file"
case "$count" in
1) printf '7|7|True|' ;;
2) printf '8|7|True|' ;;
*) printf '8|8|True|' ;;
esac
}
sleep() { :; }
cozy_wait_helmrelease_upgrade tenant-test kubernetes-demo 7 5 >/dev/null
[ "$(sed -n '1p' "$state_file")" -eq 3 ] || {
echo "the wait accepted Ready before both generation gates advanced" >&2
exit 1
}
}
@test "HelmRelease upgrade wait fails immediately on Stalled" {
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
calls=$(mktemp)
kubectl() {
printf '%s\n' "$*" >> "$calls"
case "$*" in
*" get helmrelease "*) printf '8|8|False|True' ;;
*" describe helmrelease "*) return 0 ;;
*) return 1 ;;
esac
}
sleep() { echo "sleep must not run after Stalled=True" >&2; return 1; }
if cozy_wait_helmrelease_upgrade tenant-test kubernetes-demo 7 600 >/dev/null 2>&1; then
echo "a Stalled HelmRelease was accepted" >&2
exit 1
fi
[ "$(wc -l < "$calls")" -eq 2 ] || {
echo "the Stalled path did not stop after status plus diagnostics" >&2
cat "$calls" >&2
exit 1
}
}
@test "OIDC binding query fails when kubectl cannot list the bindings" {
# Piped straight into sort, the query took its status from sort, and a
# failed kubectl read as an empty binding list. The status has to hold
# without errexit too, because callers capture it in a substitution.
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
kubectl() { echo "the server is currently unable to handle the request" >&2; return 1; }
if cozy_oidc_bindings demo >/dev/null 2>&1; then
echo "a failed binding query was reported as success" >&2
return 1
fi
}
@test "System OIDC assertion proves rendered objects bootstrap kubeconfig and tenant RBAC" {
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
kubectl() {
case "$*" in
*" wait job kubernetes-demo-oidc-bootstrap "*) return 0 ;;
*" get kamajicontrolplane kubernetes-demo "*)
printf '[--authentication-config=/etc/kubernetes/authentication-config/config.yaml --requestheader-uid-headers=X-Remote-Uid]'
;;
*" get secret kubernetes-demo-oidc-authn-config "*)
printf 'url: https://keycloak.example.test/realms/cozy\naudiences:\n- tenant-test-kubernetes-demo\nuid:\n claim: sub\n' | base64 | tr -d '\n'
;;
*" get keycloakclient.v1.edp.epam.com tenant-test-kubernetes-demo "*) printf 'true' ;;
*" get keycloakclientscope.v1.edp.epam.com tenant-test-kubernetes-demo-audience "*) printf 'oidc-audience-mapper' ;;
*" get secret kubernetes-demo-oidc-kubeconfig "*)
printf 'args:\n- oidc-login\n- --oidc-client-id=tenant-test-kubernetes-demo\n' | base64 | tr -d '\n'
;;
*) echo "unexpected kubectl call: $*" >&2; return 1 ;;
esac
}
cozy_oidc_bindings() {
printf 'e2e-admin@example.test\tcluster-admin\ne2e-viewer@example.test\tview\n'
}
cozy_assert_oidc_system demo
}
@test "System OIDC assertion fails when the aggregation-layer UID header is missing" {
# Same mock as the passing case, minus --requestheader-uid-headers, so that
# flag is the only thing left to fail on. Drop the grep for it from the helper
# and this case goes red, which is what stops it passing for the wrong reason.
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
kubectl() {
case "$*" in
*" wait job kubernetes-demo-oidc-bootstrap "*) return 0 ;;
*" get kamajicontrolplane kubernetes-demo "*)
printf '[--authentication-config=/etc/kubernetes/authentication-config/config.yaml]'
;;
*" get secret kubernetes-demo-oidc-authn-config "*)
printf 'url: https://keycloak.example.test/realms/cozy\naudiences:\n- tenant-test-kubernetes-demo\nuid:\n claim: sub\n' | base64 | tr -d '\n'
;;
*" get keycloakclient.v1.edp.epam.com tenant-test-kubernetes-demo "*) printf 'true' ;;
*" get keycloakclientscope.v1.edp.epam.com tenant-test-kubernetes-demo-audience "*) printf 'oidc-audience-mapper' ;;
*" get secret kubernetes-demo-oidc-kubeconfig "*)
printf 'args:\n- oidc-login\n- --oidc-client-id=tenant-test-kubernetes-demo\n' | base64 | tr -d '\n'
;;
*) echo "unexpected kubectl call: $*" >&2; return 1 ;;
esac
}
cozy_oidc_bindings() {
printf 'e2e-admin@example.test\tcluster-admin\ne2e-viewer@example.test\tview\n'
}
# The subshell restores `set -e`, which the chainsaw step runs the helper
# under: without it a failing `grep -q` mid-function does not end the
# function, and the exit status comes from the last assertion instead.
#
# It has to stand as its own command, not as an `if` condition. Shells
# suppress errexit for a command whose status is being tested, and that
# suppression reaches inside the subshell too, which is why the plain
# `if ( set -e; ... )` spelling reports success here.
local rc=0
set +e
( set -e; cozy_assert_oidc_system demo )
rc=$?
set -e
if [ "${rc}" -eq 0 ]; then
echo "the System assertion passed without --requestheader-uid-headers" >&2
return 1
fi
}
@test "System OIDC assertion fails when the uid claim mapping is missing" {
# Same mock as the passing case, minus the uid mapping, so that mapping is
# the only thing left to fail on. Without it a System-mode user is resolved
# to an empty UID and the published header carries nothing across the
# aggregation layer.
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
kubectl() {
case "$*" in
*" wait job kubernetes-demo-oidc-bootstrap "*) return 0 ;;
*" get kamajicontrolplane kubernetes-demo "*)
printf '[--authentication-config=/etc/kubernetes/authentication-config/config.yaml --requestheader-uid-headers=X-Remote-Uid]'
;;
*" get secret kubernetes-demo-oidc-authn-config "*)
printf 'url: https://keycloak.example.test/realms/cozy\naudiences:\n- tenant-test-kubernetes-demo\n' | base64 | tr -d '\n'
;;
*" get keycloakclient.v1.edp.epam.com tenant-test-kubernetes-demo "*) printf 'true' ;;
*" get keycloakclientscope.v1.edp.epam.com tenant-test-kubernetes-demo-audience "*) printf 'oidc-audience-mapper' ;;
*" get secret kubernetes-demo-oidc-kubeconfig "*)
printf 'args:\n- oidc-login\n- --oidc-client-id=tenant-test-kubernetes-demo\n' | base64 | tr -d '\n'
;;
*) echo "unexpected kubectl call: $*" >&2; return 1 ;;
esac
}
cozy_oidc_bindings() {
printf 'e2e-admin@example.test\tcluster-admin\ne2e-viewer@example.test\tview\n'
}
# Standalone subshell, not an `if` condition: errexit is suppressed for a
# command whose status is tested, and that reaches inside the subshell.
local rc=0
set +e
( set -e; cozy_assert_oidc_system demo )
rc=$?
set -e
if [ "${rc}" -eq 0 ]; then
echo "the System assertion passed without a uid claim mapping" >&2
return 1
fi
}
@test "CustomConfig transition fails when it cannot ask whether System leftovers are gone" {
# The teardown probes used `if kubectl get ... >/dev/null 2>&1`, which reads
# every non-zero exit as "the object is gone" -- including an RBAC denial, a
# timeout or a missing CRD -- so the suite reported a clean System teardown
# for never having observed one. --ignore-not-found separates the two.
#
# One probe is broken per iteration and the other two answer "absent", so
# each of the three is pinned individually: reverting any single one of them
# to the bare form lets that iteration pass and reddens this test. A test
# that merely broke all three would pass on any one surviving probe.
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
cozy_wait_helmrelease_upgrade() { :; }
cozy_oidc_bindings() { printf 'byo-admin@example.test\tcluster-admin\n'; }
for unaskable in \
'keycloakclient.v1.edp.epam.com tenant-test-kubernetes-demo' \
'keycloakclientscope.v1.edp.epam.com tenant-test-kubernetes-demo-audience' \
'secret kubernetes-demo-oidc-kubeconfig'; do
COZY_TEST_UNASKABLE="$unaskable"
kubectl() {
case "$*" in
*" get helmrelease kubernetes-demo "*) printf '7' ;;
*" patch kuberneteses.apps.cozystack.io demo "*) : ;;
*" wait job kubernetes-demo-oidc-bootstrap "*) return 0 ;;
*" get kamajicontrolplane kubernetes-demo "*)
printf '[--authentication-config=/etc/kubernetes/authentication-config/config.yaml --requestheader-uid-headers=X-Remote-Uid]'
;;
*" get secret kubernetes-demo-oidc-authn-config "*)
printf 'url: https://idp.byo.example.test\naudiences:\n- cozystack-byo-demo\n' | base64 | tr -d '\n'
;;
*"${COZY_TEST_UNASKABLE}"*--ignore-not-found*)
# Cannot ask: an RBAC denial or an unreachable apiserver.
return 1
;;
# Absent, the way kubectl reports it with the flag: exit 0, no output.
*--ignore-not-found*) : ;;
*) echo "unexpected kubectl call: $*" >&2; return 1 ;;
esac
}
if cozy_switch_and_assert_oidc_custom_config demo; then
echo "the transition passed while the probe for ${unaskable} could not answer" >&2
return 1
fi
done
}
@test "CustomConfig transition fails when the aggregation-layer UID header is missing" {
# The System helper's twin. Same mock as the passing transition below, minus
# --requestheader-uid-headers, so that flag is the only thing left to fail on.
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
kubectl() {
case "$*" in
*" get helmrelease kubernetes-demo "*) printf '7' ;;
*" patch kuberneteses.apps.cozystack.io demo "*) : ;;
*" wait job kubernetes-demo-oidc-bootstrap "*) return 0 ;;
*" get kamajicontrolplane kubernetes-demo "*)
printf '[--authentication-config=/etc/kubernetes/authentication-config/config.yaml]'
;;
*" get secret kubernetes-demo-oidc-authn-config "*)
printf 'url: https://idp.byo.example.test\naudiences:\n- cozystack-byo-demo\n' | base64 | tr -d '\n'
;;
*--ignore-not-found*) : ;;
*) echo "unexpected kubectl call: $*" >&2; return 1 ;;
esac
}
cozy_wait_helmrelease_upgrade() { :; }
cozy_oidc_bindings() { printf 'byo-admin@example.test\tcluster-admin\n'; }
# Standalone subshell, not an `if` condition: errexit is suppressed for a
# command whose status is tested, and that reaches inside the subshell.
local rc=0
set +e
( set -e; cozy_switch_and_assert_oidc_custom_config demo )
rc=$?
set -e
if [ "${rc}" -eq 0 ]; then
echo "the transition passed without --requestheader-uid-headers" >&2
return 1
fi
}
@test "CustomConfig transition patches the live cluster waits for the new generation and rejects System leftovers" {
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
calls=$(mktemp)
wait_args=$(mktemp)
patch_body=$(mktemp)
kubectl() {
printf '%s\n' "$*" >> "$calls"
case "$*" in
*" get helmrelease kubernetes-demo "*) printf '7' ;;
*" patch kuberneteses.apps.cozystack.io demo "*)
previous=
for argument in "$@"; do
if [ "$previous" = --patch ]; then
printf '%s\n' "$argument" > "$patch_body"
break
fi
previous="$argument"
done
;;
*" wait job kubernetes-demo-oidc-bootstrap "*) return 0 ;;
*" get kamajicontrolplane kubernetes-demo "*)
printf '[--authentication-config=/etc/kubernetes/authentication-config/config.yaml --requestheader-uid-headers=X-Remote-Uid]'
;;
*" get secret kubernetes-demo-oidc-authn-config "*)
printf 'url: https://idp.byo.example.test\naudiences:\n- cozystack-byo-demo\n' | base64 | tr -d '\n'
;;
# Absence the way kubectl reports it under --ignore-not-found: exit 0
# and no output. The fail-closed half is pinned by the next test,
# not by this one -- here a bare probe would read the catch-all's
# non-zero exit as absence and pass.
*" get keycloakclient.v1.edp.epam.com tenant-test-kubernetes-demo --ignore-not-found"*) : ;;
*" get keycloakclientscope.v1.edp.epam.com tenant-test-kubernetes-demo-audience --ignore-not-found"*) : ;;
*" get secret kubernetes-demo-oidc-kubeconfig --ignore-not-found"*) : ;;
*) echo "unexpected kubectl call: $*" >&2; return 1 ;;
esac
}
cozy_wait_helmrelease_upgrade() { printf '%s\n' "$*" > "$wait_args"; }
cozy_oidc_bindings() { printf 'byo-admin@example.test\tcluster-admin\n'; }
cozy_switch_and_assert_oidc_custom_config demo
[ "$(sed -n '1p' "$wait_args")" = 'tenant-test kubernetes-demo 7 600' ] || {
echo "the transition did not wait from the pre-patch generation" >&2
cat "$wait_args" >&2
exit 1
}
jq -e '.spec.oidc.mode == "CustomConfig"' "$patch_body" >/dev/null
jq -e '.spec.oidc.users == [{"email":"byo-admin@example.test","role":"admin"}]' "$patch_body" >/dev/null
jq -er '.spec.oidc.customConfig.config' "$patch_body" | grep -q 'https://idp.byo.example.test'
jq -er '.spec.oidc.customConfig.config' "$patch_body" | grep -q 'cozystack-byo-demo'
}