0.29.1 — review-fix round on E3: RunOutput, sshKeyPath fallback, RunStreaming consolidation, signer cache, stderr in errors
This commit is contained in:
@@ -7,11 +7,11 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dtoro/oikos/internal/actuator"
|
||||
"github.com/dtoro/oikos/internal/config"
|
||||
"github.com/dtoro/oikos/internal/db"
|
||||
"github.com/dtoro/oikos/internal/db/sqlcgen"
|
||||
@@ -981,28 +982,46 @@ func allowlistedScript(name string) bool {
|
||||
return scriptNameRe.MatchString(name)
|
||||
}
|
||||
|
||||
// sshExec runs a command on a remote host over crypto/ssh via the shared
|
||||
// actuator primitives. It replaced a fork of `os/exec ssh` so the scheduler,
|
||||
// the MCP execution path, and the actuator share one dial/run/host-key
|
||||
// implementation (plan E3). The host key is verified through the centralized
|
||||
// actuator.HostKeyCallback seam. ctx bounds the running command; timeout
|
||||
// bounds the dial.
|
||||
func sshExec(ctx context.Context, host, port, user, cmd string, timeout time.Duration) ([]byte, error) {
|
||||
args := []string{
|
||||
"-o", "ConnectTimeout=" + strconv.Itoa(int(timeout.Seconds())),
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "BatchMode=yes",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "LogLevel=ERROR",
|
||||
}
|
||||
if sshKeyPath != "" {
|
||||
args = append(args, "-i", sshKeyPath)
|
||||
}
|
||||
if port != "" && port != "22" {
|
||||
args = append(args, "-p", port)
|
||||
}
|
||||
args = append(args, "-l", user, host, cmd)
|
||||
c := exec.CommandContext(ctx, "ssh", args...)
|
||||
out, err := c.Output()
|
||||
if err != nil {
|
||||
var ee *exec.ExitError
|
||||
if errors.As(err, &ee) {
|
||||
return nil, fmt.Errorf("ssh %s: %v (stderr: %s)", host, err, string(ee.Stderr))
|
||||
keyPath := sshKeyPath
|
||||
if keyPath == "" {
|
||||
// Preserve the old os/exec-ssh behavior of deferring to a default
|
||||
// key when no explicit OIKOS_SSH_KEY_PATH is configured: the system
|
||||
// ssh binary used the agent / ~/.ssh; crypto/ssh has no agent wiring,
|
||||
// so fall back to SSH_KEY_PATH then ~/.ssh/id_rsa.
|
||||
keyPath = os.Getenv("SSH_KEY_PATH")
|
||||
if keyPath == "" {
|
||||
keyPath = os.Getenv("HOME") + "/.ssh/id_rsa"
|
||||
}
|
||||
}
|
||||
signer, err := actuator.LoadSigner(keyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ssh %s: %v", host, err)
|
||||
}
|
||||
p := 22
|
||||
if port != "" {
|
||||
if n, parseErr := strconv.Atoi(port); parseErr == nil && n > 0 {
|
||||
p = n
|
||||
}
|
||||
}
|
||||
client, err := actuator.Dial(ctx, actuator.DialOptions{
|
||||
Host: host, Port: p, User: user, Signer: signer, Timeout: timeout,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ssh %s: %v", host, err)
|
||||
}
|
||||
defer client.Close()
|
||||
// RunOutput (stdout-only) — the scheduler parses check output as JSON or
|
||||
// matches it literally, so stderr must not be merged in (RunCombinedOutput
|
||||
// is for the live-run display path in mcp/httpapi).
|
||||
out, err := actuator.RunOutput(ctx, client, cmd)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ssh %s: %v", host, err)
|
||||
}
|
||||
return out, nil
|
||||
|
||||
Reference in New Issue
Block a user