Convert the rotate tests to the injected docker executable (#808) - #812
Convert the rotate tests to the injected docker executable (#808)#812sehkone wants to merge 3 commits into
Conversation
The PATH-faking tests under src/commands/rotate/ substituted a fake
docker by writing a script into a tempdir and prepending that directory
to the process PATH. In Rust 2024 env::set_var is unsafe, and the shared
Mutex those tests held serialised only the code that took it — a tokio
runtime, an HTTP client and an env-filtered tracing subscriber read the
environment from threads that hold nothing.
Every one of them now hands production the fake through the docker seam
the tree already carries, so nothing process-global is touched: approle
sets it on the context make_ctx returns, helpers and stepca_password
replace their Path::new("docker") literal, and infra_cert's three
end-to-end tests drive a new run_rotate_with_exec. run_rotate is that
function with DOCKER_BIN supplied, unchanged in signature and behaviour,
so no rotate flow issues a different docker command than before.
The self-contained fake infra_cert kept privately is promoted into
rotate::test_support with both of its defects fixed. Its log encoding is
framed rather than delimited — an argument count followed by that many
NUL-terminated fields, appended per invocation — so an assertion can
tell ["a b"] from ["a", "b"], an empty argument survives the round trip,
and a multi-call flow keeps every call in order instead of only the
last. It also quotes a log path containing an apostrophe rather than
asserting one cannot appear, since tempdir() may legitimately hand one
back.
container_signal_addresses_the_named_container is removed rather than
converted: converted it would assert the same argv against the same
function as container_signal_runs_the_supplied_executable, differing
only in the fake being removed here.
test_support keeps everything the init tree still imports. Only
TEST_DOCKER_EXIT_ENV goes, retired by the baked-in exit code, and with
it the eleven await_holding_lock allows whose stated justification the
departing lock took with it.
Closes #808
An invocation with no arguments encodes as the bare record `0\0`, which contributes no fields of its own. That makes it the one record whose count is the only thing separating it from the next, so a decoder that scanned for a boundary rather than counting would merge it into whatever followed. Nothing in the rotate flows issues a bare `docker`, so the framing's degenerate case went unexercised. Part of #808
|
[Reviewer Round 1] I found one issue that needs a follow-up before this is ready.
|
|
[Review Verdict Round 1: NOT_APPROVED] |
The promoted helper interpolated the argv-log path into the script text through `Display`, which replaces any byte that is not valid UTF-8. A Unix path is an arbitrary NUL-free byte sequence, so a tempdir rooted below a non-UTF-8 `TMPDIR` left the fake redirecting to a different, usually nonexistent, path -- it would exit before recording argv, and an assertion on that log would read a file nothing ever wrote. This is the same class of legal temp path the apostrophe handling already covers. Assemble the script as bytes and quote the path's own bytes, so the escaping stays byte for byte and no conversion sits in the way. Part of #808
|
[Author Round 1] One review item, accepted.
|
|
[Reviewer Round 2] The Round 1 finding is resolved. The self-contained fake now embeds the log path from raw Unix path bytes, preserves the apostrophe escaping, and adds both a byte-preservation regression test and an end-to-end non-UTF-8-path test where the filesystem supports that name. I found no remaining issues in the updated diff. The tests now inject the executable through the existing seams (including the new private Approved. |
|
[Review Verdict Round 2: APPROVED] |
Suggested squash commitTitle Body |
Summary
Every
PATH-faking test undersrc/commands/rotate/now hands production its fakedockerthrough the injection seam the tree already carries, so none of them mutates the process environment. In Rust 2024env::set_varisunsafe, and the sharedMutexthose tests held serialised only the code that took it — a tokio runtime, an HTTP client and an env-filteredtracingsubscriber read the environment from threads that hold nothing.How each file reaches the seam:
approle.rs—make_ctxtakes the executable and sets it on theRotateContextit returns; the eleven converted tests point the whole tree at a fake by that one field.helpers.rsandstepca_password.rs— thePath::new("docker")literal they already passed becomes the fake's path.infra_cert.rs— the one directly-convertible test callsexecute_reload_strategyoutright. The three end-to-end tests drive a newrun_rotate_with_exec(args, docker, messages)insrc/commands/rotate.rs, mirroring the_with_execpairs ininfra.rs.run_rotateis that function withDOCKER_BINsupplied — signature and behaviour unchanged, andRotateArgsgains no field. The three keep their end-to-end coverage rather than being rewritten against a hand-built context.The promoted fake
infra_cert.rs's privatewrite_self_contained_fake_dockeris promoted intorotate::test_support(visibilitypub(super), so theinittree is neither reached nor disturbed), the local copy is gone, and both of its defects are fixed:docker restart cappends2\0restart\0c\0,docker a '' bappends3\0a\0\0b\0, and a baredockerappends0\0. The oldprintf '%s ' "$@"could not tell["a b"]from["a", "b"], and the older shared script truncated on every call so only the last invocation survived. Counting rather than scanning for a boundary means an empty argument needs no escaping and two invocations can never merge.decode_fake_docker_logsits beside the helper and returns oneVec<String>per invocation in call order; every converted assertion compares against it rather than re-parsing the log.'. The promoted one POSIX-escapes it ('→'\''), so any pathtempfile::tempdir()produces yields a working fake. The escaping and the script assembly work on the path's raw bytes rather than on aDisplayrendering: a Unix path is an arbitrary NUL-free byte sequence, and rendering one that is not UTF-8 would replace those bytes and aim the fake at a path nothing creates.write_self_contained_fake_docker_exitingbakes in a non-zero exit code, which is what retiresTEST_DOCKER_EXIT_ENV— the onetest_supportitem this leaves without a caller, and therefore the one it deletes.ENV_LOCK,env_lock(),ScopedEnvVar,path_with_prepend,write_fake_docker_scriptandTEST_DOCKER_ARGS_ENVall stay, with live callers in theinittree.Assertions
Every test that asserted on the argv still does, as exact equality against a decoded argument list —
helpers.rs's two-elementrestartvector andstepca_password.rs's sixteen-element vector included. The sixapprole.rstests that asserted nothing about docker still assert nothing; their fake exists only to keep the realdockeroff a developer's machine. The three that assert!args_log.exists()each still hand production a fake that would have written that log.run_rotate_infra_cert_signals_and_verifies_openbao's comment reasoning from truncation is rewritten. With every invocation retained, itsrestartcheck now covers the whole run instead of the last call, and its reload check is tightened fromcontains("kill") && contains("SIGHUP")to exact equality against the whole["kill", "-s", "SIGHUP", "bootroot-openbao"]record, so a longer log cannot satisfy it on an unrelated line.Removals
container_signal_addresses_the_named_containeris removed rather than converted. Converted, it would assert the same argv against the same function ascontainer_signal_runs_the_supplied_executable, differing only in the fake this issue removes; the surviving test absorbs its doc comment about addressing the instance's own container.#[allow(clippy::await_holding_lock)]attributes inapprole.rs, with their comments, go with the lock whose existence justified them.No production behaviour changes, no new
unsafe, and nothing undersrc/commands/init/orinit::steps::test_supportis touched.Closes #808
Test plan
cargo test --bin bootrootpasses — 995 tests, 0 failuresgrep -rn "set_var\|remove_var\|env_lock\|ScopedEnvVar\|path_with_prepend" src/commands/rotatereturns nothing (was 62 lines)grep -n "set_var\|remove_var" src/commands/rotate.rsreturns exactly three lines — theset_varinScopedEnvVar::setand theset_var/remove_varpair in itsDrop, retained for theinittreegrep -rn '"PATH"' src/commands/rotatereturns nothing (was nineteenScopedEnvVar::set("PATH", ...)sites)grep -rn "await_holding_lock" src/commands/rotatereturns nothing (was eleven)grep -rln "env_lock\|ScopedEnvVar\|path_with_prepend" src/commands/initstill lists exactlysteps.rs,steps/openbao_tls.rs,steps/openbao_transition.rs,steps/stepca_setup.rsandsteps/http01_admin_tls.rs["--user", "1000:1000"]vs["--user 1000:1000"]), three consecutive invocations in call order, empty arguments (["a", "", "b"]vs["a"]then["b"]), the degenerate argument-less record, a log path containing both'and a space, and the baked-in exit codeDisplayrendering fails the first"--user", &user_arginto a singleformat!("--user {user_arg}")at the production call site failschange_stepca_passphrase_invokes_docker_with_force_and_expected_paths— the encoding did not go lossyapprole.rs!args_log.exists()assertionsrun_rotate_infra_cert_signals_and_verifies_openbao's reload-signal assertion fails when the signal is wrongcargo clippy --all-targets -- -D warningsis cleancargo fmt -- --check --config group_imports=StdExternalCrateis cleanscripts/preflight/ci/check.shpasses (only the pre-existing allowedrustls-pemfileunmaintained advisory)scripts/preflight/ci/e2e-matrix.shande2e-extended.shcould not run on this machine — port 8200 is held by unrelated local processes — so CI is where the rotation flows got exercised against the real docker.run-extendedhas no PR trigger, so it was dispatched by hand and passed; it ran on the commit before the head, which only changed how the fake's script embeds its log path.