mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-04 02:57:27 +09:00
## What this PR does
Every `*.bats` file in the repo runs through `hack/cozytest.sh`, not
real bats — the e2e files via `packages/core/testing/Makefile` and the
`hack/*.bats` unit tests via `make bats-unit-tests`. cozytest runs each
`@test` under `set -e`, and POSIX errexit is suppressed for a pipeline
beginning with `!`. So a bare `! cmd` must-not-happen assertion never
fails the test: when the forbidden condition actually occurs (the
command succeeds), the negated pipeline returns non-zero but errexit
ignores it and the test passes anyway. Ten such assertions across six
files were silently vacuous.
This converts each to `if cmd; then echo "<reason>"; false; fi`. The
`if` condition is exempt from errexit (so the probe itself never aborts
the test), and the `false` in the taken branch is a plain command under
`set -e`, so it aborts and fails the test with a legible message when
the forbidden condition holds. This is the must-not-happen inverse of
the `cmd || { echo …; false; }` pattern already used in the suite (e.g.
harbor.bats, per docs/agents/e2e-testing.md). Original redirections and
quoting are preserved, so happy-path behavior is unchanged — only the
failure path now actually fails.
Converted assertions:
- `bucket`: a read-only user must not be able to upload (access-control
regression guard)
- `gateway`: a child tenant must not have its own gateway HelmRelease
(inheritance)
- `kuberture`: external-dns public/internal hostname isolation
- `e2e-install-cozystack`: tenant-name validation must reject rather
than warn; the version ConfigMap must be gone after delete
- `build-matrix` (unit test): talos/installer must be excluded from the
parallel matrix
- `capture-dataplane` (unit test): kube-dns and pending rows must be
filtered out
A separate in-flight etcd test change owns two more such assertions in
`etcd.bats`; they are left out of this PR to avoid a conflicting edit.
### Release note
```release-note
NONE
```
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Tests**
* Strengthened several end-to-end/Bats assertions to fail more
explicitly when expected cluster conditions aren’t met.
* Improved matrix validation to reliably confirm excluded components are
absent.
* Updated dataplane load balancer filtering, bucket readonly upload, and
gateway inheritance checks to avoid false/ambiguous pass cases.
* Tightened DNS hostname isolation probes and cozystack installer
validations (tenant name dashes, deletion protection) with clearer
“expected vs found” failures.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
263 lines
12 KiB
Bash
263 lines
12 KiB
Bash
#!/usr/bin/env bats
|
|
|
|
# Smoke-test the kuberture system addon: a Package CR for cozystack.kuberture
|
|
# with config.outputs configured must materialise an HR in cozy-kuberture,
|
|
# the controller Deployment must roll out, and the controller must create the
|
|
# configured headless Services with the right external-dns annotations
|
|
# (hostname/target/ttl) reflecting the IPs from default/kubernetes
|
|
# EndpointSlice. Two outputs with different annotationPrefix values exercise
|
|
# the cozystack-relevant property: a single kuberture instance can address
|
|
# multiple external-dns instances from one platform-level package.
|
|
#
|
|
# cozytest.sh is plain POSIX shell — no `[[ ... ]]`, no bats `run` helper,
|
|
# no FD 3, and setup()/teardown() are never auto-invoked. Cleanup is inlined
|
|
# at the end of the @test body (success path, fail-loud) and also wired to
|
|
# cozy_cleanup(), the hook cozytest runs at suite exit and on the first
|
|
# failing test, so the failure path is covered too.
|
|
|
|
# Shared cleanup so the inline success-path call at the end of the @test
|
|
# body and the cozy_cleanup hook cannot drift apart.
|
|
_cleanup() {
|
|
# Probe Pods and their RBAC live outside the chart and need explicit
|
|
# teardown — the Package delete cascade does not own them. The
|
|
# ClusterRole/ClusterRoleBinding are cluster-scoped and would otherwise
|
|
# leak across e2e re-runs on the same sandbox cluster.
|
|
#
|
|
# No `|| true`: on the success-path inline call these run under `set -e`, so
|
|
# a stuck finalizer or wait timeout fails the test (e2e-testing.md §4) instead
|
|
# of silently leaking; --ignore-not-found still covers the clean case. On the
|
|
# failure path cozytest wraps the cozy_cleanup hook in `|| true`, so it stays
|
|
# best-effort there without per-delete masking.
|
|
kubectl delete --namespace cozy-kuberture --ignore-not-found pod/kuberture-e2e-edns-public pod/kuberture-e2e-edns-internal
|
|
kubectl delete --namespace cozy-kuberture --ignore-not-found sa/kuberture-e2e-edns-probe
|
|
kubectl delete --ignore-not-found clusterrolebinding/kuberture-e2e-edns-probe clusterrole/kuberture-e2e-edns-probe
|
|
kubectl delete package.cozystack.io cozystack.kuberture --ignore-not-found --wait=true --timeout=180s
|
|
}
|
|
|
|
cozy_cleanup() {
|
|
# cozytest's failure-path safety net: it runs this at suite exit and on the
|
|
# first failing test. The @test also calls _cleanup inline on the success
|
|
# path; routing both through _cleanup keeps them from drifting. Without this,
|
|
# a failing run would leak the cluster-scoped ClusterRole/ClusterRoleBinding
|
|
# across e2e re-runs on the same sandbox cluster.
|
|
_cleanup
|
|
}
|
|
|
|
@test "Platform-level kuberture publishes default/kubernetes EndpointSlice as annotated headless Services for multiple external-dns instances" {
|
|
pub_host=kuberture-e2e-public.cozystack.test
|
|
int_host=kuberture-e2e-internal.cozystack.test
|
|
pub_prefix=external-dns.alpha.kubernetes.io/
|
|
int_prefix=kuberture-e2e-internal-dns.io/
|
|
|
|
kubectl apply --filename - <<EOF
|
|
apiVersion: cozystack.io/v1alpha1
|
|
kind: Package
|
|
metadata:
|
|
name: cozystack.kuberture
|
|
spec:
|
|
variant: default
|
|
components:
|
|
kuberture:
|
|
values:
|
|
kuberture:
|
|
config:
|
|
outputs:
|
|
- name: public
|
|
hostname:
|
|
- ${pub_host}
|
|
annotationPrefix: ${pub_prefix}
|
|
serviceName: kuberture-e2e-public
|
|
addressSource: endpointslice
|
|
recordTTL: 60
|
|
- name: internal
|
|
hostname:
|
|
- ${int_host}
|
|
annotationPrefix: ${int_prefix}
|
|
serviceName: kuberture-e2e-internal
|
|
addressSource: endpointslice
|
|
recordTTL: 300
|
|
EOF
|
|
|
|
# The cozystack operator materialises the namespace + HR from the Package;
|
|
# kubectl wait errors immediately if the object is not present yet, so loop
|
|
# until it appears before waiting on its condition.
|
|
timeout 180 sh -ec 'until kubectl get namespace cozy-kuberture >/dev/null 2>&1; do sleep 2; done'
|
|
timeout 180 sh -ec 'until kubectl --namespace cozy-kuberture get hr kuberture >/dev/null 2>&1; do sleep 2; done'
|
|
kubectl --namespace cozy-kuberture wait hr kuberture --timeout=300s --for=condition=ready
|
|
|
|
# Controller rollout. Use kubectl wait so the readiness check is anchored on
|
|
# the Available condition and not on a substring match of the replica count.
|
|
kubectl --namespace cozy-kuberture wait deployment kuberture --for=condition=available --timeout=180s
|
|
|
|
# The output Services are created reactively after the controller observes
|
|
# default/kubernetes EndpointSlice. Wait for both to appear.
|
|
for svc in kuberture-e2e-public kuberture-e2e-internal; do
|
|
timeout 120 sh -ec "until kubectl --namespace cozy-kuberture get service ${svc} >/dev/null 2>&1; do sleep 3; done"
|
|
done
|
|
|
|
# Both Services must be headless and carry external-dns annotations under
|
|
# their configured prefix. The `target` annotation must be non-empty —
|
|
# the actual IPs come from default/kubernetes EndpointSlice and depend on
|
|
# the cluster, so we only assert presence.
|
|
pub_json=$(kubectl --namespace cozy-kuberture get service kuberture-e2e-public --output json)
|
|
int_json=$(kubectl --namespace cozy-kuberture get service kuberture-e2e-internal --output json)
|
|
|
|
test "$(echo "${pub_json}" | jq --raw-output '.spec.clusterIP')" = "None"
|
|
test "$(echo "${int_json}" | jq --raw-output '.spec.clusterIP')" = "None"
|
|
|
|
test "$(echo "${pub_json}" | jq --raw-output --arg k "${pub_prefix}hostname" '.metadata.annotations[$k]')" = "${pub_host}"
|
|
test "$(echo "${int_json}" | jq --raw-output --arg k "${int_prefix}hostname" '.metadata.annotations[$k]')" = "${int_host}"
|
|
|
|
pub_target=$(echo "${pub_json}" | jq --raw-output --arg k "${pub_prefix}target" '.metadata.annotations[$k] // ""')
|
|
int_target=$(echo "${int_json}" | jq --raw-output --arg k "${int_prefix}target" '.metadata.annotations[$k] // ""')
|
|
test -n "${pub_target}"
|
|
test -n "${int_target}"
|
|
|
|
test "$(echo "${pub_json}" | jq --raw-output --arg k "${pub_prefix}ttl" '.metadata.annotations[$k]')" = "60"
|
|
test "$(echo "${int_json}" | jq --raw-output --arg k "${int_prefix}ttl" '.metadata.annotations[$k]')" = "300"
|
|
|
|
# The cross-prefix assertion proves the per-output isolation: the public
|
|
# Service must NOT carry the internal prefix's annotation, and vice versa.
|
|
test "$(echo "${pub_json}" | jq --raw-output --arg k "${int_prefix}hostname" '.metadata.annotations[$k] // "absent"')" = "absent"
|
|
test "$(echo "${int_json}" | jq --raw-output --arg k "${pub_prefix}hostname" '.metadata.annotations[$k] // "absent"')" = "absent"
|
|
|
|
# Routing proof: run two short-lived external-dns probes, each with a
|
|
# distinct --annotation-prefix, and verify each one picks up exactly its
|
|
# matching Service. This is the upstream Split Horizon DNS pattern from
|
|
# kubernetes-sigs/external-dns docs/advanced/split-horizon. inmemory
|
|
# provider + noop registry + --once means each probe runs one sync cycle
|
|
# against the live Services in cozy-kuberture, logs what it would publish,
|
|
# and exits — the logs are the assertion surface.
|
|
# external-dns 0.20 needs cluster-wide read on services/endpoints/nodes/
|
|
# namespaces even when `--namespace` narrows the source scope (informer
|
|
# cache initialisation reads cluster-wide before applying the filter).
|
|
# A namespace-scoped Role surfaces as a Pod crash without an obvious
|
|
# error in the bats log, so use ClusterRole + ClusterRoleBinding here.
|
|
kubectl apply --filename - <<EOF
|
|
apiVersion: v1
|
|
kind: ServiceAccount
|
|
metadata:
|
|
name: kuberture-e2e-edns-probe
|
|
namespace: cozy-kuberture
|
|
---
|
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
kind: ClusterRole
|
|
metadata:
|
|
name: kuberture-e2e-edns-probe
|
|
rules:
|
|
- apiGroups: [""]
|
|
resources: ["services", "endpoints", "pods", "nodes", "namespaces"]
|
|
verbs: ["get", "list", "watch"]
|
|
- apiGroups: ["discovery.k8s.io"]
|
|
resources: ["endpointslices"]
|
|
verbs: ["get", "list", "watch"]
|
|
---
|
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
kind: ClusterRoleBinding
|
|
metadata:
|
|
name: kuberture-e2e-edns-probe
|
|
subjects:
|
|
- kind: ServiceAccount
|
|
name: kuberture-e2e-edns-probe
|
|
namespace: cozy-kuberture
|
|
roleRef:
|
|
apiGroup: rbac.authorization.k8s.io
|
|
kind: ClusterRole
|
|
name: kuberture-e2e-edns-probe
|
|
EOF
|
|
|
|
# One Pod per probe — explicit instead of eval-indirection. cozytest.sh runs
|
|
# the body under a POSIX `sh` (not bash) and `${probe}_prefix` indirection
|
|
# via eval is brittle (a name mismatch produces "parameter not set" with no
|
|
# useful diagnostic). Two near-identical blocks read more clearly here.
|
|
kubectl apply --filename - <<EOF
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata:
|
|
name: kuberture-e2e-edns-public
|
|
namespace: cozy-kuberture
|
|
spec:
|
|
serviceAccountName: kuberture-e2e-edns-probe
|
|
restartPolicy: Never
|
|
containers:
|
|
- name: edns
|
|
image: registry.k8s.io/external-dns/external-dns:v0.20.0
|
|
args:
|
|
- --source=service
|
|
- --namespace=cozy-kuberture
|
|
- --provider=inmemory
|
|
- --inmemory-zone=cozystack.test
|
|
- --registry=noop
|
|
- --once
|
|
- --interval=10s
|
|
- --annotation-prefix=${pub_prefix}
|
|
- --domain-filter=cozystack.test
|
|
- --log-level=info
|
|
- --log-format=text
|
|
---
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata:
|
|
name: kuberture-e2e-edns-internal
|
|
namespace: cozy-kuberture
|
|
spec:
|
|
serviceAccountName: kuberture-e2e-edns-probe
|
|
restartPolicy: Never
|
|
containers:
|
|
- name: edns
|
|
image: registry.k8s.io/external-dns/external-dns:v0.20.0
|
|
args:
|
|
- --source=service
|
|
- --namespace=cozy-kuberture
|
|
- --provider=inmemory
|
|
- --inmemory-zone=cozystack.test
|
|
- --registry=noop
|
|
- --once
|
|
- --interval=10s
|
|
- --annotation-prefix=${int_prefix}
|
|
- --domain-filter=cozystack.test
|
|
- --log-level=info
|
|
- --log-format=text
|
|
EOF
|
|
|
|
for probe in public internal; do
|
|
timeout 180 sh -ec "until kubectl --namespace cozy-kuberture get pod kuberture-e2e-edns-${probe} -o jsonpath='{.status.phase}' 2>/dev/null | grep -E '^(Succeeded|Failed)$' >/dev/null; do sleep 3; done"
|
|
phase=$(kubectl --namespace cozy-kuberture get pod kuberture-e2e-edns-${probe} -o jsonpath='{.status.phase}')
|
|
if [ "${phase}" != "Succeeded" ]; then
|
|
# Dump enough context to diagnose the failure from the bats log alone
|
|
# (the e2e sandbox is destroyed before the run artefacts ship, so
|
|
# post-mortem kubectl access on the cluster is not available).
|
|
echo "==== probe ${probe} failed: phase=${phase}; describe ===="
|
|
kubectl --namespace cozy-kuberture describe pod kuberture-e2e-edns-${probe} || true
|
|
echo "==== probe ${probe} logs (current container) ===="
|
|
kubectl --namespace cozy-kuberture logs pod/kuberture-e2e-edns-${probe} || true
|
|
echo "==== probe ${probe} logs (previous container, if any) ===="
|
|
kubectl --namespace cozy-kuberture logs pod/kuberture-e2e-edns-${probe} --previous || true
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
pub_logs=$(kubectl --namespace cozy-kuberture logs pod/kuberture-e2e-edns-public)
|
|
int_logs=$(kubectl --namespace cozy-kuberture logs pod/kuberture-e2e-edns-internal)
|
|
|
|
# The probe that reads the default external-dns prefix must see
|
|
# kuberture-e2e-public.cozystack.test (kuberture-e2e-public Service carries
|
|
# external-dns.alpha.kubernetes.io/hostname=...) and must NOT see the
|
|
# internal hostname (kuberture-e2e-internal carries only the internal prefix).
|
|
# A bare `! echo | grep` is vacuous under cozytest's `set -e` (errexit is
|
|
# suppressed for a `!` pipeline), so a leak of the wrong hostname into a probe
|
|
# would not fail the test; assert the absence via `if ... grep; then ...; false`.
|
|
echo "${pub_logs}" | grep -F "${pub_host}" >/dev/null
|
|
if echo "${pub_logs}" | grep -F "${int_host}" >/dev/null; then echo "FAIL: public-prefix probe must NOT see the internal hostname ${int_host}"; false; fi
|
|
|
|
# And the inverse for the internal-prefix probe.
|
|
echo "${int_logs}" | grep -F "${int_host}" >/dev/null
|
|
if echo "${int_logs}" | grep -F "${pub_host}" >/dev/null; then echo "FAIL: internal-prefix probe must NOT see the public hostname ${pub_host}"; false; fi
|
|
|
|
# _cleanup deletes the probe Pods, their SA, the ClusterRole+ClusterRoleBinding,
|
|
# and finally the Package CR. Inlined here for the success path so a re-run
|
|
# within the same e2e session starts clean; failure path is covered by the
|
|
# sandbox teardown.
|
|
_cleanup
|
|
}
|