531b36924c
- job stop: real systemctl stop via SSH (was DB-only soft stop) resolves node from alloc_history or --peer flag - doctor db-retention: row count check for jobs/tasks/audit_log warns at 100k rows, suggests backup + cleanup - logs --lines: cap at 50000 (default 1000); --since upper bound 7d prevents OOM from unbounded journalctl - cache DB mode 0600 (was 0644; matches store.Open) - upgrade cutover: backup file + atomic rename (was sed -i) rollback restores from backup on failure Tests: job stop SSH, DB retention warning, logs lines cap, cache mode, cutover backup-restore + atomic rename. ---ci--- project: orca phase: 9 milestone: v0.13 status: complete requirements: covered: [158] ---/ci---
291 lines
7.7 KiB
Go
291 lines
7.7 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.cloudinit.dev/coreci/orca/internal/certpaths"
|
|
"git.cloudinit.dev/coreci/orca/internal/store"
|
|
)
|
|
|
|
func TestDoctorText(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
if err := runInit(discardWriter{}); err != nil {
|
|
t.Fatalf("init: %v", err)
|
|
}
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"doctor"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("doctor: %v", err)
|
|
}
|
|
out := buf.String()
|
|
for _, want := range []string{"CA", "cert", "PASS", "WARN", "FAIL"} {
|
|
_ = want
|
|
}
|
|
if !strings.Contains(out, "CA") {
|
|
t.Errorf("doctor output missing CA check: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestDoctorJSON(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
if err := runInit(discardWriter{}); err != nil {
|
|
t.Fatalf("init: %v", err)
|
|
}
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"doctor", "--json"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("doctor --json: %v", err)
|
|
}
|
|
var checks []map[string]any
|
|
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &checks); err != nil {
|
|
t.Fatalf("unmarshal doctor json: %v\n%s", err, buf.String())
|
|
}
|
|
if len(checks) == 0 {
|
|
t.Errorf("doctor --json returned no checks: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestDoctorCertSubcommand(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
if err := runInit(discardWriter{}); err != nil {
|
|
t.Fatalf("init: %v", err)
|
|
}
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"doctor", "cert"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("doctor cert: %v", err)
|
|
}
|
|
out := buf.String()
|
|
if !strings.Contains(out, "CA") {
|
|
t.Errorf("doctor cert output missing CA: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestDoctorCertJSON(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
if err := runInit(discardWriter{}); err != nil {
|
|
t.Fatalf("init: %v", err)
|
|
}
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"doctor", "cert", "--json"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("doctor cert --json: %v", err)
|
|
}
|
|
var results []map[string]any
|
|
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &results); err != nil {
|
|
t.Fatalf("unmarshal doctor cert json: %v\n%s", err, buf.String())
|
|
}
|
|
if len(results) == 0 {
|
|
t.Errorf("doctor cert --json returned no results: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestDoctorDBSubcommand(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
if err := runInit(discardWriter{}); err != nil {
|
|
t.Fatalf("init: %v", err)
|
|
}
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"doctor", "db"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("doctor db: %v", err)
|
|
}
|
|
out := buf.String()
|
|
if !strings.Contains(out, "db") {
|
|
t.Errorf("doctor db output unexpected: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestDoctorOSSubcommand(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
if err := runInit(discardWriter{}); err != nil {
|
|
t.Fatalf("init: %v", err)
|
|
}
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"doctor", "os"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("doctor os: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDoctorOSJSON(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
if err := runInit(discardWriter{}); err != nil {
|
|
t.Fatalf("init: %v", err)
|
|
}
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"doctor", "os", "--json"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("doctor os --json: %v", err)
|
|
}
|
|
var result map[string]any
|
|
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &result); err != nil {
|
|
t.Fatalf("unmarshal doctor os json: %v\n%s", err, buf.String())
|
|
}
|
|
if result["Name"] == nil {
|
|
t.Errorf("doctor os --json missing Name: %v", result)
|
|
}
|
|
}
|
|
|
|
func TestDoctorNetworkSubcommand(t *testing.T) {
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"doctor", "network"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("doctor network: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDoctorProxmoxSubcommand(t *testing.T) {
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"doctor", "proxmox"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("doctor proxmox: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDoctorProxmoxJSON(t *testing.T) {
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"doctor", "proxmox", "--json"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("doctor proxmox --json: %v", err)
|
|
}
|
|
var result map[string]any
|
|
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &result); err != nil {
|
|
t.Fatalf("unmarshal doctor proxmox json: %v\n%s", err, buf.String())
|
|
}
|
|
if result["Name"] == nil {
|
|
t.Errorf("doctor proxmox --json missing Name: %v", result)
|
|
}
|
|
}
|
|
|
|
|
|
// TestDoctorDBRetention verifies that `orca doctor db-retention` counts
|
|
// rows in jobs, tasks, and audit_log and warns when a table exceeds
|
|
// 100k rows (REQ-158, P09 T7).
|
|
func TestDoctorDBRetention(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
if err := runInit(discardWriter{}); err != nil {
|
|
t.Fatalf("init: %v", err)
|
|
}
|
|
|
|
// Insert 100001 rows into the audit_log table to trigger the warning.
|
|
// Use a multi-row VALUES insert in batches for speed.
|
|
db, err := store.Open(certpaths.DBPath())
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
defer db.Close()
|
|
ctx := context.Background()
|
|
// Build a batch insert: 500 rows per INSERT in a transaction.
|
|
// SQLite handles this much faster than 100k individual inserts.
|
|
const totalRows = 100001
|
|
const batchSize = 500
|
|
inserted := 0
|
|
for inserted < totalRows {
|
|
remaining := totalRows - inserted
|
|
batch := batchSize
|
|
if remaining < batch {
|
|
batch = remaining
|
|
}
|
|
var placeholders strings.Builder
|
|
var args []any
|
|
for j := 0; j < batch; j++ {
|
|
if j > 0 {
|
|
placeholders.WriteString(",")
|
|
}
|
|
placeholders.WriteString("(?, 'test', 'test.action', 'test-resource', 'success')")
|
|
args = append(args, time.Now().UTC())
|
|
}
|
|
q := "INSERT INTO audit_log (timestamp, actor, action, resource, result) VALUES " + placeholders.String()
|
|
if _, err := db.ExecContext(ctx, q, args...); err != nil {
|
|
t.Fatalf("batch insert at offset %d: %v", inserted, err)
|
|
}
|
|
inserted += batch
|
|
}
|
|
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"doctor", "db-retention"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("doctor db-retention: %v", err)
|
|
}
|
|
out := buf.String()
|
|
if !strings.Contains(out, "audit_log") {
|
|
t.Errorf("output missing audit_log table: %s", out)
|
|
}
|
|
if !strings.Contains(out, "WARN") {
|
|
t.Errorf("output should contain WARN for audit_log exceeding threshold: %s", out)
|
|
}
|
|
if !strings.Contains(out, "backup") {
|
|
t.Errorf("output should suggest 'orca backup': %s", out)
|
|
}
|
|
}
|
|
|
|
// TestDoctorDBRetentionNoWarn verifies that with a small DB no warning
|
|
// is emitted (REQ-158, P09 T7).
|
|
func TestDoctorDBRetentionNoWarn(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
if err := runInit(discardWriter{}); err != nil {
|
|
t.Fatalf("init: %v", err)
|
|
}
|
|
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"doctor", "db-retention"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("doctor db-retention: %v", err)
|
|
}
|
|
out := buf.String()
|
|
if strings.Contains(out, "WARN") {
|
|
t.Errorf("output should NOT contain WARN for small DB: %s", out)
|
|
}
|
|
}
|