|
2 | 2 | # ----------------------------------------------------------------------------- |
3 | 3 | # Execution-level regression test for the worker TalosConfigTemplate heredoc: |
4 | 4 | # it pins that the reconcile Job's unquoted heredoc still emits a valid config |
5 | | -# when talos.registryMirrors (and the Talos image coordinates) carry hostile |
6 | | -# free-form input. |
| 5 | +# when talos.registryMirrors, talos.sysctls and the Talos image coordinates |
| 6 | +# carry hostile free-form input. |
7 | 7 | # |
8 | 8 | # Since the Phase 2 split the worker pool, its talos-reconcile Job, and the |
9 | 9 | # TalosConfigTemplate it applies live only in the kubernetes-nodes chart; the |
|
13 | 13 | # The reconcile Job applies the TalosConfigTemplate via an UNQUOTED `cat <<EOF` |
14 | 14 | # heredoc, captured into a variable and then applied, so every line of its body |
15 | 15 | # is subject to shell parameter expansion and command substitution at Job |
16 | | -# runtime. The `talos.registryMirrors` knob and the Talos image coordinates |
17 | | -# render free-form tenant-facing input into that heredoc. A helm-unittest |
| 16 | +# runtime. The `talos.registryMirrors` and `talos.sysctls` knobs and the Talos |
| 17 | +# image coordinates render free-form tenant-facing input into that heredoc. A helm-unittest |
18 | 18 | # string `matchRegex` cannot catch a heredoc that the shell refuses to emit |
19 | 19 | # (e.g. an unbalanced backtick from an un-escaped value): it never runs the |
20 | 20 | # shell. This test does. |
|
26 | 26 | # escaped the value reaches the heredoc and is escaped for backslash, |
27 | 27 | # dollar and backtick. Assert it survives a real shell as a |
28 | 28 | # LITERAL. This covers talos.installerRepository and |
29 | | -# talos.registryMirrors, both genuinely free-form. |
| 29 | +# talos.registryMirrors and talos.sysctls (both keys and values). |
30 | 30 | # validated the value is refused at render before it can reach the heredoc. |
31 | 31 | # Assert the refusal. This covers talos.schematicID and |
32 | 32 | # talos.version, which also land in the worker DataVolume name and |
33 | 33 | # the image URL, where an unquoted YAML scalar makes escaping the |
34 | 34 | # wrong tool. templates/nodegroup.yaml pattern-checks them. It also |
35 | 35 | # covers kernelModules, whose names and parameters |
36 | 36 | # templates/_helpers.tpl checks before rebuilding the list. |
| 37 | +# Sysctl map/value types and UTF-8 are validated before escaping. |
37 | 38 | # |
38 | 39 | # A field that is in neither arm is the regression this file exists to catch. |
39 | 40 | # |
40 | | -# Needs `helm` + `yq` and must run from the repo root. |
| 41 | +# Needs `helm`, `yq`, Bash and Dash and must run from the repo root. |
41 | 42 | # Run with: bats hack/talos-reconcile-heredoc_test.bats |
42 | 43 | # ----------------------------------------------------------------------------- |
43 | 44 |
|
44 | 45 | load test_helper |
45 | 46 |
|
| 47 | +render_sysctl_pool() { |
| 48 | + out="$1"; shift |
| 49 | + helm template kubernetes-nodes-myk8s-md0 packages/apps/kubernetes-nodes \ |
| 50 | + --namespace tenant-test \ |
| 51 | + --set cluster=myk8s \ |
| 52 | + --set _cluster.cluster-domain=cozy.local \ |
| 53 | + --set version=v1.35 \ |
| 54 | + --set minReplicas=0 --set maxReplicas=3 \ |
| 55 | + --set instanceType="" \ |
| 56 | + --set diskSize=20Gi --set storageClass=replicated \ |
| 57 | + --set 'roles[0]=ingress-nginx' \ |
| 58 | + --set resources.cpu=2 --set resources.memory=4Gi \ |
| 59 | + "$@" > "$out" 2>"$out.err" |
| 60 | + [ -s "$out" ] || { cat "$out.err" >&2; return 1; } |
| 61 | +} |
| 62 | + |
46 | 63 | @test "kubernetes-nodes refuses hostile Talos image coordinates at render" { |
47 | 64 | work=$(mktemp -d) |
48 | 65 | cat > "$work/vals.yaml" <<'VALS' |
@@ -334,3 +351,85 @@ VALS |
334 | 351 | || { echo "kernelModules parameter was not preserved literally" >&2; printf '%s\n' "$out" | grep -A6 'kernel:' >&2; rm -rf "$work"; exit 1; } |
335 | 352 | rm -rf "$work" |
336 | 353 | } |
| 354 | + |
| 355 | +@test "sysctl names and values survive the actual Job heredoc literally without shell execution" { |
| 356 | + TMP=$(mktemp -d) |
| 357 | + cat > "$TMP/values.yaml" <<'VALUES' |
| 358 | +talos: |
| 359 | + sysctls: |
| 360 | + user.max_user_namespaces: "11255" |
| 361 | + net.ipv4.ip_local_port_range: "1024\t65535" |
| 362 | + kernel.panic: "0" |
| 363 | + '$(touch "$SYSCTL_MARKER")': '$(touch "$SYSCTL_MARKER")' |
| 364 | + '`touch "$SYSCTL_MARKER"`': '`touch "$SYSCTL_MARKER"`' |
| 365 | + '${SYSCTL_MARKER}': '${SYSCTL_MARKER}' |
| 366 | + 'backslash\$(touch "$SYSCTL_MARKER")': 'backslash\$(touch "$SYSCTL_MARKER")' |
| 367 | + "quotes\"\\\nEOF\n$(touch \"$SYSCTL_MARKER\")": "quotes\"\\\nEOF\n$(touch \"$SYSCTL_MARKER\")" |
| 368 | + "unicode\u0085EOF\u2028EOF\u2029": "unicode\u0085EOF\u2028EOF\u2029" |
| 369 | + "empty": "" |
| 370 | +VALUES |
| 371 | + render_sysctl_pool "$TMP/r.yaml" -f "$TMP/values.yaml" |
| 372 | + yq -r 'select(.kind == "Job") | .spec.template.spec.containers[0].command[2]' "$TMP/r.yaml" > "$TMP/job.sh" |
| 373 | + # Execute only the real manifest-producing heredoc, never the surrounding |
| 374 | + # kubectl/Talos operations. YAML is parsed on both sides of the shell so |
| 375 | + # escaping that prevents execution but corrupts the machine config also fails. |
| 376 | + awk '/^TCT_MANIFEST=/{emit=1} emit{print} emit && /^\)"$/{exit}' "$TMP/job.sh" > "$TMP/heredoc.sh" |
| 377 | + [ -s "$TMP/heredoc.sh" ] |
| 378 | + printf '\nprintf "%%s\\n" "$TCT_MANIFEST"\n' >> "$TMP/heredoc.sh" |
| 379 | + yq -o=json '.talos.sysctls | sort_keys(.)' "$TMP/values.yaml" > "$TMP/expected.json" |
| 380 | + for shell in bash dash; do |
| 381 | + SYSCTL_MARKER="$TMP/executed" TALOS_TOKEN=runtime-token "$shell" "$TMP/heredoc.sh" > "$TMP/tct.yaml" |
| 382 | + [ ! -e "$TMP/executed" ] |
| 383 | + yq -o=json '.spec.template.spec.data | from_yaml | .machine.sysctls | sort_keys(.)' "$TMP/tct.yaml" > "$TMP/actual.json" |
| 384 | + diff -u "$TMP/expected.json" "$TMP/actual.json" |
| 385 | + [ "$(yq '.spec.template.spec.data | from_yaml | .machine.token' "$TMP/tct.yaml")" = "runtime-token" ] |
| 386 | + done |
| 387 | + rm -rf "$TMP" |
| 388 | +} |
| 389 | + |
| 390 | +@test "sysctl types are rejected by both the schema and the render-time guard" { |
| 391 | + TMP=$(mktemp -d) |
| 392 | + for value in '11255' 'false' 'null' '["1"]' '{"evil":"$(id)"}'; do |
| 393 | + input="talos.sysctls={\"kernel.panic\":$value}" |
| 394 | + if render_sysctl_pool "$TMP/r.yaml" --set-json "$input"; then |
| 395 | + echo "FAIL: schema accepted a non-string sysctl: $value" |
| 396 | + false |
| 397 | + fi |
| 398 | + grep -q 'values don.t meet the specifications' "$TMP/r.yaml.err" |
| 399 | + if render_sysctl_pool "$TMP/r.yaml" --skip-schema-validation --set-json "$input"; then |
| 400 | + echo "FAIL: template accepted a non-string sysctl: $value" |
| 401 | + false |
| 402 | + fi |
| 403 | + grep -Fq 'talos.sysctls["kernel.panic"] must be a string' "$TMP/r.yaml.err" |
| 404 | + done |
| 405 | + for value in '"$(id)"' 'false' '0' '[]' '["1"]'; do |
| 406 | + input="talos.sysctls=$value" |
| 407 | + if render_sysctl_pool "$TMP/r.yaml" --set-json "$input"; then |
| 408 | + echo "FAIL: schema accepted a non-map sysctls: $value" |
| 409 | + false |
| 410 | + fi |
| 411 | + grep -q 'values don.t meet the specifications' "$TMP/r.yaml.err" |
| 412 | + if render_sysctl_pool "$TMP/r.yaml" --skip-schema-validation --set-json "$input"; then |
| 413 | + echo "FAIL: template accepted a non-map sysctls: $value" |
| 414 | + false |
| 415 | + fi |
| 416 | + grep -Fq 'talos.sysctls must be a map of strings' "$TMP/r.yaml.err" |
| 417 | + done |
| 418 | + rm -rf "$TMP" |
| 419 | +} |
| 420 | + |
| 421 | +@test "sysctl file values reject invalid UTF-8 rather than changing their bytes" { |
| 422 | + TMP=$(mktemp -d) |
| 423 | + # --set-file reads raw bytes, unlike the YAML/JSON-facing application API. |
| 424 | + for bytes in '\377' '\300\257' '\342\202'; do |
| 425 | + printf '%b' "$bytes" > "$TMP/value" |
| 426 | + if render_sysctl_pool "$TMP/r.yaml" --skip-schema-validation --set-file "talos.sysctls.kernel\\.panic=$TMP/value"; then |
| 427 | + echo "FAIL: accepted invalid UTF-8 sysctl bytes: $bytes" |
| 428 | + false |
| 429 | + fi |
| 430 | + grep -Fq 'must be valid UTF-8' "$TMP/r.yaml.err" |
| 431 | + done |
| 432 | + printf '\357\277\275' > "$TMP/value" |
| 433 | + render_sysctl_pool "$TMP/r.yaml" --skip-schema-validation --set-file "talos.sysctls.kernel\\.panic=$TMP/value" |
| 434 | + rm -rf "$TMP" |
| 435 | +} |
0 commit comments