mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
244d322dce
Admit detached metadata in order, merge identity fields without restoring stale operator edits, and persist complete snapshots with one owned writer. Seal late writes and return shutdown failures before organisation data or offboarding is removed. Preserve existing drain budgets, file format, tenant boundaries and atomic persistence. Add controlled burst, ordering, write-error, blocked-close and real deletion controls. Contract-Neutral: Behavioural repair of existing JSON persistence and organisation deletion; no route, field, permission, identity format, schema or provider authority change. Shutdown failure uses the existing HTTP error envelope. Internal ownership methods and explanatory contract text change, not the public contract shape. Change-source: pulse-maintainer
208 lines
6.8 KiB
Go
208 lines
6.8 KiB
Go
package monitoring
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
"github.com/rcourtman/pulse-go-rewrite/pkg/proxmox"
|
|
)
|
|
|
|
func TestPersistGuestIdentity_Concurrent(t *testing.T) {
|
|
// Setup temporary metadata store
|
|
tmpDir, err := os.MkdirTemp("", "persist_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
store := config.NewGuestMetadataStore(tmpDir, nil)
|
|
|
|
guestKey := "pve1:node1:100"
|
|
|
|
// Test basic persistence
|
|
persistGuestIdentity(store, guestKey, "VM 100", "qemu")
|
|
|
|
if !store.WaitForPendingWrites(time.Second) {
|
|
t.Fatal("identity writer did not drain")
|
|
}
|
|
|
|
meta := store.Get(guestKey)
|
|
if meta == nil || meta.LastKnownName != "VM 100" || meta.LastKnownType != "qemu" {
|
|
t.Errorf("Failed to persist guest identity: %+v", meta)
|
|
}
|
|
|
|
// Test persistence with "downgrade" prevention
|
|
// Set type to "oci" manually first
|
|
ociMeta := &config.GuestMetadata{
|
|
ID: guestKey,
|
|
LastKnownName: "VM 100",
|
|
LastKnownType: "oci",
|
|
}
|
|
_ = store.Set(guestKey, ociMeta)
|
|
|
|
// Try to update to "lxc"
|
|
persistGuestIdentity(store, guestKey, "VM 100", "lxc")
|
|
if !store.WaitForPendingWrites(time.Second) {
|
|
t.Fatal("identity writer did not drain")
|
|
}
|
|
|
|
meta = store.Get(guestKey)
|
|
if meta.LastKnownType != "oci" {
|
|
t.Errorf("Should ensure type 'oci' is preserved, got '%s'", meta.LastKnownType)
|
|
}
|
|
|
|
// Test persistence that shouldn't happen (no change) -> coverage of the if check
|
|
// Should not trigger Set()
|
|
persistGuestIdentity(store, guestKey, "VM 100", "oci")
|
|
if err := store.Close(time.Second); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestEnrichWithPersistedMetadata_Detail(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "enrich_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
store := config.NewGuestMetadataStore(tmpDir, nil)
|
|
|
|
// 1. Add some metadata
|
|
_ = store.Set("pve1:node1:100", &config.GuestMetadata{ID: "pve1:node1:100", LastKnownName: "PersistedVM", LastKnownType: "qemu"})
|
|
_ = store.Set("pve1:node1:101", &config.GuestMetadata{ID: "pve1:node1:101", LastKnownName: "LiveVM", LastKnownType: "qemu"})
|
|
_ = store.Set("invalid:key", &config.GuestMetadata{ID: "invalid:key", LastKnownName: "BadKey", LastKnownType: "qemu"}) // coverage for bad key
|
|
_ = store.Set("pve1:node1:badid", &config.GuestMetadata{ID: "pve1:node1:badid", LastKnownName: "BadID", LastKnownType: "qemu"}) // coverage for atoi error
|
|
|
|
// 2. Setup existing lookup
|
|
lookup := make(map[string][]alerts.GuestLookup)
|
|
// VM 101 is live
|
|
lookup["101"] = []alerts.GuestLookup{
|
|
{Name: "LiveVM", Instance: "pve1", Node: "node1", VMID: 101},
|
|
}
|
|
|
|
// 3. Run enrich
|
|
enrichWithPersistedMetadata(store, lookup)
|
|
|
|
// 4. Verify
|
|
// 100 should be added
|
|
if entries, ok := lookup["100"]; !ok || len(entries) != 1 {
|
|
t.Error("Expected VM 100 to be enriched")
|
|
} else {
|
|
if entries[0].Name != "PersistedVM" {
|
|
t.Errorf("Expected name PersistedVM, got %s", entries[0].Name)
|
|
}
|
|
}
|
|
|
|
// 101 should not be duplicated (it was live)
|
|
if len(lookup["101"]) != 1 {
|
|
t.Error("VM 101 should not be duplicated")
|
|
}
|
|
}
|
|
|
|
func TestBuildGuestLookupsFromReadState_PrefersCanonicalWorkloads(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "guest_lookup_read_state")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
store := config.NewGuestMetadataStore(tmpDir, nil)
|
|
registry := unifiedresources.NewRegistry(nil)
|
|
registry.IngestSnapshot(models.StateSnapshot{
|
|
VMs: []models.VM{{
|
|
ID: "vm-1",
|
|
Name: "canonical-vm",
|
|
Instance: "pve1",
|
|
Node: "node1",
|
|
VMID: 100,
|
|
Type: "qemu",
|
|
}},
|
|
Containers: []models.Container{{
|
|
ID: "ct-1",
|
|
Name: "canonical-ct",
|
|
Instance: "pve1",
|
|
Node: "node2",
|
|
VMID: 101,
|
|
Type: "oci",
|
|
IsOCI: true,
|
|
}},
|
|
})
|
|
|
|
byKey, byVMID := buildGuestLookupsFromReadState(registry, store)
|
|
|
|
vmKey := alerts.BuildGuestKey("pve1", "node1", 100)
|
|
if got := byKey[vmKey]; got.Name != "canonical-vm" || got.Type != "qemu" {
|
|
t.Fatalf("expected canonical vm lookup, got %+v", got)
|
|
}
|
|
ctKey := alerts.BuildGuestKey("pve1", "node2", 101)
|
|
if got := byKey[ctKey]; got.Name != "canonical-ct" || got.Type != "oci" {
|
|
t.Fatalf("expected canonical container lookup, got %+v", got)
|
|
}
|
|
if len(byVMID["100"]) != 1 || byVMID["100"][0].Name != "canonical-vm" {
|
|
t.Fatalf("expected vmid lookup for VM 100, got %+v", byVMID["100"])
|
|
}
|
|
if len(byVMID["101"]) != 1 || byVMID["101"][0].Name != "canonical-ct" {
|
|
t.Fatalf("expected vmid lookup for container 101, got %+v", byVMID["101"])
|
|
}
|
|
}
|
|
|
|
func TestBuildGuestLookupsFromReadState_UsesPersistedMetadataWhenCanonicalStateEmpty(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "guest_lookup_persisted")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
store := config.NewGuestMetadataStore(tmpDir, nil)
|
|
_ = store.Set("pve1:node1:100", &config.GuestMetadata{ID: "pve1:node1:100", LastKnownName: "persisted-vm", LastKnownType: "qemu"})
|
|
|
|
byKey, byVMID := buildGuestLookupsFromReadState(unifiedresources.NewRegistry(nil), store)
|
|
|
|
if len(byKey) != 0 {
|
|
t.Fatalf("expected no direct byKey entries without live canonical workloads, got %+v", byKey)
|
|
}
|
|
if len(byVMID["100"]) != 1 || byVMID["100"][0].Name != "persisted-vm" || byVMID["100"][0].Type != "qemu" {
|
|
t.Fatalf("expected persisted metadata fallback, got %+v", byVMID["100"])
|
|
}
|
|
}
|
|
|
|
func TestPVEBackupTemplateInventoryScopeFromClusterResources(t *testing.T) {
|
|
m := &Monitor{}
|
|
|
|
m.updatePVEBackupTemplateSubjectsFromClusterResources("pve-a", []proxmox.ClusterResource{
|
|
{Type: "qemu", Node: "node-a", VMID: 700, Template: 1},
|
|
{Type: "lxc", Node: "node-b", VMID: 701, Template: 1},
|
|
{Type: "qemu", Node: "node-c", VMID: 702, Template: 0},
|
|
})
|
|
|
|
scope := m.backupInventoryScopeForAlerts()
|
|
if scope == nil {
|
|
t.Fatalf("expected backup inventory scope")
|
|
}
|
|
if !scope.PVEOrphanInventoryReady["pve-a"]["qemu"] {
|
|
t.Fatalf("expected qemu backup inventory to be marked ready")
|
|
}
|
|
if !scope.PVEOrphanInventoryReady["pve-a"]["lxc"] {
|
|
t.Fatalf("expected lxc backup inventory to be marked ready")
|
|
}
|
|
|
|
qemuTemplate := alerts.BuildBackupPVETemplateSubjectKey("pve-a", "qemu", "node-a", 700)
|
|
if _, exists := scope.PVETemplateSubjects[qemuTemplate]; !exists {
|
|
t.Fatalf("expected qemu template subject to be captured")
|
|
}
|
|
lxcTemplate := alerts.BuildBackupPVETemplateSubjectKey("pve-a", "lxc", "node-b", 701)
|
|
if _, exists := scope.PVETemplateSubjects[lxcTemplate]; !exists {
|
|
t.Fatalf("expected lxc template subject to be captured")
|
|
}
|
|
nonTemplate := alerts.BuildBackupPVETemplateSubjectKey("pve-a", "qemu", "node-c", 702)
|
|
if _, exists := scope.PVETemplateSubjects[nonTemplate]; exists {
|
|
t.Fatalf("did not expect non-template subject to be captured")
|
|
}
|
|
}
|