95 lines
2.6 KiB
Go
95 lines
2.6 KiB
Go
// sense-orphan is a local, PostgreSQL-backed operational command. Report is
|
|
// read-only; apply requires a fresh scan ID, actor and exact confirmation.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"yovision/sense/internal/config"
|
|
"yovision/sense/internal/metrics"
|
|
"yovision/sense/internal/mtx"
|
|
"yovision/sense/internal/orphan"
|
|
"yovision/sense/internal/store"
|
|
)
|
|
|
|
func main() {
|
|
if err := run(); err != nil {
|
|
_, _ = fmt.Fprintln(os.Stderr, "sense-orphan failed:", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func run() error {
|
|
mode := flag.String("mode", "report", "report or apply")
|
|
scanID := flag.String("scan-id", "", "fresh scan ID required by apply")
|
|
actor := flag.String("actor", "", "operator identifier required by apply")
|
|
confirm := flag.String("confirm", "", "exact confirmation: DELETE <scan-id>")
|
|
flag.Parse()
|
|
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !strings.EqualFold(strings.TrimSpace(cfg.DatabaseDriver), store.DriverPostgres) {
|
|
return errors.New("sense-orphan requires SENSE_DB_DRIVER=postgres")
|
|
}
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
repository, err := store.OpenPostgres(ctx, cfg.DatabaseDSN)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer repository.Close()
|
|
media, err := mtx.NewClient(cfg.MediaMTXURL, &http.Client{Timeout: cfg.ReconcileOperationTimeout})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
instanceID := cfg.InstanceID
|
|
if instanceID == "" {
|
|
instanceID, err = metrics.GenerateInstanceID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
manager := orphan.New(repository, media, instanceID, metrics.New(instanceID, "operator"))
|
|
encoder := json.NewEncoder(os.Stdout)
|
|
switch *mode {
|
|
case "report":
|
|
scan, err := manager.Report(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return encoder.Encode(struct {
|
|
ScanID string `json:"scan_id"`
|
|
Observed int `json:"observed"`
|
|
OwnedStale int `json:"owned_stale"`
|
|
Unowned int `json:"unowned"`
|
|
SafetyAllowed bool `json:"safety_allowed"`
|
|
SafetyReason string `json:"safety_reason"`
|
|
ExpiresAt string `json:"expires_at"`
|
|
}{
|
|
ScanID: scan.ID, Observed: scan.ObservedCount,
|
|
OwnedStale: scan.OwnedStaleCount, Unowned: scan.UnownedCount,
|
|
SafetyAllowed: scan.SafetyAllowed, SafetyReason: scan.SafetyReason,
|
|
ExpiresAt: scan.ExpiresAt.UTC().Format("2006-01-02T15:04:05Z07:00"),
|
|
})
|
|
case "apply":
|
|
result, err := manager.Apply(ctx, *scanID, *actor, *confirm)
|
|
if encodeErr := encoder.Encode(result); encodeErr != nil {
|
|
return encodeErr
|
|
}
|
|
return err
|
|
default:
|
|
return errors.New("mode must be report or apply")
|
|
}
|
|
}
|