feat: patch models, scoped collections and indexes; pin vantage-shared v0.5.0
This commit is contained in:
@@ -179,6 +179,10 @@ func runSchemaSetup() {
|
||||
log.Printf("warning: failed to ensure status page indexes: %v", err)
|
||||
}
|
||||
|
||||
if err := services.EnsurePatchIndexes(); err != nil {
|
||||
log.Printf("warning: patch indexes: %v", err)
|
||||
}
|
||||
|
||||
if err := services.EnsureAuditIndexes(); err != nil {
|
||||
log.Printf("warning: failed to ensure audit indexes: %v", err)
|
||||
}
|
||||
|
||||
+2
-1
@@ -38,6 +38,7 @@ require (
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/stretchr/testify v1.12.1 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
github.com/yuin/goldmark v1.8.6 // indirect
|
||||
go.etcd.io/bbolt v1.5.0 // indirect
|
||||
go.opentelemetry.io/otel v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.46.0 // indirect
|
||||
@@ -46,7 +47,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
gitea.hostxtra.co.uk/vantage/vantage-shared v0.3.3
|
||||
gitea.hostxtra.co.uk/vantage/vantage-shared v0.5.0
|
||||
github.com/bytedance/sonic v1.15.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
|
||||
+4
-2
@@ -1,5 +1,5 @@
|
||||
gitea.hostxtra.co.uk/vantage/vantage-shared v0.3.3 h1:+ak67Hj1C92hNfTE63nlF1c2wx4L8mML+1aI6VJnukc=
|
||||
gitea.hostxtra.co.uk/vantage/vantage-shared v0.3.3/go.mod h1:dWjeOFLltQ8sv9Pnn1xRxGfWGgqa2fkG0esuaJLoPXQ=
|
||||
gitea.hostxtra.co.uk/vantage/vantage-shared v0.5.0 h1:xwSIEkQKTd4Qk+BYHvoGN+h84Isr2h5qqnitUWF1m2w=
|
||||
gitea.hostxtra.co.uk/vantage/vantage-shared v0.5.0/go.mod h1:Zo66XhqF8No3dveIowLCepvMxVg8KnhsNMz0k0Xpuck=
|
||||
github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986 h1:2a30xLN2sUZcMXl50hg+PJCIDdJgIvIbVcKqLJ/ZrtM=
|
||||
github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986/go.mod h1:NT+jyeCzXk6vXR5MTkdn4z64TgGfE5HMLC8qfj5unl8=
|
||||
github.com/aquasecurity/trivy-db v0.0.0-20260813095258-0e0340a01b57 h1:A3Lz/9ip/qigafSxqBWcu7S8i+tJbQS7DB2V0XibOKs=
|
||||
@@ -152,6 +152,8 @@ github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT0
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4=
|
||||
github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
const (
|
||||
PatchScopeAll = "all"
|
||||
PatchScopeSecurity = "security"
|
||||
|
||||
PatchRebootNever = "never"
|
||||
PatchRebootIfRequired = "if_required"
|
||||
|
||||
PatchRunRunning = "running"
|
||||
PatchRunSucceeded = "succeeded"
|
||||
PatchRunPartial = "partial"
|
||||
PatchRunFailed = "failed"
|
||||
PatchRunCancelled = "cancelled"
|
||||
|
||||
PatchSrvQueued = "queued"
|
||||
PatchSrvWaitingOffline = "waiting_offline"
|
||||
PatchSrvPatching = "patching"
|
||||
PatchSrvRebooting = "rebooting"
|
||||
PatchSrvSucceeded = "succeeded"
|
||||
PatchSrvFailed = "failed"
|
||||
PatchSrvUnsupported = "unsupported"
|
||||
PatchSrvAgentTooOld = "agent_too_old"
|
||||
PatchSrvMissedOffline = "missed_offline"
|
||||
PatchSrvWindowClosed = "window_closed"
|
||||
PatchSrvCancelled = "cancelled"
|
||||
|
||||
PatchSourceSchedule = "schedule"
|
||||
PatchSourceRunNow = "run_now"
|
||||
PatchSourceServer = "server"
|
||||
PatchSourceVulnerabilities = "vulnerabilities"
|
||||
PatchSourceMCP = "mcp"
|
||||
)
|
||||
|
||||
// MaintenanceWindow answers "when" and nothing else. Policies reference it by
|
||||
// ID, so one window can later serve alert muting and status page maintenance
|
||||
// without a second definition of the same Sunday morning.
|
||||
type MaintenanceWindow struct {
|
||||
ID bson.ObjectID `bson:"_id,omitempty" json:"-"`
|
||||
InstanceID string `bson:"instance_id" json:"instance_id"`
|
||||
WindowID string `bson:"window_id" json:"window_id"`
|
||||
Name string `bson:"name" json:"name"`
|
||||
Cron string `bson:"cron" json:"cron"` // 5-field, window start
|
||||
TZ string `bson:"tz" json:"tz"` // IANA name
|
||||
DurationMinutes int `bson:"duration_minutes" json:"duration_minutes"`
|
||||
CreatedAt time.Time `bson:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
type PatchPolicy struct {
|
||||
ID bson.ObjectID `bson:"_id,omitempty" json:"-"`
|
||||
InstanceID string `bson:"instance_id" json:"instance_id"`
|
||||
PolicyID string `bson:"policy_id" json:"policy_id"`
|
||||
Name string `bson:"name" json:"name"`
|
||||
Enabled bool `bson:"enabled" json:"enabled"`
|
||||
WindowID string `bson:"window_id" json:"window_id"`
|
||||
TargetServerIDs []string `bson:"target_server_ids" json:"target_server_ids"`
|
||||
TargetTags map[string]string `bson:"target_tags,omitempty" json:"target_tags,omitempty"`
|
||||
Scope string `bson:"scope" json:"scope"`
|
||||
Reboot string `bson:"reboot" json:"reboot"`
|
||||
MaxConcurrent int `bson:"max_concurrent" json:"max_concurrent"` // 0 = no cap
|
||||
NotifyChannelIDs []string `bson:"notify_channel_ids,omitempty" json:"notify_channel_ids,omitempty"`
|
||||
NextRunAt *time.Time `bson:"next_run_at,omitempty" json:"next_run_at,omitempty"`
|
||||
LastRunAt *time.Time `bson:"last_run_at,omitempty" json:"last_run_at,omitempty"`
|
||||
LastSkipped *Skip `bson:"last_skipped,omitempty" json:"last_skipped,omitempty"`
|
||||
DisabledReason string `bson:"disabled_reason,omitempty" json:"disabled_reason,omitempty"`
|
||||
CreatedAt time.Time `bson:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// PatchRun is one firing of a policy, or one manual Apply updates. Scope,
|
||||
// reboot and concurrency are copied from the policy at fire time so editing
|
||||
// the policy never rewrites what a past run shows.
|
||||
type PatchRun struct {
|
||||
ID bson.ObjectID `bson:"_id,omitempty" json:"-"`
|
||||
InstanceID string `bson:"instance_id" json:"instance_id"`
|
||||
RunID string `bson:"run_id" json:"run_id"`
|
||||
PolicyID string `bson:"policy_id,omitempty" json:"policy_id,omitempty"`
|
||||
PolicyName string `bson:"policy_name,omitempty" json:"policy_name,omitempty"`
|
||||
TriggeredBy string `bson:"triggered_by" json:"triggered_by"`
|
||||
Source string `bson:"source" json:"source"`
|
||||
Scope string `bson:"scope" json:"scope"`
|
||||
Reboot string `bson:"reboot" json:"reboot"`
|
||||
MaxConcurrent int `bson:"max_concurrent" json:"max_concurrent"`
|
||||
WindowEnd *time.Time `bson:"window_end,omitempty" json:"window_end,omitempty"`
|
||||
Status string `bson:"status" json:"status"`
|
||||
CancelledAt *time.Time `bson:"cancelled_at,omitempty" json:"cancelled_at,omitempty"`
|
||||
StartedAt time.Time `bson:"started_at" json:"started_at"`
|
||||
FinishedAt *time.Time `bson:"finished_at,omitempty" json:"finished_at,omitempty"`
|
||||
Servers []PatchServerRun `bson:"servers" json:"servers"`
|
||||
}
|
||||
|
||||
type PatchServerRun struct {
|
||||
ServerID string `bson:"server_id" json:"server_id"`
|
||||
Hostname string `bson:"hostname" json:"hostname"`
|
||||
Status string `bson:"status" json:"status"`
|
||||
CommandID string `bson:"command_id,omitempty" json:"-"`
|
||||
PendingBefore int `bson:"pending_before" json:"pending_before"`
|
||||
PendingAfter *int `bson:"pending_after,omitempty" json:"pending_after,omitempty"`
|
||||
RebootedAt *time.Time `bson:"rebooted_at,omitempty" json:"rebooted_at,omitempty"`
|
||||
VerifiedAt *time.Time `bson:"verified_at,omitempty" json:"verified_at,omitempty"`
|
||||
Output string `bson:"output,omitempty" json:"output,omitempty"`
|
||||
Error string `bson:"error,omitempty" json:"error,omitempty"`
|
||||
StartedAt *time.Time `bson:"started_at,omitempty" json:"started_at,omitempty"`
|
||||
FinishedAt *time.Time `bson:"finished_at,omitempty" json:"finished_at,omitempty"`
|
||||
}
|
||||
@@ -48,6 +48,9 @@ var ScopedCollections = []string{
|
||||
"server_workloads",
|
||||
"status_pages",
|
||||
"status_incidents",
|
||||
"maintenance_windows",
|
||||
"patch_policies",
|
||||
"patch_runs",
|
||||
}
|
||||
|
||||
// collectionRenames maps the two collections whose names change. Ordered so the
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// EnsurePatchIndexes builds the patching indexes. The command_id index is the
|
||||
// one that matters: every PatchResult is matched to its server run through it.
|
||||
func EnsurePatchIndexes() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if _, err := db.Col("maintenance_windows").Indexes().CreateOne(ctx, mongo.IndexModel{
|
||||
Keys: bson.D{{Key: "instance_id", Value: 1}, {Key: "window_id", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.Col("patch_policies").Indexes().CreateMany(ctx, []mongo.IndexModel{
|
||||
{Keys: bson.D{{Key: "instance_id", Value: 1}, {Key: "policy_id", Value: 1}}, Options: options.Index().SetUnique(true)},
|
||||
{Keys: bson.D{{Key: "enabled", Value: 1}, {Key: "next_run_at", Value: 1}}},
|
||||
{Keys: bson.D{{Key: "instance_id", Value: 1}, {Key: "window_id", Value: 1}}},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := db.Col("patch_runs").Indexes().CreateMany(ctx, []mongo.IndexModel{
|
||||
{Keys: bson.D{{Key: "instance_id", Value: 1}, {Key: "run_id", Value: 1}}, Options: options.Index().SetUnique(true)},
|
||||
{Keys: bson.D{{Key: "instance_id", Value: 1}, {Key: "policy_id", Value: 1}, {Key: "started_at", Value: -1}}},
|
||||
{Keys: bson.D{{Key: "instance_id", Value: 1}, {Key: "servers.server_id", Value: 1}, {Key: "started_at", Value: -1}}},
|
||||
{Keys: bson.D{{Key: "status", Value: 1}}},
|
||||
{Keys: bson.D{{Key: "servers.command_id", Value: 1}}},
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package services
|
||||
|
||||
import "testing"
|
||||
|
||||
// A tenant-scoped collection missing from ScopedCollections outlives its
|
||||
// instance when the instance is purged.
|
||||
func TestPatchCollectionsAreScoped(t *testing.T) {
|
||||
for _, name := range []string{"maintenance_windows", "patch_policies", "patch_runs"} {
|
||||
found := false
|
||||
for _, got := range ScopedCollections {
|
||||
if got == name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("ScopedCollections is missing %q", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user