Pulse/internal/api/connections_grouping_test.go
rcourtman 321f563a52 Surface blocked workload inventory sources
Show workload-capable source failures on Workloads and keep matching Proxmox host agents attached to their API source when inventory collection is blocked.
2026-05-14 00:22:18 +01:00

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)
}
}