fix(backups): run the kafka-metadata demo CLI Pod as restricted

The kafka-metadata walkthrough started its long-lived CLI Pod with a
bare kubectl run, so the Pod carried no securityContext and a namespace
enforcing the restricted Pod Security level would reject it. The kafka
topic-data walkthrough already layers the restricted context onto its
CLI Pod; the metadata demo now applies the same JSON patch. A running
CLI Pod left by an earlier run without that context is replaced rather
than reused, since a Pod's securityContext cannot change in place.

The new bats cases read the overrides kubectl run receives and fail
unless both the Pod-level and the container-level fields are present
and the patch is sent as --override-type=json, and check that an owned
Pod without the restricted context is recreated.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
This commit is contained in:
Aleksei Sviridkin committed 2026-10-03 01:43:02 +03:00
1 parent 309875ab62
commit 874e1e44b7
2 files changed
+61 -12

No files matched your search

+16 -4
View File
@@ -150,23 +150,35 @@ kafka_wait_ready() {
# Ensure the long-lived kafka-cli Pod exists and is Ready, so kafka_run can exec
# into it. Idempotent: a Ready Pod this demo owns is reused across calls and
# across the numbered demo scripts; a leftover in a terminal phase (Succeeded /
# Failed) is replaced rather than waited on; and a same-named Pod this demo does
# not own is refused rather than hijacked or deleted. cleanup.sh removes it.
# Failed) is replaced rather than waited on, and so is one started before the
# Pod ran under the restricted profile, since a Pod's securityContext cannot be
# changed in place; and a same-named Pod this demo does not own is refused
# rather than hijacked or deleted. cleanup.sh removes it.
# Every step returns on failure explicitly: this runs as the left side of
# `|| return 1`, and some callers also wrap it in $(...), so errexit never
# applies inside it and a bare failure would carry on to the next step.
kafka_cli_pod() {
local phase owner
local phase owner nonroot overrides
phase=$(kubectl -n "$NAMESPACE" get pod "$KAFKA_CLI_POD" --ignore-not-found -o jsonpath='{.status.phase}') || return 1
owner=$(kubectl -n "$NAMESPACE" get pod "$KAFKA_CLI_POD" --ignore-not-found -o jsonpath='{.metadata.labels.cozystack\.io/backup-demo}') || return 1
if [ -n "$phase" ] && [ "$owner" != "kafka-metadata" ]; then
log_error "Pod $NAMESPACE/$KAFKA_CLI_POD exists but this demo does not own it; refusing to use or delete it"
return 1
fi
if [ "$phase" != "Running" ] && [ "$phase" != "Pending" ]; then
# Only this helper creates the Pod, and it sets every restricted field at
# once, so one of them tells a current Pod from an older one.
nonroot=$(kubectl -n "$NAMESPACE" get pod "$KAFKA_CLI_POD" --ignore-not-found -o jsonpath='{.spec.securityContext.runAsNonRoot}') || return 1
if { [ "$phase" != "Running" ] && [ "$phase" != "Pending" ]; } || [ "$nonroot" != "true" ]; then
# The Strimzi image runs as non-root UID 1001, so runAsNonRoot and
# seccomp at the Pod level plus allowPrivilegeEscalation=false and
# drop-ALL on the container satisfy PSA "restricted" without pinning a
# runAsUser specific to this image tag. The add-only JSON patch leaves
# the run-generated Pod's image, command and args untouched.
overrides='[{"op":"add","path":"/spec/securityContext","value":{"runAsNonRoot":true,"seccompProfile":{"type":"RuntimeDefault"}}},{"op":"add","path":"/spec/containers/0/securityContext","value":{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]}}}]'
kubectl -n "$NAMESPACE" delete pod "$KAFKA_CLI_POD" --grace-period=1 --ignore-not-found >/dev/null || return 1
kubectl -n "$NAMESPACE" run "$KAFKA_CLI_POD" --image="$KAFKA_IMAGE" \
--labels=cozystack.io/backup-demo=kafka-metadata \
--override-type=json --overrides="$overrides" \
--restart=Never --command -- sleep infinity >/dev/null || return 1
fi
kubectl -n "$NAMESPACE" wait --for=condition=Ready "pod/$KAFKA_CLI_POD" \
+45 -8
View File
@@ -3,11 +3,13 @@
# helper the kafka-3-metadata-roundtrip suite seeds and verifies topics through.
#
# kubectl and the Kafka CLI are stub executables on PATH. The kubectl stub
# answers the calls kafka_run makes -- get the CLI Pod's phase and owner label,
# create it when absent, wait for it, and `exec` into it -- and runs the exec'd
# snippet locally against the kafka-topics.sh stub. STUB_PHASE / STUB_OWNER drive
# the reuse guard, and STUB_EXEC_ERROR makes the exec fail so the "a failed read
# says why" claim is pinned rather than asserted in prose.
# answers the calls kafka_run makes -- get the CLI Pod's phase, owner label and
# runAsNonRoot, create it when absent or started without the restricted profile
# (recording the overrides it is given), wait for it, and `exec` into it -- and
# runs the exec'd snippet locally against the kafka-topics.sh stub. STUB_PHASE /
# STUB_OWNER / STUB_NONROOT drive the reuse guard, and STUB_EXEC_ERROR makes the
# exec fail so the "a failed read says why" claim is pinned rather than asserted
# in prose.
#
# cozytest.sh's awk runner recognizes only @test blocks and a bare `}`; there is
# no bats `run` or `$status`, and no setup/teardown, so each case calls
@@ -26,13 +28,21 @@ case "$1 $2 $3" in
case "$*" in
*"jsonpath={.status.phase}"*) printf '%s' "${STUB_PHASE-Running}" ;;
*"jsonpath={.metadata.labels"*) printf '%s' "${STUB_OWNER-kafka-metadata}" ;;
*"jsonpath={.spec.securityContext.runAsNonRoot}"*) printf '%s' "${STUB_NONROOT-true}" ;;
esac
exit 0 ;;
"-n tenant-root delete")
exit 0 ;;
"-n tenant-root run")
# Reachable only when the Pod is absent; a reuse that starts a Pod is a bug.
[ -z "${STUB_PHASE-Running}" ] && exit 0
for a in "$@"; do
case "$a" in
--overrides=*) printf '%s' "${a#--overrides=}" > "$STUB_DIR/overrides.json" ;;
--override-type=*) printf '%s' "${a#--override-type=}" > "$STUB_DIR/override-type" ;;
esac
done
# Reachable only when the Pod is absent or predates the restricted
# profile; a reuse that starts a Pod is a bug.
{ [ -z "${STUB_PHASE-Running}" ] || [ -z "${STUB_NONROOT-true}" ]; } && exit 0
echo "kafka_run started a Pod though a Ready one it owns exists" >&2; exit 95 ;;
"-n tenant-root wait")
exit 0 ;;
@@ -59,7 +69,7 @@ EOF
}
run_topic_meta() {
STUB_DIR=$stub STUB_PHASE="${STUB_PHASE-Running}" STUB_OWNER="${STUB_OWNER-kafka-metadata}" \
STUB_DIR=$stub STUB_PHASE="${STUB_PHASE-Running}" STUB_OWNER="${STUB_OWNER-kafka-metadata}" STUB_NONROOT="${STUB_NONROOT-true}" \
STUB_EXEC_ERROR="${STUB_EXEC_ERROR:-}" STUB_GET_ERROR="${STUB_GET_ERROR:-}" PATH="$stub:$PATH" NAMESPACE=tenant-root \
KAFKA_BIN=$stub TOPIC=orders bash -c '
set -euo pipefail
@@ -88,6 +98,33 @@ run_topic_meta() {
grep -q 'exec -i kafka-cli -- bash -c' "$stub/kubectl.argv"
}
@test "kafka_run starts the CLI Pod under the restricted Pod Security profile" {
make_stubs
export STUB_PHASE=
run_topic_meta >/dev/null
# The overrides are a JSON patch, which kubectl applies only with
# --override-type=json; under the default merge type the array is rejected.
[ "$(cat "$stub/override-type")" = "json" ]
jq --exit-status '
(map(select(.op == "add" and .path == "/spec/securityContext")) | .[0].value) as $pod
| (map(select(.op == "add" and .path == "/spec/containers/0/securityContext")) | .[0].value) as $ctr
| $pod.runAsNonRoot == true
and $pod.seccompProfile.type == "RuntimeDefault"
and $ctr.allowPrivilegeEscalation == false
and $ctr.capabilities.drop == ["ALL"]
' "$stub/overrides.json"
}
@test "kafka_run replaces a Pod it owns that predates the restricted profile" {
make_stubs
export STUB_NONROOT=
out=$(run_topic_meta)
[ "$out" = "3 123456" ]
grep -q 'delete pod kafka-cli' "$stub/kubectl.argv"
grep -q 'run kafka-cli --image=' "$stub/kubectl.argv"
[ "$(cat "$stub/override-type")" = "json" ]
}
@test "kafka_run refuses a same-named Pod the demo does not own" {
make_stubs
export STUB_OWNER=someone-else