From cb39df63bcb976c7ed5633f16eba7cbad14030a4 Mon Sep 17 00:00:00 2001 From: Daniel Arroyo Date: Thu, 9 Jul 2026 20:45:32 -0400 Subject: [PATCH] fix: deploy keys StdinPipe order and add structured logging - sshmanager/deploy.go: StdinPipe() must be called BEFORE Start(), not after. Reordered the calls to fix "ssh: StdinPipe after process started" error. - Return DeployResult instead of nil error on SSH dial failure so the frontend always gets a parseable response. - Add slog.Debug for key upload, slog.Warn for ssh-keyscan and authorized_keys failures, slog.Info for final result summary. --- internal/sshmanager/deploy.go | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/internal/sshmanager/deploy.go b/internal/sshmanager/deploy.go index 396e628..252d69e 100644 --- a/internal/sshmanager/deploy.go +++ b/internal/sshmanager/deploy.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "log/slog" "net" "os" "path/filepath" @@ -55,7 +56,8 @@ func DeployKeysToMachine(ctx context.Context, serverKeyPath, serverPubKey string conn, err := ssh.Dial("tcp", addr, cfg) if err != nil { - return nil, fmt.Errorf("connecting to %s: %w", addr, err) + slog.Warn("deploy keys: SSH dial failed", "host", addr, "error", err) + return &DeployResult{Success: false, Messages: []string{}, Errors: []string{fmt.Sprintf("connecting to %s: %v", addr, err)}}, nil } defer conn.Close() @@ -101,17 +103,22 @@ func DeployKeysToMachine(ctx context.Context, serverKeyPath, serverPubKey string sess2.Stdout = &stdout sess2.Stderr = &stderr + slog.Debug("deploy: uploading key", "local", k.LocalPath, "remote", k.RemotePath, "host", host) + cmd := fmt.Sprintf("cat > %s && chmod 0%o %s", k.RemotePath, mode, k.RemotePath) - if err := sess2.Start(cmd); err != nil { - result.Errors = append(result.Errors, fmt.Sprintf("starting command for %s: %v", k.RemotePath, err)) - result.Success = false - continue - } stdin, err := sess2.StdinPipe() if err != nil { result.Errors = append(result.Errors, fmt.Sprintf("stdin pipe for %s: %v", k.RemotePath, err)) result.Success = false + sess2.Close() + continue + } + + if err := sess2.Start(cmd); err != nil { + result.Errors = append(result.Errors, fmt.Sprintf("starting command for %s: %v", k.RemotePath, err)) + result.Success = false + sess2.Close() continue } @@ -119,6 +126,8 @@ func DeployKeysToMachine(ctx context.Context, serverKeyPath, serverPubKey string if err != nil { result.Errors = append(result.Errors, fmt.Sprintf("writing key %s: %v", k.RemotePath, err)) result.Success = false + stdin.Close() + sess2.Close() continue } stdin.Close() @@ -143,6 +152,7 @@ func DeployKeysToMachine(ctx context.Context, serverKeyPath, serverPubKey string err := session2.Run(fmt.Sprintf("ssh-keyscan -H -p %d %s 2>/dev/null >> %s/known_hosts", port, knownHostsHost, remoteSSHDir)) session2.Close() if err != nil { + slog.Warn("deploy: ssh-keyscan failed", "host", knownHostsHost, "port", port, "error", err) result.Errors = append(result.Errors, fmt.Sprintf("ssh-keyscan %s: %v (stderr: %s)", knownHostsHost, err, stderr.String())) result.Success = false } else { @@ -151,7 +161,7 @@ func DeployKeysToMachine(ctx context.Context, serverKeyPath, serverPubKey string } } - if addServerPubKey && serverPubKey != "" { + if addServerPubKey && serverPubKey != "" { session3, err := conn.NewSession() if err != nil { result.Errors = append(result.Errors, fmt.Sprintf("session for authorized_keys: %v", err)) @@ -163,6 +173,7 @@ func DeployKeysToMachine(ctx context.Context, serverKeyPath, serverPubKey string err := session3.Run(fmt.Sprintf("mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo '%s' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys", pubKeyClean)) session3.Close() if err != nil { + slog.Warn("deploy: adding to authorized_keys failed", "host", host, "error", err) result.Errors = append(result.Errors, fmt.Sprintf("adding to authorized_keys: %v (stderr: %s)", err, stderr.String())) result.Success = false } else { @@ -171,5 +182,6 @@ func DeployKeysToMachine(ctx context.Context, serverKeyPath, serverPubKey string } } + slog.Info("deploy keys result", "host", host, "success", result.Success, "messages", len(result.Messages), "errors", len(result.Errors)) return result, nil }