|
| 1 | +#!/usr/bin/env bats |
| 2 | +# ----------------------------------------------------------------------------- |
| 3 | +# Contract: a Chainsaw script op whose script carries its own waits gives those |
| 4 | +# waits room to fire first. When the op ceiling fires instead, Chainsaw kills |
| 5 | +# the shell, the script's own failure message never prints, and a cleanup trap |
| 6 | +# never runs (.chainsaw.yaml states the rule above its catch scripts). |
| 7 | +# |
| 8 | +# The gateway step is held by arithmetic: its polls are `timeout N` prefixes |
| 9 | +# and its cleanup deletes run under `timeout -k K N`, so their sum, kill |
| 10 | +# grace included, is readable from the text. kubectl's own `--timeout` is not |
| 11 | +# counted: it bounds only the wait after the delete request, not discovery or |
| 12 | +# the request itself. Only those two spellings are summed: a wait written any |
| 13 | +# other way, a `kubectl wait` or a sleep loop, is invisible to the sum. The |
| 14 | +# redis-tls step cannot be summed that way, because some of its |
| 15 | +# reads sit in loops and one loop polls, so it carries a step budget that |
| 16 | +# every kubectl call is clamped to, and the guard holds that budget under the |
| 17 | +# op. Its failure messages are checked by running the step's script against a |
| 18 | +# stubbed kubectl. |
| 19 | +# |
| 20 | +# cozytest.sh's awk parser recognizes only @test blocks and a bare `}` on its |
| 21 | +# own line; a `}` at column zero gets `return 0` inserted ahead of it, so the |
| 22 | +# helpers below report through stdout only. |
| 23 | +# |
| 24 | +# Run with: hack/cozytest.sh hack/chainsaw-op-budgets.bats |
| 25 | +# ----------------------------------------------------------------------------- |
| 26 | + |
| 27 | +GATEWAY_SUITE="hack/e2e-chainsaw/gateway/chainsaw-test.yaml" |
| 28 | +GATEWAY_STEP="parent-child-and-route-drive-listener-set" |
| 29 | +REDIS_SUITE="hack/e2e-chainsaw/redis/chainsaw-test.yaml" |
| 30 | +REDIS_STEP="verify-tenant-ca-projection" |
| 31 | + |
| 32 | +# Seconds the gateway op keeps above its summed waits for what carries no cap |
| 33 | +# of its own: three applies and one label read. |
| 34 | +GATEWAY_UNCAPPED=60 |
| 35 | +# Seconds the redis op keeps above its step budget: the kill grace on the last |
| 36 | +# clamped read, one 5s poll sleep, and the openssl and shell work between reads. |
| 37 | +REDIS_AFTER_BUDGET=30 |
| 38 | + |
| 39 | +# Print the lines of the first `- script:` op inside step $2 of file $1, with |
| 40 | +# the op's own indentation, from `- script:` up to the next op or step. |
| 41 | +op_lines() { |
| 42 | + awk -v step="$2" ' |
| 43 | + $0 ~ "^[[:space:]]*- name: " step "[[:space:]]*$" { instep = 1; next } |
| 44 | + instep && !inop && /^[[:space:]]*- script:[[:space:]]*$/ { |
| 45 | + match($0, /^[[:space:]]*/); opindent = RLENGTH; inop = 1; print; next |
| 46 | + } |
| 47 | + inop && /[^[:space:]]/ { |
| 48 | + match($0, /^[[:space:]]*/) |
| 49 | + if (RLENGTH <= opindent) exit |
| 50 | + } |
| 51 | + inop { print } |
| 52 | + instep && /^---/ { exit } |
| 53 | + ' "$1" |
| 54 | +} |
| 55 | + |
| 56 | +# Print the op timeout in seconds of the op in $1 (op_lines output), or |
| 57 | +# nothing when it states none in minutes or seconds. |
| 58 | +op_timeout_seconds() { |
| 59 | + printf '%s\n' "$1" | awk ' |
| 60 | + /^[[:space:]]*timeout:[[:space:]]*[0-9]+m[[:space:]]*$/ { match($0, /[0-9]+/); print substr($0, RSTART, RLENGTH) * 60; exit } |
| 61 | + /^[[:space:]]*timeout:[[:space:]]*[0-9]+s[[:space:]]*$/ { match($0, /[0-9]+/); print substr($0, RSTART, RLENGTH); exit } |
| 62 | + /^[[:space:]]*content:/ { exit } |
| 63 | + ' |
| 64 | +} |
| 65 | + |
| 66 | +# Print the op's shell, or `sh`, the Chainsaw default, when it names none. |
| 67 | +op_shell() { |
| 68 | + sh_name=$(printf '%s\n' "$1" | awk ' |
| 69 | + /^[[:space:]]*shell:[[:space:]]*[^[:space:]]+[[:space:]]*$/ { sub(/^[[:space:]]*shell:[[:space:]]*/, ""); sub(/[[:space:]]*$/, ""); print; exit } |
| 70 | + /^[[:space:]]*content:/ { exit } |
| 71 | + ') |
| 72 | + printf '%s\n' "${sh_name:-sh}" |
| 73 | +} |
| 74 | + |
| 75 | +# Print the op's script, dedented, without the content key. |
| 76 | +op_content() { |
| 77 | + printf '%s\n' "$1" | awk ' |
| 78 | + !incontent && /^[[:space:]]*content:[[:space:]]*\|[[:space:]]*$/ { match($0, /^[[:space:]]*/); keyindent = RLENGTH; incontent = 1; next } |
| 79 | + incontent && /[^[:space:]]/ { |
| 80 | + match($0, /^[[:space:]]*/) |
| 81 | + if (RLENGTH <= keyindent) exit |
| 82 | + if (!bodyindent) bodyindent = RLENGTH |
| 83 | + } |
| 84 | + incontent { print substr($0, bodyindent + 1) } |
| 85 | + ' |
| 86 | +} |
| 87 | + |
| 88 | +# Write a kubectl stub into directory $1 that answers the redis-tls step's |
| 89 | +# reads as a healthy cluster would. STUB_FAIL, when set, is a pattern matched |
| 90 | +# against the arguments: a matching call prints STUB_ERR to stderr and exits |
| 91 | +# STUB_RC instead, after printing STUB_OUT to stdout when that is set, from the |
| 92 | +# STUB_FAIL_FROM-th matching call on (default the first). STUB_PROJ_KEY=1 puts |
| 93 | +# PEM private key material into the projection. STUB_SLEEP_ON is a pattern |
| 94 | +# whose matching calls sleep STUB_SLEEP seconds first; the stub execs sleep, |
| 95 | +# so a timeout that kills it leaves no child holding the output pipe. |
| 96 | +write_redis_stubs() { |
| 97 | + cat > "$1/kubectl" <<'STUB' |
| 98 | +#!/bin/sh |
| 99 | +args="$*" |
| 100 | +if [ -n "${STUB_SLEEP_ON:-}" ]; then |
| 101 | + case "$args" in |
| 102 | + $STUB_SLEEP_ON) exec sleep "${STUB_SLEEP:-60}" ;; |
| 103 | + esac |
| 104 | +fi |
| 105 | +if [ -n "${STUB_FAIL:-}" ]; then |
| 106 | + case "$args" in |
| 107 | + $STUB_FAIL) |
| 108 | + echo x >> "$(dirname "$0")/fail-matches" |
| 109 | + if [ "$(wc -l < "$(dirname "$0")/fail-matches")" -ge "${STUB_FAIL_FROM:-1}" ]; then |
| 110 | + [ -z "${STUB_OUT:-}" ] || echo "$STUB_OUT" |
| 111 | + echo "${STUB_ERR:-error: stubbed failure}" >&2; exit "${STUB_RC:-1}" |
| 112 | + fi ;; |
| 113 | + esac |
| 114 | +fi |
| 115 | +cert_b64=$(printf 'CERT' | base64) |
| 116 | +proj_b64=$cert_b64 |
| 117 | +if [ "${STUB_PROJ_KEY:-}" = 1 ]; then |
| 118 | + proj_b64=$(printf 'CERT\n-----BEGIN PRIVATE KEY-----\nKEY\n' | base64 | tr -d '\n') |
| 119 | +fi |
| 120 | +case "$args" in |
| 121 | + *"get secret redis-secure.tenant-ca -o go-template"*) printf 'ca.crt\n' ;; |
| 122 | + *"get secret redis-secure.ca-cert -o go-template"*) printf 'ca.crt\n' ;; |
| 123 | + *"get secret redis-secure.ca-tls -o go-template"*) printf 'ca.crt\ntls.crt\ntls.key\n' ;; |
| 124 | + *"get secret redis-secure.ca-tls -o jsonpath"*) printf '%s' "$cert_b64" ;; |
| 125 | + *"get secret redis-secure.tenant-ca -o jsonpath"*) printf '%s' "$proj_b64" ;; |
| 126 | + *"get tenantsecret redis-secure.tenant-ca -o jsonpath"*) printf '%s' "$(printf -- '-----BEGIN CERTIFICATE-----' | base64)" ;; |
| 127 | + *"get tenantsecret "*) echo "Error from server (NotFound): tenantsecrets \"x\" not found" >&2; exit 1 ;; |
| 128 | + *"exec rfr-redis-secure-0 -c redis -- cat /tls/ca.crt"*) printf 'CERT\n' ;; |
| 129 | + *"exec rfr-redis-secure-0 -c redis -- timeout 15 redis-cli --tls"*) printf 'PONG\n' ;; |
| 130 | + *"exec rfr-redis-secure-0 -c redis -- timeout 15 redis-cli -h"*) printf 'I/O error\n'; exit 1 ;; |
| 131 | + *"exec rfr-redis-secure-0 -c redis -- sh -c"*) printf 'role:master\n' ;; |
| 132 | + *"exec rfr-redis-secure-1 -c redis -- sh -c"*) printf 'role:slave\nmaster_link_status:up\n' ;; |
| 133 | + *"get pod -l app.kubernetes.io/component=sentinel"*) printf 'rfs-redis-secure-0' ;; |
| 134 | + *"exec rfs-redis-secure-0 -c sentinel"*) printf '10.0.0.1\n6379\n' ;; |
| 135 | + *) echo "kubectl stub: unexpected call: $args" >&2; exit 99 ;; |
| 136 | +esac |
| 137 | +STUB |
| 138 | + cat > "$1/openssl" <<'STUB' |
| 139 | +#!/bin/sh |
| 140 | +cat >/dev/null |
| 141 | +echo "sha256 Fingerprint=AA" |
| 142 | +STUB |
| 143 | + chmod +x "$1/kubectl" "$1/openssl" |
| 144 | +} |
| 145 | + |
| 146 | +# Run the redis-tls step's script, as its op's shell would, against the stubs |
| 147 | +# in $1. Prints the script's combined output and then `rc=<status>`. RS_BUDGET, |
| 148 | +# when set, replaces the step's budget so the clamp can be driven in seconds. |
| 149 | +run_redis_step() { |
| 150 | + rs_op=$(op_lines "$REDIS_SUITE" "$REDIS_STEP") |
| 151 | + rs_shell=$(op_shell "$rs_op") |
| 152 | + op_content "$rs_op" > "$1/step.sh" |
| 153 | + if [ -n "${RS_BUDGET:-}" ]; then |
| 154 | + sed "s/^step_budget=[0-9]*\$/step_budget=${RS_BUDGET}/" "$1/step.sh" > "$1/step.budget" |
| 155 | + if cmp -s "$1/step.sh" "$1/step.budget"; then echo "no step_budget line to override"; echo "rc=97"; return; fi |
| 156 | + mv "$1/step.budget" "$1/step.sh" |
| 157 | + fi |
| 158 | + rs_rc=0 |
| 159 | + rs_out=$(cd "$1" && PATH="$1:$PATH" NAMESPACE=tenant-test STUB_FAIL="${STUB_FAIL:-}" STUB_FAIL_FROM="${STUB_FAIL_FROM:-1}" STUB_RC="${STUB_RC:-1}" STUB_ERR="${STUB_ERR:-}" STUB_OUT="${STUB_OUT:-}" STUB_SLEEP_ON="${STUB_SLEEP_ON:-}" STUB_SLEEP="${STUB_SLEEP:-}" STUB_PROJ_KEY="${STUB_PROJ_KEY:-}" "$rs_shell" "$1/step.sh" 2>&1) || rs_rc=$? |
| 160 | + printf '%s\nrc=%s\n' "$rs_out" "$rs_rc" |
| 161 | +} |
| 162 | + |
| 163 | +@test "the gateway listener-set op outlasts the waits inside it" { |
| 164 | + gw_op=$(op_lines "$GATEWAY_SUITE" "$GATEWAY_STEP") |
| 165 | + [ -n "$gw_op" ] || { echo "no script op found in step $GATEWAY_STEP of $GATEWAY_SUITE" >&2; exit 1; } |
| 166 | + gw_timeout=$(op_timeout_seconds "$gw_op") |
| 167 | + case "$gw_timeout" in ''|*[!0-9]*) echo "the $GATEWAY_STEP op states no timeout in m or s (got '${gw_timeout}')" >&2; exit 1 ;; esac |
| 168 | + gw_body=$(op_content "$gw_op") |
| 169 | + gw_polls=$(printf '%s\n' "$gw_body" | grep -oE '^[[:space:]]*timeout +[0-9]+' | grep -oE '[0-9]+$' || true) |
| 170 | + gw_n=$(printf '%s\n' "$gw_polls" | grep -c . || true) |
| 171 | + [ "$gw_n" -ge 5 ] || { echo "found ${gw_n} capped polls in $GATEWAY_STEP, expected at least 5; the extractor is reading the wrong lines" >&2; exit 1; } |
| 172 | + gw_deletes=$(printf '%s\n' "$gw_body" | grep -E '(^|[[:space:]])kubectl .* delete ' | grep -oE '(^|[[:space:]])timeout -k [0-9]+ [0-9]+ kubectl ' | awk '{ print $3 + $4 }' || true) |
| 173 | + gw_sum=0 |
| 174 | + for gw_w in $gw_polls $gw_deletes; do gw_sum=$(( gw_sum + gw_w )); done |
| 175 | + [ $(( gw_timeout - gw_sum )) -ge "$GATEWAY_UNCAPPED" ] || { |
| 176 | + echo "the $GATEWAY_STEP op allows ${gw_timeout}s; its polls ($(printf '%s ' $gw_polls)) and cleanup deletes ($(printf '%s ' $gw_deletes)) may wait ${gw_sum}s" >&2 |
| 177 | + echo "it must keep ${GATEWAY_UNCAPPED}s above them for the uncapped applies and reads" >&2 |
| 178 | + exit 1 |
| 179 | + } |
| 180 | +} |
| 181 | + |
| 182 | +@test "every delete in the gateway listener-set cleanup is bounded as a whole" { |
| 183 | + # A tenant delete blocks until its child namespaces drain, and a stalled |
| 184 | + # API request blocks with no bound at all, so a delete not wrapped in an |
| 185 | + # outer timeout is a wait the op sum above cannot see. |
| 186 | + gw_op=$(op_lines "$GATEWAY_SUITE" "$GATEWAY_STEP") |
| 187 | + gw_deletes=$(op_content "$gw_op" | grep -E '(^|[[:space:]])kubectl .* delete ' || true) |
| 188 | + [ -n "$gw_deletes" ] || { echo "no kubectl delete found in $GATEWAY_STEP; the cleanup this test reasons about is gone" >&2; exit 1; } |
| 189 | + gw_bad=$(printf '%s\n' "$gw_deletes" | grep -vE '(^|[[:space:]])timeout -k [0-9]+ [0-9]+ kubectl ' || true) |
| 190 | + [ -z "$gw_bad" ] || { echo "deletes in $GATEWAY_STEP whose wait has no bound:" >&2; printf '%s\n' "$gw_bad" >&2; exit 1; } |
| 191 | +} |
| 192 | + |
| 193 | +@test "the gateway listener-set cleanup deletes the parent only once the child is gone" { |
| 194 | + # A parent deleted while its child is still uninstalling wedges the |
| 195 | + # parent's cleanup, so a child delete that ran into its bound must leave |
| 196 | + # the parent alone. |
| 197 | + gw_op=$(op_lines "$GATEWAY_SUITE" "$GATEWAY_STEP") |
| 198 | + gw_order=$(op_content "$gw_op" | awk ' |
| 199 | + /^[[:space:]]*if timeout -k [0-9]+ [0-9]+ kubectl .* delete tenant rchild .*; then[[:space:]]*$/ { guarded = 1; next } |
| 200 | + guarded && /[^[:space:]]/ { print (/delete tenant rparent/ ? "ok" : "unguarded"); exit } |
| 201 | + /delete tenant rparent/ { print "unguarded"; exit } |
| 202 | + ') |
| 203 | + [ "$gw_order" = ok ] || { echo "the rparent delete in $GATEWAY_STEP does not sit directly under a successful rchild delete (got '${gw_order}')" >&2; exit 1; } |
| 204 | +} |
| 205 | + |
| 206 | +@test "a failed tenant delete in the gateway cleanup fails the step with its own error" { |
| 207 | + # A tenant left in place is one every later suite inherits, so |
| 208 | + # the step must say so and fail rather than pass with the leak. |
| 209 | + gw_dir=$(mktemp -d) |
| 210 | + op_content "$(op_lines "$GATEWAY_SUITE" "$GATEWAY_STEP")" | awk '/^cleanup\(\) \{/ { on = 1 } on { print } on && /^\}/ { exit }' > "$gw_dir/cleanup.sh" |
| 211 | + grep -q 'delete tenant rchild' "$gw_dir/cleanup.sh" || { echo "no cleanup() with the rchild delete found in $GATEWAY_STEP" >&2; exit 1; } |
| 212 | + # The stub fails the delete of the tenant named in STUB_FAIL_TENANT with a |
| 213 | + # Forbidden error, which is not the bound firing, and logs every delete. |
| 214 | + printf '%s\n' '#!/bin/sh' 'echo "$*" >> "$(dirname "$0")/calls"' 'case "$*" in *"delete tenant $STUB_FAIL_TENANT "*) echo "Error from server (Forbidden): stubbed" >&2; exit 3 ;; esac' > "$gw_dir/kubectl" |
| 215 | + chmod +x "$gw_dir/kubectl" |
| 216 | + for gw_tenant in rchild rparent; do |
| 217 | + : > "$gw_dir/calls" |
| 218 | + gw_rc=0 |
| 219 | + gw_out=$(PATH="$gw_dir:$PATH" STUB_FAIL_TENANT="$gw_tenant" sh -c '. "$1/cleanup.sh"; ns=tenant-test; cleanup' sh "$gw_dir" 2>&1) || gw_rc=$? |
| 220 | + [ "$gw_rc" -ne 0 ] || { printf '%s\n' "$gw_out" >&2; echo "cleanup exited 0 with the $gw_tenant delete failed" >&2; exit 1; } |
| 221 | + case "$gw_out" in *"Forbidden"*"$gw_tenant delete exited 3"*"rparent is left in tenant-test"*) ;; *) printf '%s\n' "$gw_out" >&2; echo "with the $gw_tenant delete failed, the output does not carry kubectl's error, its exit status and the tenant left behind" >&2; exit 1 ;; esac |
| 222 | + if [ "$gw_tenant" = rchild ] && grep -q 'delete tenant rparent' "$gw_dir/calls"; then echo "the parent was deleted although the child delete failed" >&2; exit 1; fi |
| 223 | + done |
| 224 | + rm -rf "$gw_dir" |
| 225 | +} |
| 226 | + |
| 227 | +@test "the redis-tls projection op outlasts the step budget its reads are clamped to" { |
| 228 | + rt_op=$(op_lines "$REDIS_SUITE" "$REDIS_STEP") |
| 229 | + [ -n "$rt_op" ] || { echo "no script op found in step $REDIS_STEP of $REDIS_SUITE" >&2; exit 1; } |
| 230 | + rt_timeout=$(op_timeout_seconds "$rt_op") |
| 231 | + case "$rt_timeout" in ''|*[!0-9]*) echo "the $REDIS_STEP op states no timeout in m or s (got '${rt_timeout}')" >&2; exit 1 ;; esac |
| 232 | + rt_body=$(op_content "$rt_op") |
| 233 | + rt_budget=$(printf '%s\n' "$rt_body" | sed -n 's/^step_budget=\([0-9][0-9]*\)$/\1/p') |
| 234 | + [ -n "$rt_budget" ] || { |
| 235 | + echo "$REDIS_STEP sets no step_budget, so nothing bounds the sum of its reads below the ${rt_timeout}s op" >&2 |
| 236 | + echo "its kubectl call sites alone come to $(printf '%s\n' "$rt_body" | grep -cE '(^|[^[:alnum:]_])k -n ')" >&2 |
| 237 | + exit 1 |
| 238 | + } |
| 239 | + [ $(( rt_timeout - rt_budget )) -ge "$REDIS_AFTER_BUDGET" ] || { |
| 240 | + echo "the $REDIS_STEP op allows ${rt_timeout}s and the step budget is ${rt_budget}s" >&2 |
| 241 | + echo "it must keep ${REDIS_AFTER_BUDGET}s above the budget for the last read's kill grace and the work between reads" >&2 |
| 242 | + exit 1 |
| 243 | + } |
| 244 | + # The budget bounds only the reads that go through k(). A bare kubectl |
| 245 | + # outside it would add an uncapped read the arithmetic above never sees. |
| 246 | + rt_bare=$(printf '%s\n' "$rt_body" | grep -vE '^[[:space:]]*#' | grep -nE '(^|[^[:alnum:]_-])kubectl([[:space:]]|$)' | grep -v '"\$k_left" kubectl' || true) |
| 247 | + [ -z "$rt_bare" ] || { echo "kubectl calls in $REDIS_STEP that bypass the clamped k():" >&2; printf '%s\n' "$rt_bare" >&2; exit 1; } |
| 248 | +} |
| 249 | + |
| 250 | +@test "the redis-tls projection script passes against a healthy stub" { |
| 251 | + # The positive control for the two failure tests below: without it, a |
| 252 | + # stub that broke the script somewhere else would make them pass. |
| 253 | + rt_dir=$(mktemp -d) |
| 254 | + write_redis_stubs "$rt_dir" |
| 255 | + rt_out=$(run_redis_step "$rt_dir") |
| 256 | + case "$rt_out" in *" |
| 257 | +rc=0") ;; *) printf '%s\n' "$rt_out" >&2; echo "the step failed against a healthy stub" >&2; exit 1 ;; esac |
| 258 | + rm -rf "$rt_dir" |
| 259 | +} |
| 260 | + |
| 261 | +@test "a failed replication exec reports its own error, not the 15s cap" { |
| 262 | + rt_dir=$(mktemp -d) |
| 263 | + write_redis_stubs "$rt_dir" |
| 264 | + rt_out=$(STUB_FAIL='*exec rfr-redis-secure-1 -c redis -- sh -c*' STUB_RC=1 \ |
| 265 | + STUB_ERR='Error from server (Forbidden): pods "rfr-redis-secure-1" is forbidden' run_redis_step "$rt_dir") |
| 266 | + case "$rt_out" in *" |
| 267 | +rc=0") printf '%s\n' "$rt_out" >&2; echo "the step passed with a forbidden exec" >&2; exit 1 ;; esac |
| 268 | + case "$rt_out" in *"15s cap"*) printf '%s\n' "$rt_out" >&2; echo "a forbidden exec was reported as the 15s cap firing" >&2; exit 1 ;; esac |
| 269 | + case "$rt_out" in *"rfr-redis-secure-1"*"exit 1"*"Forbidden"*) ;; *) printf '%s\n' "$rt_out" >&2; echo "the message does not carry the exit status and what the exec printed" >&2; exit 1 ;; esac |
| 270 | + rm -rf "$rt_dir" |
| 271 | +} |
| 272 | + |
| 273 | +@test "a failed sentinel exec reports its own error, not the 15s cap" { |
| 274 | + rt_dir=$(mktemp -d) |
| 275 | + write_redis_stubs "$rt_dir" |
| 276 | + rt_out=$(STUB_FAIL='*exec rfs-redis-secure-0 -c sentinel*' STUB_RC=1 \ |
| 277 | + STUB_ERR='command terminated with exit code 1' STUB_OUT='NOAUTH Authentication required.' run_redis_step "$rt_dir") |
| 278 | + case "$rt_out" in *" |
| 279 | +rc=0") printf '%s\n' "$rt_out" >&2; echo "the step passed with a failed sentinel exec" >&2; exit 1 ;; esac |
| 280 | + case "$rt_out" in *"15s cap"*) printf '%s\n' "$rt_out" >&2; echo "a refused sentinel command was reported as the 15s cap firing" >&2; exit 1 ;; esac |
| 281 | + case "$rt_out" in *"command terminated with exit code 1"*"rfs-redis-secure-0 failed (exit 1): NOAUTH Authentication required."*) ;; *) printf '%s\n' "$rt_out" >&2; echo "the output does not carry what the exec wrote to both streams and its exit status" >&2; exit 1 ;; esac |
| 282 | + rm -rf "$rt_dir" |
| 283 | +} |
| 284 | + |
| 285 | +@test "a failed Secret read is not reported as a wrong source name" { |
| 286 | + # base64 -d exits 0 on empty input, so without pipefail the pipeline takes |
| 287 | + # its status and the emptiness check below names the wrong cause. |
| 288 | + rt_dir=$(mktemp -d) |
| 289 | + write_redis_stubs "$rt_dir" |
| 290 | + rt_out=$(STUB_FAIL='*get secret redis-secure.ca-tls -o jsonpath*' STUB_RC=1 \ |
| 291 | + STUB_ERR='Error from server (ServiceUnavailable): the server is currently unable to handle the request' run_redis_step "$rt_dir") |
| 292 | + case "$rt_out" in *" |
| 293 | +rc=0") printf '%s\n' "$rt_out" >&2; echo "the step passed with a failed Secret read" >&2; exit 1 ;; esac |
| 294 | + case "$rt_out" in *"source name or key is wrong"*) printf '%s\n' "$rt_out" >&2; echo "an API error was reported as a wrong source name" >&2; exit 1 ;; esac |
| 295 | + case "$rt_out" in *"ServiceUnavailable"*) ;; *) printf '%s\n' "$rt_out" >&2; echo "the API error is missing from the output" >&2; exit 1 ;; esac |
| 296 | + rm -rf "$rt_dir" |
| 297 | +} |
| 298 | + |
| 299 | +@test "the private-key check fails when the projection it reads carries a key" { |
| 300 | + # The projection is read once above this check. A check that reads it |
| 301 | + # again passes when that second read fails, because an empty read holds |
| 302 | + # no key material: here the first read returns a key and the second one |
| 303 | + # is refused. |
| 304 | + rt_dir=$(mktemp -d) |
| 305 | + write_redis_stubs "$rt_dir" |
| 306 | + rt_out=$(STUB_PROJ_KEY=1 STUB_FAIL='*get secret redis-secure.tenant-ca -o jsonpath*' STUB_FAIL_FROM=2 STUB_RC=1 \ |
| 307 | + STUB_ERR='Error from server (ServiceUnavailable): the server is currently unable to handle the request' run_redis_step "$rt_dir") |
| 308 | + case "$rt_out" in *" |
| 309 | +rc=0") printf '%s\n' "$rt_out" >&2; echo "the step passed with key material in the projection" >&2; exit 1 ;; esac |
| 310 | + case "$rt_out" in *"projection carries PEM private key material"*) ;; *) printf '%s\n' "$rt_out" >&2; echo "the step did not fail on the key material" >&2; exit 1 ;; esac |
| 311 | + rm -rf "$rt_dir" |
| 312 | +} |
| 313 | + |
| 314 | +@test "a read that finds the step budget spent does not run and says so" { |
| 315 | + rt_dir=$(mktemp -d) |
| 316 | + write_redis_stubs "$rt_dir" |
| 317 | + rt_out=$(RS_BUDGET=0 run_redis_step "$rt_dir") |
| 318 | + case "$rt_out" in *" |
| 319 | +rc=0") printf '%s\n' "$rt_out" >&2; echo "the step passed with its budget spent before the first read" >&2; exit 1 ;; esac |
| 320 | + case "$rt_out" in *"step budget is spent, so this read did not run: -n tenant-test get secret redis-secure.tenant-ca"*) ;; *) printf '%s\n' "$rt_out" >&2; echo "the first read did not report the spent budget" >&2; exit 1 ;; esac |
| 321 | + rm -rf "$rt_dir" |
| 322 | +} |
| 323 | + |
| 324 | +@test "a read is clamped to what the step budget has left and names itself" { |
| 325 | + # A 2s budget against a read that would hang for 20s: the clamp has to |
| 326 | + # end the read at the budget, not at the 30s per-read cap. |
| 327 | + rt_dir=$(mktemp -d) |
| 328 | + write_redis_stubs "$rt_dir" |
| 329 | + rt_start=$(date +%s) |
| 330 | + rt_out=$(RS_BUDGET=2 STUB_SLEEP_ON='*get secret redis-secure.tenant-ca -o go-template*' STUB_SLEEP=20 run_redis_step "$rt_dir") |
| 331 | + rt_took=$(( $(date +%s) - rt_start )) |
| 332 | + case "$rt_out" in *" |
| 333 | +rc=0") printf '%s\n' "$rt_out" >&2; echo "the step passed with a hung read" >&2; exit 1 ;; esac |
| 334 | + case "$rt_out" in *"ran into its "[12]"s bound: -n tenant-test get secret redis-secure.tenant-ca"*) ;; *) printf '%s\n' "$rt_out" >&2; echo "the hung read did not name itself at the budget's bound" >&2; exit 1 ;; esac |
| 335 | + [ "$rt_took" -lt 15 ] || { echo "the step took ${rt_took}s against a 2s budget; the read was not clamped" >&2; exit 1; } |
| 336 | + rm -rf "$rt_dir" |
| 337 | +} |
0 commit comments