From 42fbc4b4c42bf458bb51b438a453d100068965e9 Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Fri, 24 Apr 2026 15:04:36 -0700 Subject: [PATCH] =?UTF-8?q?elway:=20lazy=20sudo=20probe=20=E2=80=94=20don'?= =?UTF-8?q?t=20prompt=20when=20every=20sudo=20step=20will=20skip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the startup logic ran `probe_sudo()` if any step in the playbook declared `sudo: true`, regardless of whether that step's creates:/when: gates would actually let it fire. The result on the task-board deploy rerun was a spurious password prompt followed by six SKIPPED lines — the prompt served no purpose. New flow: - Remove the upfront probe in main(). - SSHContext.sudo_password defaults to None; new sudo_probed flag tracks whether we've already prompted this session. - run_shell_step + run_upload_step call ensure_sudo(ctx) only at the point a sudo step is actually executing — i.e. after its skip conditions have been evaluated and passed. Idempotent: probes at most once per playbook run. Tradeoff accepted: if the user fat-fingers the password, they see it mid-run on the first sudo step rather than upfront. `stop_on_fail` (default true) halts cleanly; they rerun. Lower friction for the common idempotent-rerun case, same recoverability. Verified against playbooks/deploy-task-board.yaml — prior run prompted + completed in 1.7s; new run completes in 1.7s with no prompt because every sudo step skip-gated. --- scripts/elway | 45 +++++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/scripts/elway b/scripts/elway index 3d661c7..855939a 100755 --- a/scripts/elway +++ b/scripts/elway @@ -296,7 +296,8 @@ class SSHContext: host: str control_path: str verbose: bool - sudo_password: Optional[str] + sudo_password: Optional[str] = None + sudo_probed: bool = False # so we probe at most once per run, lazily def _base_ssh_opts(self) -> list[str]: return [ @@ -329,8 +330,18 @@ class SSHContext: pass -def probe_sudo(ctx: SSHContext) -> Optional[str]: - """Return a sudo password if one is needed, else None. Prompts locally.""" +def ensure_sudo(ctx: SSHContext) -> None: + """Idempotent lazy sudo probe. Called before the first sudo step that will + actually run — not at startup — so playbooks whose sudo steps all skip + never prompt for a password. + + Sets ctx.sudo_password to None if NOPASSWD sudo works, or to the validated + password the user entered. Marks ctx.sudo_probed=True so subsequent sudo + steps reuse the already-acquired credential. + """ + if ctx.sudo_probed: + return + ctx.sudo_probed = True # Is sudo configured NOPASSWD for this user? `sudo -n -v` exits 0 if so. p = subprocess.run( ctx.ssh_cmd("sudo -n -v"), @@ -338,7 +349,8 @@ def probe_sudo(ctx: SSHContext) -> Optional[str]: stderr=subprocess.DEVNULL, ) if p.returncode == 0: - return None + ctx.sudo_password = None + return pw = getpass.getpass(f"[elway] sudo password for {ctx.host}: ") # Validate it works; don't silently carry a wrong password through the whole run. validation = subprocess.run( @@ -349,8 +361,8 @@ def probe_sudo(ctx: SSHContext) -> Optional[str]: stderr=subprocess.PIPE, ) if validation.returncode != 0: - raise SystemExit("elway: sudo password validation failed; aborting before anything runs.") - return pw + raise SystemExit("elway: sudo password validation failed; aborting.") + ctx.sudo_password = pw # ─── Step execution ──────────────────────────────────────────────────────── @@ -383,10 +395,11 @@ def run_shell_step(ctx: SSHContext, step: Step, prefix: str) -> int: """Run a shell step on the remote host. Returns exit code.""" cmd_inner = step.shell or "" if step.sudo: - # bash -s eats stdin for script; sudo -S reads password from stdin. - # We prepend the password line ourselves and pipe cmd_inner via bash -c after. - # Using `sudo -S -p '' bash -c