mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
Show workload-capable source failures on Workloads and keep matching Proxmox host agents attached to their API source when inventory collection is blocked.
509 lines
18 KiB
Go
509 lines
18 KiB
Go
package api
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
|
unified "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
)
|
|
|
|
func TestBuildConnectionSystems_AttachesMergedAgentToOwningProxmoxSource(t *testing.T) {
|
|
cfg := &config.Config{DataPath: t.TempDir()}
|
|
monitor, err := monitoring.New(cfg)
|
|
if err != nil {
|
|
t.Fatalf("monitoring.New: %v", err)
|
|
}
|
|
t.Cleanup(func() { monitor.Stop() })
|
|
|
|
now := time.Date(2026, 4, 22, 12, 0, 0, 0, time.UTC)
|
|
adapter := unified.NewMonitorAdapter(nil)
|
|
adapter.PopulateSupplementalRecords(unified.SourceProxmox, []unified.IngestRecord{
|
|
{
|
|
SourceID: "node-pi",
|
|
Resource: unified.Resource{
|
|
ID: "resource-node-pi",
|
|
Type: unified.ResourceTypeAgent,
|
|
Name: "pi",
|
|
Status: unified.StatusOnline,
|
|
LastSeen: now,
|
|
Sources: []unified.DataSource{unified.SourceProxmox, unified.SourceAgent},
|
|
Identity: unified.ResourceIdentity{
|
|
MachineID: "machine-pi",
|
|
Hostnames: []string{"pi"},
|
|
},
|
|
Proxmox: &unified.ProxmoxData{
|
|
Instance: "pi",
|
|
NodeName: "pi",
|
|
LinkedAgentID: "agent-pi",
|
|
},
|
|
Agent: &unified.AgentData{
|
|
AgentID: "agent-pi",
|
|
Hostname: "pi",
|
|
MachineID: "machine-pi",
|
|
LinkedNodeID: "node-pi",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
setTestUnexportedField(t, monitor, "resourceStore", monitoring.ResourceStoreInterface(adapter))
|
|
|
|
connections := []Connection{
|
|
{
|
|
ID: "pve:pi",
|
|
Type: ConnectionTypePVE,
|
|
Name: "pi",
|
|
Address: "https://192.168.0.2:8006",
|
|
State: ConnectionStateActive,
|
|
Enabled: true,
|
|
Surfaces: []string{"vms", "containers", "storage", "backups"},
|
|
Scope: map[string]bool{"vms": true, "containers": true, "storage": true, "backups": true},
|
|
Source: ConnectionSourceAgent,
|
|
Capabilities: ConnectionCapabilities{SupportsPause: true, SupportsScope: true, SupportsTest: true},
|
|
},
|
|
{
|
|
ID: "agent:agent-pi",
|
|
Type: ConnectionTypeAgent,
|
|
Name: "pi",
|
|
Address: "pi",
|
|
State: ConnectionStateActive,
|
|
Enabled: true,
|
|
Surfaces: []string{"host"},
|
|
Scope: map[string]bool{"host": true},
|
|
LastSeen: &now,
|
|
Source: ConnectionSourceAgent,
|
|
Capabilities: ConnectionCapabilities{SupportsPause: false, SupportsScope: false, SupportsTest: false},
|
|
},
|
|
}
|
|
|
|
systems := buildConnectionSystems(connections, monitor)
|
|
if len(systems) != 1 {
|
|
t.Fatalf("expected 1 grouped system, got %d (%+v)", len(systems), systems)
|
|
}
|
|
|
|
system := systems[0]
|
|
if system.ID != "pve:pi" {
|
|
t.Fatalf("system id = %q, want %q", system.ID, "pve:pi")
|
|
}
|
|
if system.Type != ConnectionTypePVE {
|
|
t.Fatalf("system type = %q, want %q", system.Type, ConnectionTypePVE)
|
|
}
|
|
if len(system.Components) != 2 {
|
|
t.Fatalf("expected 2 system components, got %+v", system.Components)
|
|
}
|
|
if system.Components[0].ConnectionID != "pve:pi" || system.Components[0].Role != ConnectionSystemComponentRolePrimary {
|
|
t.Fatalf("unexpected primary component: %+v", system.Components[0])
|
|
}
|
|
if system.Components[1].ConnectionID != "agent:agent-pi" || system.Components[1].Role != ConnectionSystemComponentRoleAttachment {
|
|
t.Fatalf("unexpected attachment component: %+v", system.Components[1])
|
|
}
|
|
}
|
|
|
|
func TestBuildConnectionSystems_ClusterMemberAgentsAttachToOwningProxmoxSystem(t *testing.T) {
|
|
cfg := &config.Config{DataPath: t.TempDir()}
|
|
monitor, err := monitoring.New(cfg)
|
|
if err != nil {
|
|
t.Fatalf("monitoring.New: %v", err)
|
|
}
|
|
t.Cleanup(func() { monitor.Stop() })
|
|
|
|
now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC)
|
|
adapter := unified.NewMonitorAdapter(nil)
|
|
adapter.PopulateSupplementalRecords(unified.SourceProxmox, []unified.IngestRecord{
|
|
{
|
|
SourceID: "node-delly",
|
|
Resource: unified.Resource{
|
|
ID: "resource-node-delly",
|
|
Type: unified.ResourceTypeAgent,
|
|
Name: "delly",
|
|
Status: unified.StatusOnline,
|
|
LastSeen: now,
|
|
Sources: []unified.DataSource{unified.SourceProxmox, unified.SourceAgent},
|
|
Identity: unified.ResourceIdentity{
|
|
MachineID: "machine-delly",
|
|
Hostnames: []string{"delly"},
|
|
IPAddresses: []string{"192.168.0.10"},
|
|
},
|
|
Proxmox: &unified.ProxmoxData{
|
|
Instance: "delly",
|
|
NodeName: "delly",
|
|
ClusterName: "homelab",
|
|
IsClusterMember: true,
|
|
HostURL: "https://delly:8006",
|
|
LinkedAgentID: "agent-delly",
|
|
},
|
|
Agent: &unified.AgentData{
|
|
AgentID: "agent-delly",
|
|
Hostname: "delly",
|
|
MachineID: "machine-delly",
|
|
LinkedNodeID: "node-delly",
|
|
},
|
|
},
|
|
Identity: unified.ResourceIdentity{
|
|
MachineID: "machine-delly",
|
|
Hostnames: []string{"delly"},
|
|
IPAddresses: []string{"192.168.0.10"},
|
|
},
|
|
},
|
|
{
|
|
SourceID: "node-minipc",
|
|
Resource: unified.Resource{
|
|
ID: "resource-node-minipc",
|
|
Type: unified.ResourceTypeAgent,
|
|
Name: "minipc",
|
|
Status: unified.StatusOnline,
|
|
LastSeen: now,
|
|
Sources: []unified.DataSource{unified.SourceProxmox, unified.SourceAgent},
|
|
Identity: unified.ResourceIdentity{
|
|
MachineID: "machine-minipc",
|
|
Hostnames: []string{"minipc"},
|
|
IPAddresses: []string{"192.168.0.11"},
|
|
},
|
|
Proxmox: &unified.ProxmoxData{
|
|
Instance: "delly",
|
|
NodeName: "minipc",
|
|
ClusterName: "homelab",
|
|
IsClusterMember: true,
|
|
HostURL: "https://minipc:8006",
|
|
LinkedAgentID: "agent-minipc",
|
|
},
|
|
Agent: &unified.AgentData{
|
|
AgentID: "agent-minipc",
|
|
Hostname: "minipc",
|
|
MachineID: "machine-minipc",
|
|
LinkedNodeID: "node-minipc",
|
|
},
|
|
},
|
|
Identity: unified.ResourceIdentity{
|
|
MachineID: "machine-minipc",
|
|
Hostnames: []string{"minipc"},
|
|
IPAddresses: []string{"192.168.0.11"},
|
|
},
|
|
},
|
|
})
|
|
setTestUnexportedField(t, monitor, "resourceStore", monitoring.ResourceStoreInterface(adapter))
|
|
|
|
connections := []Connection{
|
|
{
|
|
ID: "pve:delly",
|
|
Type: ConnectionTypePVE,
|
|
Name: "delly",
|
|
Address: "https://delly:8006",
|
|
State: ConnectionStateActive,
|
|
Enabled: true,
|
|
Surfaces: []string{"vms", "containers", "storage", "backups"},
|
|
Scope: map[string]bool{"vms": true, "containers": true, "storage": true, "backups": true},
|
|
Source: ConnectionSourceAgent,
|
|
Capabilities: ConnectionCapabilities{SupportsPause: true, SupportsScope: true, SupportsTest: true},
|
|
},
|
|
{
|
|
ID: "agent:agent-delly",
|
|
Type: ConnectionTypeAgent,
|
|
Name: "delly",
|
|
Address: "delly",
|
|
State: ConnectionStateActive,
|
|
Enabled: true,
|
|
Surfaces: []string{"host"},
|
|
Scope: map[string]bool{"host": true},
|
|
LastSeen: &now,
|
|
Source: ConnectionSourceAgent,
|
|
Capabilities: ConnectionCapabilities{SupportsPause: false, SupportsScope: false, SupportsTest: false},
|
|
},
|
|
{
|
|
ID: "agent:agent-minipc",
|
|
Type: ConnectionTypeAgent,
|
|
Name: "minipc",
|
|
Address: "minipc",
|
|
State: ConnectionStateActive,
|
|
Enabled: true,
|
|
Surfaces: []string{"host"},
|
|
Scope: map[string]bool{"host": true},
|
|
LastSeen: &now,
|
|
Source: ConnectionSourceAgent,
|
|
Capabilities: ConnectionCapabilities{SupportsPause: false, SupportsScope: false, SupportsTest: false},
|
|
},
|
|
}
|
|
|
|
systems := buildConnectionSystems(connections, monitor)
|
|
if len(systems) != 1 {
|
|
t.Fatalf("expected 1 grouped cluster system, got %d (%+v)", len(systems), systems)
|
|
}
|
|
|
|
system := systems[0]
|
|
if system.ID != "pve:delly" {
|
|
t.Fatalf("system id = %q, want %q", system.ID, "pve:delly")
|
|
}
|
|
if system.Type != ConnectionTypePVE {
|
|
t.Fatalf("system type = %q, want %q", system.Type, ConnectionTypePVE)
|
|
}
|
|
if system.ClusterName != "homelab" {
|
|
t.Fatalf("system clusterName = %q, want %q", system.ClusterName, "homelab")
|
|
}
|
|
if len(system.Components) != 3 {
|
|
t.Fatalf("expected 3 system components, got %+v", system.Components)
|
|
}
|
|
if len(system.Members) != 2 {
|
|
t.Fatalf("expected 2 system members, got %+v", system.Members)
|
|
}
|
|
|
|
componentRoles := make(map[string]ConnectionSystemComponentRole, len(system.Components))
|
|
for _, component := range system.Components {
|
|
componentRoles[component.ConnectionID] = component.Role
|
|
}
|
|
if componentRoles["pve:delly"] != ConnectionSystemComponentRolePrimary {
|
|
t.Fatalf("pve:delly role = %q, want %q", componentRoles["pve:delly"], ConnectionSystemComponentRolePrimary)
|
|
}
|
|
if componentRoles["agent:agent-delly"] != ConnectionSystemComponentRoleAttachment {
|
|
t.Fatalf("agent:agent-delly role = %q, want %q", componentRoles["agent:agent-delly"], ConnectionSystemComponentRoleAttachment)
|
|
}
|
|
if componentRoles["agent:agent-minipc"] != ConnectionSystemComponentRoleAttachment {
|
|
t.Fatalf("agent:agent-minipc role = %q, want %q", componentRoles["agent:agent-minipc"], ConnectionSystemComponentRoleAttachment)
|
|
}
|
|
|
|
membersByName := make(map[string]ConnectionSystemMember, len(system.Members))
|
|
for _, member := range system.Members {
|
|
membersByName[member.Name] = member
|
|
}
|
|
|
|
dellyMember, ok := membersByName["delly"]
|
|
if !ok {
|
|
t.Fatalf("expected delly member, got %+v", system.Members)
|
|
}
|
|
if !dellyMember.Primary {
|
|
t.Fatalf("expected delly to be the primary cluster member, got %+v", dellyMember)
|
|
}
|
|
if dellyMember.Endpoint != "https://delly:8006" {
|
|
t.Fatalf("delly endpoint = %q, want %q", dellyMember.Endpoint, "https://delly:8006")
|
|
}
|
|
if !reflect.DeepEqual(dellyMember.HostAliases, []string{"delly", "192.168.0.10"}) {
|
|
t.Fatalf("delly host aliases = %+v, want %+v", dellyMember.HostAliases, []string{"delly", "192.168.0.10"})
|
|
}
|
|
if dellyMember.AgentConnectionID != "agent:agent-delly" {
|
|
t.Fatalf("delly agent connection = %q, want %q", dellyMember.AgentConnectionID, "agent:agent-delly")
|
|
}
|
|
if dellyMember.State != ConnectionStateActive {
|
|
t.Fatalf("delly state = %q, want %q", dellyMember.State, ConnectionStateActive)
|
|
}
|
|
|
|
minipcMember, ok := membersByName["minipc"]
|
|
if !ok {
|
|
t.Fatalf("expected minipc member, got %+v", system.Members)
|
|
}
|
|
if minipcMember.Primary {
|
|
t.Fatalf("minipc should not be marked primary: %+v", minipcMember)
|
|
}
|
|
if minipcMember.Endpoint != "https://minipc:8006" {
|
|
t.Fatalf("minipc endpoint = %q, want %q", minipcMember.Endpoint, "https://minipc:8006")
|
|
}
|
|
if !reflect.DeepEqual(minipcMember.HostAliases, []string{"minipc", "192.168.0.11"}) {
|
|
t.Fatalf("minipc host aliases = %+v, want %+v", minipcMember.HostAliases, []string{"minipc", "192.168.0.11"})
|
|
}
|
|
if minipcMember.AgentConnectionID != "agent:agent-minipc" {
|
|
t.Fatalf("minipc agent connection = %q, want %q", minipcMember.AgentConnectionID, "agent:agent-minipc")
|
|
}
|
|
if minipcMember.State != ConnectionStateActive {
|
|
t.Fatalf("minipc state = %q, want %q", minipcMember.State, ConnectionStateActive)
|
|
}
|
|
}
|
|
|
|
func TestMergeConnectionSystemMembersKeepsMostSevereState(t *testing.T) {
|
|
activeMember := ConnectionSystemMember{
|
|
ID: "node-minipc",
|
|
Name: "minipc",
|
|
State: ConnectionStateActive,
|
|
Primary: true,
|
|
}
|
|
offlineMember := ConnectionSystemMember{
|
|
ID: "resource-node-minipc",
|
|
Name: "minipc",
|
|
State: ConnectionStateUnreachable,
|
|
}
|
|
|
|
merged := mergeConnectionSystemMembers(activeMember, offlineMember)
|
|
if merged.State != ConnectionStateUnreachable {
|
|
t.Fatalf("merged state = %q, want %q", merged.State, ConnectionStateUnreachable)
|
|
}
|
|
if !merged.Primary {
|
|
t.Fatalf("expected primary marker to survive merge: %+v", merged)
|
|
}
|
|
|
|
staleMember := ConnectionSystemMember{Name: "minipc", State: ConnectionStateStale}
|
|
freshMember := ConnectionSystemMember{Name: "minipc", State: ConnectionStateActive}
|
|
merged = mergeConnectionSystemMembers(staleMember, freshMember)
|
|
if merged.State != ConnectionStateStale {
|
|
t.Fatalf("merged state = %q, want %q", merged.State, ConnectionStateStale)
|
|
}
|
|
}
|
|
|
|
func TestBuildConnectionSystems_GuestAgentStaysStandaloneWhenOnlyClusterInstanceMatches(t *testing.T) {
|
|
cfg := &config.Config{DataPath: t.TempDir()}
|
|
monitor, err := monitoring.New(cfg)
|
|
if err != nil {
|
|
t.Fatalf("monitoring.New: %v", err)
|
|
}
|
|
t.Cleanup(func() { monitor.Stop() })
|
|
|
|
now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC)
|
|
adapter := unified.NewMonitorAdapter(nil)
|
|
adapter.PopulateSupplementalRecords(unified.SourceProxmox, []unified.IngestRecord{
|
|
{
|
|
SourceID: "guest-100",
|
|
Resource: unified.Resource{
|
|
ID: "resource-guest-100",
|
|
Type: unified.ResourceTypeAgent,
|
|
Name: "docker-vm",
|
|
Status: unified.StatusOnline,
|
|
LastSeen: now,
|
|
Sources: []unified.DataSource{unified.SourceProxmox, unified.SourceAgent},
|
|
Identity: unified.ResourceIdentity{
|
|
MachineID: "machine-guest-100",
|
|
Hostnames: []string{"docker-vm"},
|
|
},
|
|
Proxmox: &unified.ProxmoxData{
|
|
Instance: "delly",
|
|
NodeName: "minipc",
|
|
ClusterName: "homelab",
|
|
IsClusterMember: true,
|
|
VMID: 100,
|
|
},
|
|
Agent: &unified.AgentData{
|
|
AgentID: "agent-guest-100",
|
|
Hostname: "docker-vm",
|
|
MachineID: "machine-guest-100",
|
|
LinkedVMID: "guest-100",
|
|
LinkedNodeID: "",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
setTestUnexportedField(t, monitor, "resourceStore", monitoring.ResourceStoreInterface(adapter))
|
|
|
|
connections := []Connection{
|
|
{
|
|
ID: "pve:delly",
|
|
Type: ConnectionTypePVE,
|
|
Name: "delly",
|
|
Address: "https://delly:8006",
|
|
State: ConnectionStateActive,
|
|
Enabled: true,
|
|
Surfaces: []string{"vms", "containers", "storage", "backups"},
|
|
Scope: map[string]bool{"vms": true, "containers": true, "storage": true, "backups": true},
|
|
Source: ConnectionSourceAgent,
|
|
Capabilities: ConnectionCapabilities{SupportsPause: true, SupportsScope: true, SupportsTest: true},
|
|
},
|
|
{
|
|
ID: "agent:agent-guest-100",
|
|
Type: ConnectionTypeAgent,
|
|
Name: "docker-vm",
|
|
Address: "docker-vm",
|
|
State: ConnectionStateActive,
|
|
Enabled: true,
|
|
Surfaces: []string{"host"},
|
|
Scope: map[string]bool{"host": true},
|
|
LastSeen: &now,
|
|
Source: ConnectionSourceAgent,
|
|
Capabilities: ConnectionCapabilities{SupportsPause: false, SupportsScope: false, SupportsTest: false},
|
|
},
|
|
}
|
|
|
|
systems := buildConnectionSystems(connections, monitor)
|
|
if len(systems) != 2 {
|
|
t.Fatalf("expected 2 grouped systems, got %d (%+v)", len(systems), systems)
|
|
}
|
|
|
|
systemsByID := make(map[string]ConnectionSystem, len(systems))
|
|
for _, system := range systems {
|
|
systemsByID[system.ID] = system
|
|
}
|
|
|
|
if len(systemsByID["pve:delly"].Components) != 1 {
|
|
t.Fatalf("pve:delly should keep only its primary component, got %+v", systemsByID["pve:delly"].Components)
|
|
}
|
|
guestSystem := systemsByID["agent:agent-guest-100"]
|
|
if len(guestSystem.Components) != 1 || guestSystem.Components[0].Role != ConnectionSystemComponentRolePrimary {
|
|
t.Fatalf("guest agent should remain standalone, got %+v", guestSystem.Components)
|
|
}
|
|
}
|
|
|
|
func TestBuildConnectionSystems_AttachesHostAgentToMatchingProxmoxSourceWithoutNodeInventory(t *testing.T) {
|
|
cfg := &config.Config{DataPath: t.TempDir()}
|
|
monitor, err := monitoring.New(cfg)
|
|
if err != nil {
|
|
t.Fatalf("monitoring.New: %v", err)
|
|
}
|
|
t.Cleanup(func() { monitor.Stop() })
|
|
|
|
now := time.Date(2026, 5, 13, 23, 45, 0, 0, time.UTC)
|
|
connections := []Connection{
|
|
{
|
|
ID: "pve:delly",
|
|
Type: ConnectionTypePVE,
|
|
Name: "delly",
|
|
Address: "https://delly:8006",
|
|
HostAliases: []string{"delly"},
|
|
State: ConnectionStateUnauthorized,
|
|
Enabled: true,
|
|
Surfaces: []string{"vms", "containers", "storage", "backups"},
|
|
Scope: map[string]bool{"vms": true, "containers": true, "storage": true, "backups": true},
|
|
Source: ConnectionSourceAgent,
|
|
Capabilities: ConnectionCapabilities{SupportsPause: true, SupportsScope: true, SupportsTest: true},
|
|
},
|
|
{
|
|
ID: "agent:agent-delly",
|
|
Type: ConnectionTypeAgent,
|
|
Name: "delly",
|
|
Address: "delly",
|
|
HostAliases: []string{"delly", "192.168.0.5"},
|
|
State: ConnectionStateActive,
|
|
Enabled: true,
|
|
Surfaces: []string{"host"},
|
|
Scope: map[string]bool{"host": true},
|
|
LastSeen: &now,
|
|
Source: ConnectionSourceAgent,
|
|
Capabilities: ConnectionCapabilities{SupportsPause: false, SupportsScope: false, SupportsTest: false},
|
|
},
|
|
{
|
|
ID: "agent:agent-minipc",
|
|
Type: ConnectionTypeAgent,
|
|
Name: "minipc",
|
|
Address: "minipc",
|
|
HostAliases: []string{"minipc", "192.168.0.134"},
|
|
State: ConnectionStateActive,
|
|
Enabled: true,
|
|
Surfaces: []string{"host"},
|
|
Scope: map[string]bool{"host": true},
|
|
LastSeen: &now,
|
|
Source: ConnectionSourceAgent,
|
|
Capabilities: ConnectionCapabilities{SupportsPause: false, SupportsScope: false, SupportsTest: false},
|
|
},
|
|
}
|
|
|
|
systems := buildConnectionSystems(connections, monitor)
|
|
systemsByID := make(map[string]ConnectionSystem, len(systems))
|
|
for _, system := range systems {
|
|
systemsByID[system.ID] = system
|
|
}
|
|
|
|
dellySystem := systemsByID["pve:delly"]
|
|
if len(dellySystem.Components) != 2 {
|
|
t.Fatalf("expected delly API and agent components, got %+v", dellySystem.Components)
|
|
}
|
|
|
|
componentRoles := make(map[string]ConnectionSystemComponentRole, len(dellySystem.Components))
|
|
for _, component := range dellySystem.Components {
|
|
componentRoles[component.ConnectionID] = component.Role
|
|
}
|
|
if componentRoles["pve:delly"] != ConnectionSystemComponentRolePrimary {
|
|
t.Fatalf("pve:delly role = %q, want %q", componentRoles["pve:delly"], ConnectionSystemComponentRolePrimary)
|
|
}
|
|
if componentRoles["agent:agent-delly"] != ConnectionSystemComponentRoleAttachment {
|
|
t.Fatalf("agent:agent-delly role = %q, want %q", componentRoles["agent:agent-delly"], ConnectionSystemComponentRoleAttachment)
|
|
}
|
|
|
|
minipcSystem := systemsByID["agent:agent-minipc"]
|
|
if len(minipcSystem.Components) != 1 || minipcSystem.Components[0].Role != ConnectionSystemComponentRolePrimary {
|
|
t.Fatalf("minipc should remain standalone without a direct source host match, got %+v", minipcSystem.Components)
|
|
}
|
|
}
|