Make the docker executable a caller-supplied value (#807) - #811
Conversation
|
[Reviewer Round 1] No blocking findings. The implementation follows the issue's deliberately narrow contract: the default wrappers in src/commands/infra.rs and ComposeInvocation::command retain their callers while forwarding &Path to executable-taking siblings; the four in-scope spawn paths receive the value through the three specified context carriers; and InitRollback::docker() remains the sole None-to-default resolution point. I also checked that DOCKER_BIN is read only at the permitted default/construction sites and that the explicitly out-of-scope Docker spawns remain untouched. The tests include end-to-end, self-contained fake executables for the rollback compose/restart path, the direct signal/restart spawns, and certificate issuance, so the seam is exercised without changing process-global environment state. PR hygiene is in order as well: it closes #807, links the parent work, and includes a checklist test plan. |
|
[Review Verdict Round 1: APPROVED] |
Suggested squash commitTitle Body |
073e05d to
ad01887
Compare
Tests that substitute a fake `docker` write a script by that name into a temporary directory and prepend it to `PATH`. In Rust 2024 `std::env::set_var` is `unsafe`, and the mutex those tests share does not discharge its contract — it serialises only the code that takes it, while a tokio runtime, an HTTP client and an env-filtered tracing subscriber read the environment from threads that hold nothing. They cannot stop mutating `PATH` until production offers another way to name the executable, because production is what spawns the child. This creates that seam and nothing else. The executable travels as a `&Path` parameter named `docker`, or as a field where the carrier is a method on a value a test already builds: `RotateContext::docker`, `OpenBaoTlsTransition::docker`, and `InitRollback::docker`, whose `Option` keeps a value taken from the derived `Default` from spawning the empty path. `run_docker`, `run_compose`, `run_compose_with_env` and `ComposeInvocation::command` keep their signatures and delegate to executable-taking siblings, so every call site outside the carrier table compiles untouched. Behaviour with nothing supplied is unchanged: a bare program name is still resolved against `PATH` by `Command` itself, and the argument vectors, error contexts and instance pin are as they were. No test is converted and no `PATH` mutation is removed here, and the four spawn sites no conversion reaches stay hard-coded. Closes #807
ad01887 to
1e83bff
Compare
|
Rebased onto One conflict, in
Verified locally: |
Summary
Makes the
dockerexecutable a value the caller supplies at the four spawn sites aPATH-faking test reaches, so a test can redirect the child without mutating the process environment. This creates the seam only — no test is converted and noPATHmutation is removed here.DOCKER_BIN(compose_project.rs) widens topub(crate)and becomes the only spelling of the literal the seam uses. No spawn site reads it.infra::run_docker,infra::run_compose,infra::run_compose_with_envandComposeInvocation::commandkeep their present signatures and become one-line delegations to*_with_execsiblings that takedocker: &Path, so all thirty-nine-odd call sites outside the carrier table compile untouched.&Pathparameter nameddocker, placed immediately beforemessages, except where the carrier is a method on a value a test already constructs. There it is a field:RotateContext::docker: PathBuf(set once inrun_rotate),OpenBaoTlsTransition::docker: &'a Path(set innew), andInitRollback::docker: Option<PathBuf>with a singledocker()accessor resolvingNonetoDOCKER_BIN.InitRollbackkeeps its#[derive(Default)]; theOptionis what keeps a defaulted value from spawning the empty path.rotate::helpers::restart_openbao_agent/restart_container,rotate::approle's entry points (via the context field, avoiding an eighth argument and the#[allow(clippy::too_many_arguments)]it would need),rotate::stepca_password::change_stepca_passphrase,rotate::infra_cert::execute_reload_strategy/try_signal_container,infra::run_compose,init::steps::openbao_tls::issue_openbao_tls_cert,init::steps::http01_admin_tls::issue_http01_admin_tls_cert, andinit::steps::stepca_setup::restart_stepca_openbao_agent.orchestratorpassesrollback.docker()for the threeinitspawns it drives directly.infra::docker_output, the twoclean.rshelpers androtate::helpers::try_restart_container— includingexecute_reload_strategy'sContainerRestartarm, whose&dyn Fn(&str) -> Result<()>seam inrotate/ca.rsmust not move.rollbackgains#[allow(clippy::too_many_lines)], with a comment saying the teardown is one linear sequence that splitting would only scatter. It is the singleallowin the diff, and the one the issue's acceptance criteria name as permitted.rollback_runs_the_executable_it_was_givensupplies it throughInitRollback::docker, runs the realrollback, and asserts the fake ran for both spawn shapes (the compose recreate and the sidecar restart);issue_openbao_tls_cert_runs_the_supplied_executable,restarting_the_stepca_sidecar_runs_the_supplied_executableandcontainer_signal_runs_the_supplied_executabledo the same at the three remaining carriers that reach a spawn. The other four pin the defaults and the plumbing —rollback_defaults_to_the_docker_executable,rollback_resolves_the_executable_it_was_given,the_transition_defaults_to_docker_and_honours_a_named_executable, andcompose_command_runs_the_supplied_executable, which also asserts the argument vector is identical to the default path's. Every fake is self-contained: its argv-log path is interpolated into the script text at write time, so it reads no environment variable at runtime.write_self_contained_fake_dockerininit::steps::test_support, and a private twin ininfra_cert.rs's test module. The one module both trees can reach,rotate::test_support, holds thePATH-based fake every unconverted test still depends on and has to stay untouched until the conversion issues land. The duplication is what that costs, and it goes away with them.Behaviour with nothing supplied is unchanged:
ProcessCommand::new(Path::new("docker"))resolves againstPATHexactly asProcessCommand::new("docker")does, and the argument vectors, error contexts and theBOOTROOT_INSTANCEpinComposeInvocation::commandapplies last are all untouched.Closes #807
Test plan
cargo test --bin bootrootpasses (979 passed, 0 failed), including every test still faking docker throughPATHcargo test --libpasses (398 passed, 0 failed)BOOTROOT_TEST_DOCKER_ARGSandBOOTROOT_TEST_DOCKER_EXITunset — their fakes carry their own log paths — and each fails if the executable is pointed somewhere wrong (the log readexpects the fake ran)InitRollbackwith..Default::default()anddockerleftNone, and the resolved program is stilldocker, so the empty-path trap a barePathBufwould open cannot reopengrep -rn 'Command::new("docker")\|ProcessCommand::new("docker")\|ProcessCommand::new(DOCKER_BIN)' srcreturns onlyinfra.rs'sdocker_output, the twoclean.rshelpers androtate/helpers.rs'stry_restart_containergit diff --name-onlylists no change tosrc/commands/monitoring.rsorsrc/commands/dns_alias.rs;src/commands/rotate/ca.rschanges by exactly three lines (thectx_for_instancefield plus the two imports it compels)grep -rn "set_var\|remove_var" srcreturns 86 lines, exactly as onmain— this change removes none of them, andwrite_fake_docker_script,ScopedEnvVar,path_with_prependandverify.rs'sENV_LOCKare unchangedunsafeblock anywhere in the diffcargo clippy --all-targets -- -D warningsis cleancargo fmt -- --check --config group_imports=StdExternalCrateis cleandocker, so they are the check that the default path is genuinely unchanged (the extended workflow has no PR trigger; dispatched by hand on this branch and green on 073e05d)