feat: Phase 5a — probe adapters under adapters/probes/ + Checker registry
Problem: probe logic (checkHTTP, checkTCP, checkPing, checkDNS, checkSSHScript, etc.) was embedded inside scheduler/scheduler.go as unexported functions coupled to sqlcgen types — unreachable from the core ObservationService the hexagonal refactor needs. Change: - adapters/probes/network.go: HTTP, TCP, ping, and DNS probe adapters implementing ports.Checker. Each parses CheckDef.Config (json map), runs the probe against the Target, and returns a ports.CheckResult. configMap helper unmarshals config JSON; parseStr/parseFloat extract typed values. - adapters/probes/ssh.go: SSHChecker wraps actuator.Dial + RunCombinedOutput with a SignerSource for key resolution. Registry (map[string]ports.Checker) with NewRegistry() pre-populating all known kinds (ssh-script, vm-status, backup-freshness, cert-expiry set to nil — filled by the ObservationService when signers are available). Verification: go build/vet, full test suite (19 pkgs), DB integration (postgres + mcp — green).
This commit is contained in:
105
internal/adapters/probes/ssh.go
Normal file
105
internal/adapters/probes/ssh.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package probes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dtoro/oikos/internal/actuator"
|
||||
"github.com/dtoro/oikos/internal/core/ports"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// SSHChecker runs ssh-script probes via the actuator. Constructed with a
|
||||
// key source so the caller controls SSH key resolution.
|
||||
type SSHChecker struct {
|
||||
pool *actuator.DialPool
|
||||
signerFn func() (ssh.Signer, error)
|
||||
}
|
||||
|
||||
func NewSSHChecker(pool *actuator.DialPool, signerFn func() (ssh.Signer, error)) *SSHChecker {
|
||||
return &SSHChecker{pool: pool, signerFn: signerFn}
|
||||
}
|
||||
|
||||
func (c *SSHChecker) Check(ctx context.Context, def ports.CheckDef, target ports.Target) ports.CheckResult {
|
||||
cfg := configMap(def)
|
||||
script, _ := parseStr(cfg, "script")
|
||||
if script == "" {
|
||||
return ports.CheckResult{State: "unknown", Message: "no script in config"}
|
||||
}
|
||||
|
||||
signer, err := c.signerFn()
|
||||
if err != nil {
|
||||
return ports.CheckResult{State: "unknown", Message: fmt.Sprintf("signer: %v", err)}
|
||||
}
|
||||
|
||||
client, err := actuator.Dial(ctx, actuator.DialOptions{
|
||||
Host: target.Host,
|
||||
User: target.User,
|
||||
Signer: signer,
|
||||
})
|
||||
if err != nil {
|
||||
return ports.CheckResult{State: "unknown", Message: fmt.Sprintf("dial %s: %v", target.Host, err)}
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
cmd := fmt.Sprintf("/opt/oikos/checks/%s", script)
|
||||
if target.Wrap != nil {
|
||||
cmd = target.Wrap(cmd)
|
||||
}
|
||||
|
||||
output, err := actuator.RunCombinedOutput(ctx, client, cmd)
|
||||
if err != nil {
|
||||
return ports.CheckResult{
|
||||
Value: -1,
|
||||
State: "critical",
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
return parseSSHResult(output)
|
||||
}
|
||||
|
||||
func parseSSHResult(output []byte) ports.CheckResult {
|
||||
line := strings.TrimSpace(string(output))
|
||||
var value float64
|
||||
if len(line) > 0 {
|
||||
parts := strings.SplitN(line, " ", 3)
|
||||
switch parts[0] {
|
||||
case "OK":
|
||||
if len(parts) > 1 {
|
||||
fmt.Sscanf(parts[1], "%f", &value)
|
||||
}
|
||||
return ports.CheckResult{State: "ok", Value: value, Message: line}
|
||||
case "WARN":
|
||||
if len(parts) > 1 {
|
||||
fmt.Sscanf(parts[1], "%f", &value)
|
||||
}
|
||||
return ports.CheckResult{State: "warning", Value: value, Message: line}
|
||||
case "CRIT":
|
||||
if len(parts) > 1 {
|
||||
fmt.Sscanf(parts[1], "%f", &value)
|
||||
}
|
||||
return ports.CheckResult{State: "critical", Value: value, Message: line}
|
||||
}
|
||||
}
|
||||
return ports.CheckResult{State: "unknown", Message: "unparseable output"}
|
||||
}
|
||||
|
||||
// Registry maps check kinds to Checker implementations.
|
||||
type Registry map[string]ports.Checker
|
||||
|
||||
func NewRegistry() Registry {
|
||||
return Registry{
|
||||
"http": NewHTTPChecker(),
|
||||
"tcp": NewTCPChecker(),
|
||||
"ping": NewPingChecker(),
|
||||
"dns": NewDNSChecker(),
|
||||
"vm-status": nil,
|
||||
"ssh-script": nil,
|
||||
"backup-freshness": nil,
|
||||
"cert-expiry": nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (r Registry) Get(kind string) ports.Checker { return r[kind] }
|
||||
func (r Registry) Register(kind string, c ports.Checker) { r[kind] = c }
|
||||
Reference in New Issue
Block a user