Files
oikos/internal/actuator/client.go
dtoro 75c0848a6f
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
0.29.0 — code-quality refactor (plan E1–E5): file splits, sqlc migration, SSH unification, test coverage
E1: split monolithic files — cmd/nomos (main.go → server.go + mcp.go + workers.go),
    internal/mcp/tools.go → entity_tools/ops_tools/knowledge_tools/analysis_tools,
    internal/httpapi/impl.go → domain files (entities, events, signals, ontology,
    fleet_health, client_context, client_lifecycle, entity_mutations, query_audit).
E2: migrate raw pool.Exec queries to sqlc (entities/relationships queries + generated).
E3: unify SSH — consolidate crypto/ssh dial into actuator/client.go (+client_test).
E4/E5: add tests — db/lifecycle, checkdefaults/build, ontology/preconditions, policy/risk.
2026-08-08 22:47:06 +02:00

102 lines
2.7 KiB
Go

package actuator
import (
"context"
"fmt"
"net"
"os"
"time"
"golang.org/x/crypto/ssh"
)
// defaultDialTimeout bounds an SSH dial when the caller leaves Timeout unset.
// 10s matches the previous hardcoded value at every dial site.
const defaultDialTimeout = 10 * time.Second
// LoadSigner reads and parses the private key at keyPath.
func LoadSigner(keyPath string) (ssh.Signer, error) {
key, err := os.ReadFile(keyPath)
if err != nil {
return nil, fmt.Errorf("read ssh key: %w", err)
}
return LoadSignerFromBytes(key)
}
// LoadSignerFromBytes parses an in-memory private key into an ssh.Signer.
func LoadSignerFromBytes(key []byte) (ssh.Signer, error) {
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
return nil, fmt.Errorf("parse ssh key: %w", err)
}
return signer, nil
}
// DialOptions configures an SSH dial.
type DialOptions struct {
Host string
Port int // 0 means 22
User string
Signer ssh.Signer
Timeout time.Duration // dial timeout; <=0 means defaultDialTimeout
}
// Dial opens a crypto/ssh connection through the centralized HostKeyCallback.
// The connection itself is bounded by Timeout; ctx is respected by callers
// via RunCombinedOutput once the session is running.
func Dial(ctx context.Context, opts DialOptions) (*ssh.Client, error) {
port := opts.Port
if port <= 0 {
port = 22
}
timeout := opts.Timeout
if timeout <= 0 {
timeout = defaultDialTimeout
}
cfg := &ssh.ClientConfig{
User: opts.User,
Auth: []ssh.AuthMethod{ssh.PublicKeys(opts.Signer)},
HostKeyCallback: HostKeyCallback(),
Timeout: timeout,
}
addr := net.JoinHostPort(opts.Host, fmt.Sprintf("%d", port))
client, err := ssh.Dial("tcp", addr, cfg)
if err != nil {
return nil, fmt.Errorf("ssh dial %s:%d: %w", opts.Host, port, err)
}
return client, nil
}
// RunCombinedOutput runs cmd on an established client and returns its combined
// stdout/stderr. Context cancellation closes the session to abort the remote
// command instead of blocking until it finishes — the same goroutine+select
// pattern the actuator, mcp, and scheduler each reimplemented before.
func RunCombinedOutput(ctx context.Context, client *ssh.Client, cmd string) ([]byte, error) {
session, err := client.NewSession()
if err != nil {
return nil, fmt.Errorf("create session: %w", err)
}
defer session.Close()
type result struct {
out []byte
err error
}
ch := make(chan result, 1)
go func() {
out, err := session.CombinedOutput(cmd)
ch <- result{out: out, err: err}
}()
select {
case <-ctx.Done():
session.Close()
return nil, ctx.Err()
case res := <-ch:
if res.err != nil {
return res.out, fmt.Errorf("command: %w", res.err)
}
return res.out, nil
}
}