From 291b45565b3a61ccec00fe6384cf07a7288790af Mon Sep 17 00:00:00 2001 From: dtoro Date: Wed, 8 Jul 2026 21:36:05 +0200 Subject: [PATCH] fix: metric samples timestamp + ssh-script port/user handling - InsertMetricSample now includes ts=now() (TimescaleDB hypertable requires it) - ssh-script: pass host and port separately (ssh uses -p flag, not host:port) - ssh-script: use OIKOS_SSH_USER from config/env, default root - Add -o LogLevel=ERROR to suppress SSH warnings polluting JSON output - Use Output() (stdout only) instead of CombinedOutput() - Set OIKOS_SSH_USER=root in scheduler docker-compose service --- docker-compose.yml | 1 + internal/db/queries/operations.sql | 4 ++-- internal/db/sqlcgen/operations.sql.go | 4 ++-- internal/scheduler/scheduler.go | 25 +++++++++++++++++++------ 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 9f8e868..91f057e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -84,6 +84,7 @@ services: OIKOS_DEBUG: "true" OIKOS_SCHEDULER_INTERVAL: "30s" OIKOS_SSH_KEY_PATH: /etc/oikos/ssh_key + OIKOS_SSH_USER: root volumes: - ${OIKOS_SSH_KEY_PATH:-~/.ssh/id_ed25519}:/etc/oikos/ssh_key:ro cap_add: diff --git a/internal/db/queries/operations.sql b/internal/db/queries/operations.sql index dc2b696..e5a4dcc 100644 --- a/internal/db/queries/operations.sql +++ b/internal/db/queries/operations.sql @@ -240,8 +240,8 @@ SELECT * FROM risk_classes ORDER BY name; SELECT * FROM approval_rules ORDER BY entity_type, action; -- name: InsertMetricSample :exec -INSERT INTO metric_samples (entity_id, metric, value, tags) -VALUES ($1, $2, $3, $4); +INSERT INTO metric_samples (entity_id, metric, value, tags, ts) +VALUES ($1, $2, $3, $4, now()); -- name: QueryMetrics :many SELECT time_bucket(sqlc.arg('bucket_interval')::interval, ts) AS bucket, diff --git a/internal/db/sqlcgen/operations.sql.go b/internal/db/sqlcgen/operations.sql.go index 0056ec1..ee11b89 100644 --- a/internal/db/sqlcgen/operations.sql.go +++ b/internal/db/sqlcgen/operations.sql.go @@ -560,8 +560,8 @@ func (q *Queries) InsertFeedback(ctx context.Context, arg InsertFeedbackParams) } const insertMetricSample = `-- name: InsertMetricSample :exec -INSERT INTO metric_samples (entity_id, metric, value, tags) -VALUES ($1, $2, $3, $4) +INSERT INTO metric_samples (entity_id, metric, value, tags, ts) +VALUES ($1, $2, $3, $4, now()) ` type InsertMetricSampleParams struct { diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go index 05757b1..bbe970b 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -7,6 +7,7 @@ import ( "context" "crypto/tls" "encoding/json" + "errors" "fmt" "log/slog" "net" @@ -567,13 +568,13 @@ func checkSSHScript(ctx context.Context, cd sqlcgen.ListEnabledCheckDefsRow) che defer cancel() scriptPath := "/opt/oikos/checks/" + cfg.Script - addr := net.JoinHostPort(cfg.Host, strconv.Itoa(cfg.Port)) + port := strconv.Itoa(cfg.Port) - output, err := sshExec(ctx, addr, cfg.User, scriptPath, timeout) + output, err := sshExec(ctx, cfg.Host, port, cfg.User, scriptPath, timeout) if err != nil { return checkResult{ health: "down", signalKind: "ssh-script", - evidence: fmt.Sprintf("ssh %s %s: %v", addr, cfg.Script, err), + evidence: fmt.Sprintf("ssh %s:%s %s: %v", cfg.Host, strconv.Itoa(cfg.Port), cfg.Script, err), err: err, } } @@ -617,19 +618,31 @@ func allowlistedScript(name string) bool { return scriptNameRe.MatchString(name) } -func sshExec(ctx context.Context, addr, user, cmd string, timeout time.Duration) ([]byte, error) { +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) } - args = append(args, "-l", user, addr, cmd) + if port != "" && port != "22" { + args = append(args, "-p", port) + } + args = append(args, "-l", user, host, cmd) c := exec.CommandContext(ctx, "ssh", args...) - return c.Output() + 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)) + } + return nil, fmt.Errorf("ssh %s: %v", host, err) + } + return out, nil }