Fail init's prompts on EOF instead of answering them (#817) - #820
Conversation
`init`'s prompt reader discarded the count `read_line` returns, so the terminating EOF was indistinguishable from a blank line and every prompt past the end of a piped answer sequence answered itself. The EAB credential prompt, the only one that retries, re-prompted forever — gigabytes of validation errors until the process was killed. The rest returned an empty string, which silently skipped EAB registration, declined to save freshly generated unseal keys, or accepted a default the operator never chose. The count is now checked and a zero-length read bails with `error_prompt_eof`. A blank line keeps its meaning everywhere, so the three readers gain reader-backed helpers an in-memory cursor can drive and the public signatures stay as they were. The EAB loop takes the reader as a parameter, which is what makes the unbounded loop itself testable rather than only the primitive under it. The save-unseal-keys prompt runs outside the rollback envelope with the keys held nowhere else, so an unanswerable prompt there emits the cleartext echo before failing — the same code path the declined branch uses, extracted so both cannot drift apart. Two preflight scripts were relying on EOF as an answer and drove the EAB loop into exactly that spin; both now supply one answer per prompt the run reaches. Closes #817
The prompts change is only half the story for anyone driving `init` over a pipe: the manual still showed a piped answer sequence shorter than the prompts the run reaches, and said nothing about what a run does when it runs out of input. Document both, including the one prompt that still echoes the unseal keys before it fails. The save-unseal-keys echo had no test that ran it. The unit test covers the shared formatting helper, but nothing proved `init` itself reaches that prompt, echoes on EOF, and then exits nonzero — the property that keeps a partial init from leaving freshly generated keys recorded nowhere. Stub an OpenBao that `init` has to initialize itself, which is the only state that reaches the prompt at all, and drive a real `init` into it with empty stdin. A half-answered EAB pair gets its own case too: that is the shape a piped sequence actually runs out in, and reading the `kid` before the `hmac` goes missing is how the old loop started spinning. Part of #817
|
[Reviewer Round 1]
Aside from that, the reader-backed helpers preserve the blank-line behavior, the EAB loop now propagates EOF, and the unseal-key fallback is exercised end to end through the shared echo path. |
|
[Review Verdict Round 1: NOT_APPROVED] |
A prefix assignment binds to one command, and in a pipeline that is the command it precedes -- the left-hand printf, which never reads the variable. The init runs downstream of those pipes were therefore taking whatever locale the caller happened to export, while the assertions on their output (the root-token awk, the cleartext unseal-key grep) and the EOF message quoted in the manual all assume English. Nothing fails today because en is also the default, so the mistake is invisible until someone runs the preflight with BOOTROOT_LANG=ko set. Move the assignment to the right of the pipe, which is where the tree's other piped init already puts it. Part of #817
|
[Author Round 1]
Not changed, and flagged rather than folded in. The same misplacement predates this branch in four files it does not otherwise touch: Verification: |
|
[Reviewer Round 2] Round 1 P2 is resolved: each rewritten pipeline now puts No unresolved items or new findings. [Review Verdict Round 2: APPROVED] |
|
[Review Verdict Round 2: APPROVED] |
Suggested squash commitTitle Body |
Summary
init's prompt reader discarded the byte countread_linereturns, so the terminating EOF was indistinguishable from a blank line and every prompt past the end of a piped answer sequence answered itself. The EAB credential prompt — the only one that retries — re-prompted forever (gigabytes of validation errors until the process was killed), and the rest returned an empty string, which silently skipped EAB registration, declined to save freshly generated unseal keys, or accepted a default nobody chose.src/commands/init/steps/prompts.rsnow checks the count and bails with the existingerror_prompt_eofmessage on a zero-length read. A blank line keeps its meaning everywhere: the empty answer, the offered default, orn.prompt_text,prompt_text_with_default,prompt_yes_no) gained reader-backed helpers an in-memoryCursorcan drive. Their names, parameter lists and visibility are unchanged, so every existing call site — includingcleanandreinit— compiles untouched.prompt_eab_with_validationtakes the reader as a parameter, so the unbounded loop itself is under test rather than only the primitive beneath it.maybe_register_eabpasses the stdin lock as a temporary, keeping the futureSendacross theregister_eab_secretawait.initinvocations intests/openbao_stepca_integration.rswere extended the same way, andrun_command_with_inputsays so at its definition.docs/en/cli.md,docs/ko/cli.md) and the E2E walkthrough now describe the new behaviour, and the walkthrough's sample sequence was corrected.initline this branch touches now carriesBOOTROOT_LANG=enon the right of the pipe. A prefix assignment binds to the command it precedes, so on the left it reached onlyprintfand the run itself took the caller's locale — while the assertions parsing that run's output, and the EOF message quoted in the walkthrough, all assume English.Closes #817
Test plan
prompt_texterrors witherror_prompt_eof()whenread_linereturns0"\n"yields"", the default, andfalserespectively — blank line and EOF stay distinct"value\n"yields"value"trimmed;"y\n"yieldstrueprompt_eab_with_validationwith an empty reader returns the EOF error instead of looping — noOpenBaoClient, no async runtime, no networkkid/hmacpair errors rather than retryinginitagainst an uninitializedOpenBaostub with empty stdin exits nonzero, names EOF on stderr, and still prints every generated unseal keycleanandreinitcall sites compile untouchedcargo testpassescargo clippy --all-targets -- -D warningscleancargo fmt -- --check --config group_imports=StdExternalCratecleanscripts/preflight/ci/check.sh,validate-deploy-compose.sh,validate-compose-instance-names.shandextra/deploy-no-build-smoke.shpassscripts/preflight/ci/test-core.sh: both rewritten piped-answerinitruns pass, including thePASS: unseal keys displayed in cleartext after declining saveassertion, which now exercises the declined path deliberately rather than the EOF pathscripts/preflight/extra/cli-scenarios.shpasses end to end; itsy\nn\nn\nlines up one-to-one with the db-provision, EAB-registration and save-unseal-keys promptsBOOTROOT_LANGplacement verified against the built binary:printf "" | BOOTROOT_LANG=ko bootroot cleanprints the Korean EOF message,BOOTROOT_LANG=ko printf "" | bootroot cleanprints the English onescripts/preflight/run-all.shend to end — not reproducible on the macOS Docker host used here, for reasons unrelated to this change and identical onmain:test-core.shcannotdocker exec ... >> /etc/hostsinto the unprivileged step-ca container,e2e-matrix.shneedssudohost-file edits plus the Linuxdocker0gateway, ande2e-extended.shfails the same 3 of 7 cases onmainas on this branch (runner-timer/runner-cronreject the host'sAcos-MacBook-Pro.localas a DNS label;reinit-recoverycannot bind172.17.0.1). No artefact from any of these runs contains the EOF error. The Linux equivalents are green in CI:Unit & CLI Smokecoverstest-core.shand all tenDocker E2Ejobs cover the matrix.e2e-extended.ymlisworkflow_dispatch/scheduleonly, so it does not run on pull requests.