From 0838d1d7355712c5c78e4d8186e2d3edc6b8f980 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Fri, 7 Aug 2026 08:45:00 +0100 Subject: [PATCH] feat: models and indexes for the workload registry --- server/cmd/main.go | 4 ++ server/internal/models/workloads.go | 70 +++++++++++++++++++++ server/internal/services/workloadindexes.go | 34 ++++++++++ 3 files changed, 108 insertions(+) create mode 100644 server/internal/models/workloads.go create mode 100644 server/internal/services/workloadindexes.go diff --git a/server/cmd/main.go b/server/cmd/main.go index 92a174a..6c8a9b9 100644 --- a/server/cmd/main.go +++ b/server/cmd/main.go @@ -136,6 +136,10 @@ func runSchemaSetup() { log.Printf("warning: failed to ensure vuln indexes: %v", err) } + if err := services.EnsureWorkloadIndexes(); err != nil { + log.Printf("warning: failed to ensure workload indexes: %v", err) + } + if instanceIDs, err := services.ListInstanceIDs(); err != nil { log.Printf("warning: failed to list instances for default step seeding: %v", err) } else { diff --git a/server/internal/models/workloads.go b/server/internal/models/workloads.go new file mode 100644 index 0000000..60bbe4b --- /dev/null +++ b/server/internal/models/workloads.go @@ -0,0 +1,70 @@ +package models + +import ( + "time" + + "go.mongodb.org/mongo-driver/v2/bson" +) + +// Workload kinds. +const ( + WorkloadContainer = "container" + WorkloadUnit = "unit" +) + +// Control actions. +const ( + WorkloadStart = "start" + WorkloadStop = "stop" + WorkloadRestart = "restart" +) + +// Workload is one container or one systemd unit. +type Workload struct { + Kind string `bson:"kind" json:"kind"` // container | unit + ID string `bson:"id" json:"id"` // container id, or unit name + Name string `bson:"name" json:"name"` + + // State is deliberately NOT collapsed into a shared vocabulary across the + // two kinds. Containers report running/exited/paused/restarting/created; + // units report active/inactive/failed/activating. A failed unit and an + // exited container mean different things, and flattening them loses the + // distinction the operator needs. + State string `bson:"state" json:"state"` + Health string `bson:"health,omitempty" json:"health,omitempty"` + + Image string `bson:"image,omitempty" json:"image,omitempty"` + Stack string `bson:"stack,omitempty" json:"stack,omitempty"` // compose project label + Ports []string `bson:"ports,omitempty" json:"ports,omitempty"` + + Restarts int `bson:"restarts,omitempty" json:"restarts,omitempty"` + StartedAt time.Time `bson:"started_at,omitempty" json:"started_at,omitempty"` + + // Protected is computed agent-side and reported so the UI can render the + // action disabled with a reason rather than offering a button whose refusal + // is already known. The field is the courtesy; the agent's own check is the + // boundary. + Protected bool `bson:"protected" json:"protected"` +} + +// ServerWorkloads holds one server's whole workload list in ONE document. +type ServerWorkloads struct { + ID bson.ObjectID `bson:"_id,omitempty" json:"-"` + InstanceID string `bson:"instance_id" json:"-"` + ServerID string `bson:"server_id" json:"server_id"` + Hash string `bson:"hash" json:"hash"` + Workloads []Workload `bson:"workloads" json:"workloads"` + CollectedAt time.Time `bson:"collected_at" json:"collected_at"` + + // A host with no Docker and a host with Docker running nothing both produce + // an empty list. One should read "not in use here", the other "nothing + // running", and only the second deserves any alarm. + // + // The error strings separate a third case the booleans cannot: installed + // with the daemon down. "Not installed" and "installed but not responding" + // are different problems with different fixes. + DockerOK bool `bson:"docker_ok" json:"docker_ok"` + DockerError string `bson:"docker_error,omitempty" json:"docker_error,omitempty"` + SystemdOK bool `bson:"systemd_ok" json:"systemd_ok"` + SystemdError string `bson:"systemd_error,omitempty" json:"systemd_error,omitempty"` +} diff --git a/server/internal/services/workloadindexes.go b/server/internal/services/workloadindexes.go new file mode 100644 index 0000000..6a5872a --- /dev/null +++ b/server/internal/services/workloadindexes.go @@ -0,0 +1,34 @@ +package services + +import ( + "context" + "log" + + "gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/db" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" +) + +// EnsureWorkloadIndexes declares the indexes for the workload registry. +// +// It warns rather than being fatal, matching EnsureSecretIndexes and +// EnsureVulnIndexes: a missing index degrades these queries to a collection +// scan, which is no reason to refuse to serve the fleet. +func EnsureWorkloadIndexes() error { + ctx := context.Background() + + idx := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "instance_id", Value: 1}, {Key: "server_id", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + // Multikey, for the fleet-wide "which servers run image X" query, which + // is the reason the snapshot is stored rather than fetched and discarded. + {Keys: bson.D{{Key: "instance_id", Value: 1}, {Key: "workloads.image", Value: 1}}}, + } + if _, err := db.Col("server_workloads").Indexes().CreateMany(ctx, idx); err != nil { + log.Printf("warning: server_workloads indexes: %v", err) + } + return nil +}