mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-23 03:27:50 +09:00
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>
205 lines
7.6 KiB
Bash
205 lines
7.6 KiB
Bash
#!/usr/bin/env bats
|
|
# Asserts that the cozystack-basics admission policies name every API version
|
|
# the vendored Gateway API CRD bundle actually serves for the kind they gate:
|
|
# the three route hostname policies and the Gateway listener hostname policy.
|
|
#
|
|
# Why this needs a test rather than a comment. The policy lists API versions
|
|
# literally in matchConstraints, but the version set it must cover lives in a
|
|
# different package (packages/system/gateway-api-crds), is vendored from
|
|
# upstream, and moves whenever someone runs `make update` there. Nothing
|
|
# connects the two, so the list drifts silently in both directions.
|
|
#
|
|
# Both directions fail closed-looking and open-behaving. A version the bundle
|
|
# serves but the policy does not name is unguarded unless matchPolicy rescues
|
|
# it -- and matchPolicy (unset here, so Equivalent) only converts a request
|
|
# into a version the rule already names, so that rescue lasts exactly as long
|
|
# as at least one named version stays served. A version the policy names but
|
|
# the bundle drops takes the rescue away with it: once no named version is
|
|
# served the rule selects nothing at all, and failurePolicy: Fail does not
|
|
# fire, because nothing failed -- there was simply no match. A fail-closed
|
|
# tenant hostname control then stops applying with no error, no denial, and
|
|
# no log line. Upstream marks versions deprecated well before removing them,
|
|
# so the warning arrives in the bundle diff and nowhere else -- which is
|
|
# precisely the diff nobody reads line by line.
|
|
#
|
|
# Drift here is not caused by a bundle bump: the tlsroutes rule had named a
|
|
# single version since the policy was written, while the bundle served three
|
|
# the whole time.
|
|
#
|
|
# The assertion is coverage, not equality: naming a version the bundle does
|
|
# not serve is inert (an unmatched resourceRules entry is not an error), so
|
|
# the policy may name more than is served, never fewer.
|
|
#
|
|
# The policy side is read from `helm template` output rather than the template
|
|
# source, so what is checked is the object the apiserver would receive.
|
|
#
|
|
# Paths are repo-root-relative. Direct assertions retain compatibility with
|
|
# cozytest, which does not provide Bats-specific helpers or directory variables.
|
|
#
|
|
# Requires: yq (mikefarah v4+), helm. Both are available on the project's CI
|
|
# image and are already relied on by other hack/*.bats.
|
|
#
|
|
# Run with: bats hack/route-hostname-policy-version-coverage.bats
|
|
|
|
load test_helper
|
|
|
|
CRD_BUNDLE=packages/system/gateway-api-crds/templates/crds-experimental.yaml
|
|
BASICS_CHART=packages/system/cozystack-basics
|
|
|
|
served_versions() {
|
|
yq eval-all \
|
|
"select(.kind == \"CustomResourceDefinition\" and .metadata.name == \"$1\")
|
|
| .spec.versions[] | select(.served == true) | .name" \
|
|
"$CRD_BUNDLE" | sort -u
|
|
}
|
|
|
|
policy_versions() {
|
|
helm template cozystack-basics "$BASICS_CHART" \
|
|
--api-versions "admissionregistration.k8s.io/v1/ValidatingAdmissionPolicy" \
|
|
| yq eval-all \
|
|
"select(.kind == \"ValidatingAdmissionPolicy\" and .metadata.name == \"$1\")
|
|
| .spec.matchConstraints.resourceRules[]
|
|
| select(.resources[] | select(. == \"$2\")) | .apiVersions[]" - | sort -u
|
|
}
|
|
|
|
# Echoes, space-prefixed, each served version the policy does not name. The
|
|
# comparison lives here rather than inline so the fixtures at the bottom can
|
|
# drive it with synthetic input; neutering it turns them red.
|
|
#
|
|
# Membership by loop rather than comm(1) with process substitution: the
|
|
# cozytest.sh harness rejects <(...) with a syntax error, which exits non-zero
|
|
# and would read as a genuine failure.
|
|
uncovered() {
|
|
u_served=$1
|
|
u_named=$2
|
|
out=
|
|
for version in $u_served; do
|
|
if ! echo "$u_named" | grep -qxF "$version"; then
|
|
out="$out $version"
|
|
fi
|
|
done
|
|
echo "$out"
|
|
}
|
|
|
|
# Fails on an empty set from either side: a yq expression that stops matching
|
|
# (renamed policy, restructured CRD) would otherwise report full coverage of
|
|
# nothing.
|
|
assert_covers() {
|
|
crd_name=$1
|
|
policy_name=$2
|
|
resource=$3
|
|
|
|
served=$(served_versions "$crd_name")
|
|
named=$(policy_versions "$policy_name" "$resource")
|
|
|
|
if [ -z "$served" ]; then
|
|
echo "no served versions found for $crd_name in $CRD_BUNDLE" >&2
|
|
exit 1
|
|
fi
|
|
if [ -z "$named" ]; then
|
|
echo "no apiVersions found for $resource in $policy_name" >&2
|
|
exit 1
|
|
fi
|
|
|
|
missing=$(uncovered "$served" "$named")
|
|
|
|
if [ -n "$missing" ]; then
|
|
echo "$policy_name does not name every served version of $crd_name" >&2
|
|
echo "served: $(echo "$served" | tr '\n' ' ')" >&2
|
|
echo "named: $(echo "$named" | tr '\n' ' ')" >&2
|
|
echo "missing: $(echo "$missing" | tr '\n' ' ')" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
@test "TLSRoute hostname policy names every served tlsroutes version" {
|
|
assert_covers tlsroutes.gateway.networking.k8s.io \
|
|
cozystack-route-hostname-policy-tls tlsroutes
|
|
}
|
|
|
|
@test "HTTPRoute hostname policy names every served httproutes version" {
|
|
assert_covers httproutes.gateway.networking.k8s.io \
|
|
cozystack-route-hostname-policy httproutes
|
|
}
|
|
|
|
@test "GRPCRoute hostname policy names every served grpcroutes version" {
|
|
assert_covers grpcroutes.gateway.networking.k8s.io \
|
|
cozystack-route-hostname-policy-grpc grpcroutes
|
|
}
|
|
|
|
@test "Gateway hostname policy names every served gateways version" {
|
|
assert_covers gateways.gateway.networking.k8s.io \
|
|
cozystack-gateway-hostname-policy gateways
|
|
}
|
|
|
|
# The four cases above only ever see a tree that is already correct, so on
|
|
# their own they cannot tell a working comparison from one that stopped
|
|
# comparing.
|
|
# These drive it with synthetic input, the way hack/bats-no-exit-trap.bats and
|
|
# hack/md-no-hardwrap.bats feed their checkers a broken sample and require a
|
|
# complaint.
|
|
#
|
|
# Both layers need their own fixture. Driving uncovered() alone leaves
|
|
# assert_covers unprotected -- emptying it to a no-op keeps every other case in
|
|
# this file green, because the live-tree cases are the only callers and the
|
|
# tree they read is correct. So the cases below feed assert_covers itself, in a
|
|
# subshell with the two readers redefined.
|
|
#
|
|
# Each requires the complaint it expects, not merely a non-zero exit. A renamed
|
|
# helper exits 127 and an unbound variable under `set -u` exits 1, and either
|
|
# would read as a successful rejection if only the status were checked -- red
|
|
# for the wrong reason is the same false comfort as no fixture at all.
|
|
assert_covers_rejects() {
|
|
expected=$1
|
|
rc=0
|
|
out=$(
|
|
served_versions() { printf '%s\n' "$FIXTURE_SERVED"; }
|
|
policy_versions() { printf '%s\n' "$FIXTURE_NAMED"; }
|
|
assert_covers fixture.crd fixture-policy fixtureroutes 2>&1
|
|
) || rc=$?
|
|
[ "$rc" != "0" ]
|
|
case "$out" in
|
|
*"$expected"*) ;;
|
|
*) echo "rejected, but not for the expected reason" >&2
|
|
echo "wanted: $expected" >&2
|
|
echo "got: $out" >&2
|
|
exit 1 ;;
|
|
esac
|
|
}
|
|
|
|
@test "assert_covers rejects a policy that omits a served version" {
|
|
FIXTURE_SERVED='v1
|
|
v1alpha2'
|
|
FIXTURE_NAMED='v1alpha2'
|
|
export FIXTURE_SERVED FIXTURE_NAMED
|
|
assert_covers_rejects "does not name every served version"
|
|
}
|
|
|
|
@test "assert_covers rejects an empty served set rather than passing vacuously" {
|
|
FIXTURE_SERVED=''
|
|
FIXTURE_NAMED='v1'
|
|
export FIXTURE_SERVED FIXTURE_NAMED
|
|
assert_covers_rejects "no served versions found"
|
|
}
|
|
|
|
@test "assert_covers rejects an empty named set rather than passing vacuously" {
|
|
FIXTURE_SERVED='v1'
|
|
FIXTURE_NAMED=''
|
|
export FIXTURE_SERVED FIXTURE_NAMED
|
|
assert_covers_rejects "no apiVersions found"
|
|
}
|
|
|
|
@test "the coverage check names a served version the policy omits" {
|
|
missing=$(uncovered 'v1
|
|
v1alpha2
|
|
v1alpha3' 'v1alpha2')
|
|
[ "$missing" = " v1 v1alpha3" ]
|
|
}
|
|
|
|
@test "the coverage check stays silent when every served version is named" {
|
|
missing=$(uncovered 'v1
|
|
v1beta1' 'v1
|
|
v1beta1')
|
|
[ -z "$missing" ]
|
|
}
|