mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-20 10:52:08 +00:00
860 lines
29 KiB
Go
860 lines
29 KiB
Go
package api
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/remoteconfig"
|
|
)
|
|
|
|
func ptrTime(t time.Time) *time.Time { return &t }
|
|
|
|
func ptrBool(v bool) *bool { return &v }
|
|
|
|
func healthEntry(lastSuccess *time.Time, errMessage, errCategory string, breakerState string) monitoring.InstanceHealth {
|
|
ps := monitoring.InstancePollStatus{LastSuccess: lastSuccess}
|
|
if errMessage != "" {
|
|
ps.LastError = &monitoring.ErrorDetail{
|
|
At: time.Now(),
|
|
Message: errMessage,
|
|
Category: errCategory,
|
|
}
|
|
}
|
|
return monitoring.InstanceHealth{
|
|
PollStatus: ps,
|
|
Breaker: monitoring.InstanceBreaker{State: breakerState},
|
|
}
|
|
}
|
|
|
|
func desiredAgentConfigFingerprint(t *testing.T, commandsEnabled *bool, settings map[string]interface{}) ConnectionFleetConfigFingerprint {
|
|
t.Helper()
|
|
desired := desiredAgentConfig(t, commandsEnabled, settings)
|
|
if desired.Fingerprint == nil {
|
|
t.Fatal("expected desired config fingerprint")
|
|
}
|
|
return *desired.Fingerprint
|
|
}
|
|
|
|
func desiredAgentConfig(t *testing.T, commandsEnabled *bool, settings map[string]interface{}) connectionAgentDesiredConfig {
|
|
t.Helper()
|
|
metadata, err := remoteconfig.BuildDesiredConfigMetadata(commandsEnabled, settings)
|
|
if err != nil {
|
|
t.Fatalf("BuildDesiredConfigMetadata: %v", err)
|
|
}
|
|
fingerprint := ConnectionFleetConfigFingerprint{
|
|
Version: metadata.Version,
|
|
Hash: metadata.Hash,
|
|
}
|
|
return connectionAgentDesiredConfig{
|
|
Fingerprint: &fingerprint,
|
|
CommandsEnabled: cloneBoolPtr(commandsEnabled),
|
|
}
|
|
}
|
|
|
|
func TestDeriveConnectionState_Paused(t *testing.T) {
|
|
state, reason, _, _ := deriveConnectionState(false, monitoring.InstanceHealth{}, time.Now())
|
|
if state != ConnectionStatePaused {
|
|
t.Fatalf("got %q, want %q", state, ConnectionStatePaused)
|
|
}
|
|
if reason == "" {
|
|
t.Fatal("expected non-empty reason for paused state")
|
|
}
|
|
}
|
|
|
|
func TestDeriveConnectionState_Pending(t *testing.T) {
|
|
state, _, lastSeen, lastError := deriveConnectionState(true, monitoring.InstanceHealth{}, time.Now())
|
|
if state != ConnectionStatePending {
|
|
t.Fatalf("got %q, want %q", state, ConnectionStatePending)
|
|
}
|
|
if lastSeen != nil || lastError != nil {
|
|
t.Fatal("expected nil lastSeen and lastError in pending state")
|
|
}
|
|
}
|
|
|
|
func TestDeriveConnectionState_Unauthorized(t *testing.T) {
|
|
now := time.Now()
|
|
h := healthEntry(ptrTime(now.Add(-30*time.Second)), "401 Unauthorized: token invalid", "auth", "closed")
|
|
state, reason, _, err := deriveConnectionState(true, h, now)
|
|
if state != ConnectionStateUnauthorized {
|
|
t.Fatalf("got %q, want %q", state, ConnectionStateUnauthorized)
|
|
}
|
|
if reason == "" {
|
|
t.Fatal("expected reason to include error message")
|
|
}
|
|
if err == nil || err.Message != "401 Unauthorized: token invalid" {
|
|
t.Fatalf("unexpected lastError: %+v", err)
|
|
}
|
|
}
|
|
|
|
func TestDeriveConnectionState_Unreachable(t *testing.T) {
|
|
now := time.Now()
|
|
h := healthEntry(ptrTime(now.Add(-30*time.Second)), "connection refused", "network", "open")
|
|
state, _, _, _ := deriveConnectionState(true, h, now)
|
|
if state != ConnectionStateUnreachable {
|
|
t.Fatalf("got %q, want %q", state, ConnectionStateUnreachable)
|
|
}
|
|
}
|
|
|
|
func TestDeriveConnectionState_Stale(t *testing.T) {
|
|
now := time.Now()
|
|
stale := now.Add(-5 * time.Minute)
|
|
h := healthEntry(ptrTime(stale), "", "", "closed")
|
|
state, reason, _, _ := deriveConnectionState(true, h, now)
|
|
if state != ConnectionStateStale {
|
|
t.Fatalf("got %q, want %q", state, ConnectionStateStale)
|
|
}
|
|
if reason == "" {
|
|
t.Fatal("expected reason to describe staleness")
|
|
}
|
|
}
|
|
|
|
func TestDeriveConnectionState_Active(t *testing.T) {
|
|
now := time.Now()
|
|
h := healthEntry(ptrTime(now.Add(-10*time.Second)), "", "", "closed")
|
|
state, _, _, _ := deriveConnectionState(true, h, now)
|
|
if state != ConnectionStateActive {
|
|
t.Fatalf("got %q, want %q", state, ConnectionStateActive)
|
|
}
|
|
}
|
|
|
|
func TestBuildConnections_SortsByTypeThenName(t *testing.T) {
|
|
now := time.Now()
|
|
in := aggregatorInputs{
|
|
pveInstances: []config.PVEInstance{
|
|
{Name: "beta", Host: "https://b.lan:8006", MonitorVMs: true},
|
|
{Name: "alpha", Host: "https://a.lan:8006", MonitorVMs: true},
|
|
},
|
|
pbsInstances: []config.PBSInstance{
|
|
{Name: "backups", Host: "https://bkp.lan:8007"},
|
|
},
|
|
now: now,
|
|
}
|
|
got := buildConnections(in)
|
|
if len(got) != 3 {
|
|
t.Fatalf("expected 3 connections, got %d", len(got))
|
|
}
|
|
if got[0].Type != ConnectionTypePBS {
|
|
t.Fatalf("expected PBS first, got %q", got[0].Type)
|
|
}
|
|
if got[1].Name != "alpha" || got[2].Name != "beta" {
|
|
t.Fatalf("PVE order wrong: %q, %q", got[1].Name, got[2].Name)
|
|
}
|
|
}
|
|
|
|
func TestBuildConnections_PVEPausedRespectsDisabled(t *testing.T) {
|
|
in := aggregatorInputs{
|
|
pveInstances: []config.PVEInstance{{Name: "pve1", Host: "https://pve1.lan:8006", Disabled: true}},
|
|
now: time.Now(),
|
|
}
|
|
got := buildConnections(in)
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected 1 connection, got %d", len(got))
|
|
}
|
|
if got[0].State != ConnectionStatePaused {
|
|
t.Fatalf("expected paused, got %q", got[0].State)
|
|
}
|
|
if got[0].Enabled {
|
|
t.Fatal("expected Enabled=false for Disabled PVE instance")
|
|
}
|
|
}
|
|
|
|
func TestBuildConnections_AgentStateFromLastSeen(t *testing.T) {
|
|
now := time.Now()
|
|
in := aggregatorInputs{
|
|
hosts: []models.Host{
|
|
{ID: "fresh", Hostname: "h1", LastSeen: now.Add(-10 * time.Second)},
|
|
{ID: "stale", Hostname: "h2", LastSeen: now.Add(-5 * time.Minute)},
|
|
{ID: "never", Hostname: "h3"},
|
|
},
|
|
now: now,
|
|
}
|
|
got := buildConnections(in)
|
|
byID := map[string]Connection{}
|
|
for _, c := range got {
|
|
byID[c.ID] = c
|
|
}
|
|
if byID["agent:fresh"].State != ConnectionStateActive {
|
|
t.Fatalf("fresh agent: got %q, want active", byID["agent:fresh"].State)
|
|
}
|
|
if byID["agent:stale"].State != ConnectionStateStale {
|
|
t.Fatalf("stale agent: got %q, want stale", byID["agent:stale"].State)
|
|
}
|
|
if byID["agent:never"].State != ConnectionStatePending {
|
|
t.Fatalf("never-reported agent: got %q, want pending", byID["agent:never"].State)
|
|
}
|
|
for _, c := range got {
|
|
if c.Capabilities.SupportsPause || c.Capabilities.SupportsScope {
|
|
t.Fatalf("agents must not advertise pause/scope capabilities: %+v", c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildConnections_AvailabilityTargetStateFromProbeStatus(t *testing.T) {
|
|
checkedAt := time.Date(2026, 5, 6, 10, 0, 0, 0, time.UTC)
|
|
in := aggregatorInputs{
|
|
availabilityTargets: []config.AvailabilityTarget{
|
|
{
|
|
ID: "sensor-1",
|
|
Name: "Energy monitor",
|
|
Address: "192.0.2.10",
|
|
Protocol: config.AvailabilityProbeICMP,
|
|
Enabled: true,
|
|
FailureThreshold: 2,
|
|
},
|
|
},
|
|
availabilityStatuses: map[string]monitoring.AvailabilityProbeStatus{
|
|
"sensor-1": {
|
|
TargetID: "sensor-1",
|
|
Available: false,
|
|
LastChecked: checkedAt,
|
|
ConsecutiveFailures: 1,
|
|
LastError: "timeout",
|
|
},
|
|
},
|
|
now: checkedAt.Add(5 * time.Second),
|
|
}
|
|
|
|
got := buildConnections(in)
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected 1 connection, got %d", len(got))
|
|
}
|
|
connection := got[0]
|
|
if connection.ID != "availability:sensor-1" {
|
|
t.Fatalf("ID = %q, want availability:sensor-1", connection.ID)
|
|
}
|
|
if connection.Type != ConnectionTypeAvailability {
|
|
t.Fatalf("Type = %q, want availability", connection.Type)
|
|
}
|
|
if connection.State != ConnectionStateUnreachable {
|
|
t.Fatalf("State = %q, want unreachable", connection.State)
|
|
}
|
|
if connection.LastError == nil || connection.LastError.Category != "availability" {
|
|
t.Fatalf("LastError = %+v, want availability category", connection.LastError)
|
|
}
|
|
if !reflect.DeepEqual(connection.Surfaces, []string{"availability"}) {
|
|
t.Fatalf("Surfaces = %+v, want [availability]", connection.Surfaces)
|
|
}
|
|
if !connection.Capabilities.SupportsPause || connection.Capabilities.SupportsScope || !connection.Capabilities.SupportsTest {
|
|
t.Fatalf("Capabilities = %+v, want pause/test without scope", connection.Capabilities)
|
|
}
|
|
if !reflect.DeepEqual(connection.HostAliases, []string{"192.0.2.10"}) {
|
|
t.Fatalf("HostAliases = %+v, want endpoint address", connection.HostAliases)
|
|
}
|
|
}
|
|
|
|
func TestBuildConnections_AgentHostAliasesIncludeReportedIdentityHints(t *testing.T) {
|
|
now := time.Now()
|
|
in := aggregatorInputs{
|
|
hosts: []models.Host{
|
|
{
|
|
ID: "pi",
|
|
Hostname: "pi",
|
|
Platform: "slackware",
|
|
OSName: "Unraid",
|
|
OSVersion: "7.1.0",
|
|
KernelVersion: "6.12.0",
|
|
Architecture: "x86_64",
|
|
ReportIP: "192.168.0.2",
|
|
LastSeen: now,
|
|
AgentVersion: "6.0.0",
|
|
CommandsEnabled: true,
|
|
NetworkInterfaces: []models.HostNetworkInterface{
|
|
{Name: "eth0", Addresses: []string{"192.168.0.2/24", "fe80::1/64"}},
|
|
},
|
|
},
|
|
},
|
|
expectedAgentVersion: "6.0.1",
|
|
now: now,
|
|
}
|
|
|
|
got := buildConnections(in)
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected 1 connection, got %d", len(got))
|
|
}
|
|
if !reflect.DeepEqual(got[0].HostAliases, []string{"pi", "192.168.0.2", "fe80::1"}) {
|
|
t.Fatalf("agent host aliases = %+v, want %+v", got[0].HostAliases, []string{"pi", "192.168.0.2", "fe80::1"})
|
|
}
|
|
if got[0].AgentIdentity == nil {
|
|
t.Fatal("expected agent identity metadata")
|
|
}
|
|
if !reflect.DeepEqual(
|
|
got[0].AgentIdentity,
|
|
&ConnectionAgentIdentity{
|
|
Hostname: "pi",
|
|
Platform: "linux",
|
|
HostProfile: "unraid",
|
|
OSName: "Unraid",
|
|
OSVersion: "7.1.0",
|
|
KernelVersion: "6.12.0",
|
|
Architecture: "x86_64",
|
|
ReportIP: "192.168.0.2",
|
|
CommandsEnabled: true,
|
|
},
|
|
) {
|
|
t.Fatalf("agent identity = %+v", got[0].AgentIdentity)
|
|
}
|
|
}
|
|
|
|
func TestBuildConnections_AgentHostProfileFromUnraidStorageFacts(t *testing.T) {
|
|
now := time.Now()
|
|
in := aggregatorInputs{
|
|
hosts: []models.Host{
|
|
{
|
|
ID: "tower",
|
|
Hostname: "tower",
|
|
Platform: "linux",
|
|
Unraid: &models.HostUnraidStorage{ArrayStarted: true},
|
|
LastSeen: now,
|
|
AgentVersion: "6.12.10",
|
|
CommandsEnabled: false,
|
|
},
|
|
},
|
|
now: now,
|
|
}
|
|
|
|
got := buildConnections(in)
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected 1 connection, got %d", len(got))
|
|
}
|
|
identity := got[0].AgentIdentity
|
|
if identity == nil {
|
|
t.Fatal("expected agent identity metadata")
|
|
}
|
|
if identity.HostProfile != "unraid" {
|
|
t.Fatalf("host profile = %q, want %q", identity.HostProfile, "unraid")
|
|
}
|
|
if identity.Platform != "linux" {
|
|
t.Fatalf("platform = %q, want %q", identity.Platform, "linux")
|
|
}
|
|
if identity.OSName != "" {
|
|
t.Fatalf("os name = %q, want empty when source identity is absent", identity.OSName)
|
|
}
|
|
}
|
|
|
|
func TestBuildConnections_AgentVersionUpdateAvailability(t *testing.T) {
|
|
now := time.Now()
|
|
in := aggregatorInputs{
|
|
hosts: []models.Host{
|
|
{ID: "current", Hostname: "current", LastSeen: now, AgentVersion: "6.0.2"},
|
|
{ID: "outdated", Hostname: "outdated", LastSeen: now, AgentVersion: "6.0.0"},
|
|
{ID: "unknown", Hostname: "unknown", LastSeen: now},
|
|
},
|
|
expectedAgentVersion: "6.0.2",
|
|
now: now,
|
|
}
|
|
|
|
got := buildConnections(in)
|
|
byID := map[string]Connection{}
|
|
for _, connection := range got {
|
|
byID[connection.ID] = connection
|
|
}
|
|
|
|
if byID["agent:current"].AgentVersion != "6.0.2" {
|
|
t.Fatalf("current agent version = %q, want %q", byID["agent:current"].AgentVersion, "6.0.2")
|
|
}
|
|
if byID["agent:current"].ExpectedAgentVersion != "6.0.2" {
|
|
t.Fatalf(
|
|
"current expected agent version = %q, want %q",
|
|
byID["agent:current"].ExpectedAgentVersion,
|
|
"6.0.2",
|
|
)
|
|
}
|
|
if byID["agent:current"].AgentUpdateAvailable {
|
|
t.Fatal("current agent should not report an update available")
|
|
}
|
|
|
|
if byID["agent:outdated"].AgentVersion != "6.0.0" {
|
|
t.Fatalf(
|
|
"outdated agent version = %q, want %q",
|
|
byID["agent:outdated"].AgentVersion,
|
|
"6.0.0",
|
|
)
|
|
}
|
|
if !byID["agent:outdated"].AgentUpdateAvailable {
|
|
t.Fatal("outdated agent should report an update available")
|
|
}
|
|
|
|
if byID["agent:unknown"].AgentVersion != "" {
|
|
t.Fatalf("unknown agent version = %q, want empty", byID["agent:unknown"].AgentVersion)
|
|
}
|
|
if byID["agent:unknown"].AgentUpdateAvailable {
|
|
t.Fatal("agent without a reported version should not raise an update flag")
|
|
}
|
|
}
|
|
|
|
func TestBuildConnections_AgentFleetGovernance(t *testing.T) {
|
|
now := time.Now()
|
|
currentDesiredConfig := desiredAgentConfig(t, ptrBool(true), nil)
|
|
currentDesired := *currentDesiredConfig.Fingerprint
|
|
in := aggregatorInputs{
|
|
hosts: []models.Host{
|
|
{
|
|
ID: "current",
|
|
Hostname: "current",
|
|
LastSeen: now,
|
|
AgentVersion: "6.0.2",
|
|
CommandsEnabled: true,
|
|
},
|
|
{
|
|
ID: "outdated",
|
|
Hostname: "outdated",
|
|
LastSeen: now,
|
|
AgentVersion: "6.0.0",
|
|
},
|
|
{
|
|
ID: "pending",
|
|
Hostname: "pending",
|
|
},
|
|
},
|
|
agentDesiredConfigs: map[string]connectionAgentDesiredConfig{
|
|
"current": currentDesiredConfig,
|
|
},
|
|
expectedAgentVersion: "6.0.2",
|
|
now: now,
|
|
}
|
|
|
|
got := buildConnections(in)
|
|
byID := map[string]Connection{}
|
|
for _, connection := range got {
|
|
byID[connection.ID] = connection
|
|
}
|
|
|
|
current := byID["agent:current"].Fleet
|
|
if current.EnrollmentState != fleetStateEnrolled ||
|
|
current.LivenessState != fleetStateActive ||
|
|
current.VersionDrift != fleetStateCurrent ||
|
|
current.AdapterHealth != fleetStateHealthy ||
|
|
current.ConfigRollout != fleetStateReported ||
|
|
current.CredentialStatus != fleetStateVerified ||
|
|
current.UpdateStatus != fleetStateCurrent ||
|
|
current.RemoteControl != fleetStateEnabled {
|
|
t.Fatalf("current agent fleet governance = %+v", current)
|
|
}
|
|
if current.ConfigDrift == nil ||
|
|
current.ConfigDrift.Status != fleetStatePending ||
|
|
current.ConfigDrift.Desired == nil ||
|
|
current.ConfigDrift.Applied != nil ||
|
|
current.ConfigDrift.Desired.Version != connectionAgentConfigFingerprintVersion ||
|
|
current.ConfigDrift.Desired.Hash != currentDesired.Hash {
|
|
t.Fatalf("current agent config drift = %+v", current.ConfigDrift)
|
|
}
|
|
if current.Rollout == nil || current.Rollout.Status != fleetStatePending || current.Rollout.Stage != fleetRolloutStagePending {
|
|
t.Fatalf("current agent rollout = %+v", current.Rollout)
|
|
}
|
|
if current.CommandPolicy == nil ||
|
|
current.CommandPolicy.Status != fleetStateEnabled ||
|
|
current.CommandPolicy.Desired != fleetStateEnabled ||
|
|
current.CommandPolicy.Applied != fleetStateEnabled ||
|
|
current.CommandPolicy.Enforcement != fleetCommandPolicyInSync {
|
|
t.Fatalf("current agent command policy = %+v", current.CommandPolicy)
|
|
}
|
|
|
|
outdated := byID["agent:outdated"].Fleet
|
|
if outdated.VersionDrift != fleetStateBehind ||
|
|
outdated.UpdateStatus != fleetStateUpdateAvailable ||
|
|
outdated.RemoteControl != fleetStateDisabled {
|
|
t.Fatalf("outdated agent fleet governance = %+v", outdated)
|
|
}
|
|
if outdated.CommandPolicy == nil ||
|
|
outdated.CommandPolicy.Status != fleetStateDisabled ||
|
|
outdated.CommandPolicy.Desired != fleetStateUnknown ||
|
|
outdated.CommandPolicy.Applied != fleetStateDisabled ||
|
|
outdated.CommandPolicy.Enforcement != fleetCommandPolicyNotApplicable {
|
|
t.Fatalf("outdated agent command policy = %+v", outdated.CommandPolicy)
|
|
}
|
|
|
|
pending := byID["agent:pending"].Fleet
|
|
if pending.EnrollmentState != fleetStatePending ||
|
|
pending.LivenessState != string(ConnectionStatePending) ||
|
|
pending.AdapterHealth != fleetStateDegraded ||
|
|
pending.ConfigRollout != fleetStateUnknown ||
|
|
pending.CredentialStatus != fleetStateUnknown {
|
|
t.Fatalf("pending agent fleet governance = %+v", pending)
|
|
}
|
|
if pending.ConfigDrift == nil || pending.ConfigDrift.Status != fleetStateUnknown {
|
|
t.Fatalf("pending agent config drift = %+v", pending.ConfigDrift)
|
|
}
|
|
if pending.Rollout == nil || pending.Rollout.Status != fleetStatePending {
|
|
t.Fatalf("pending agent rollout = %+v", pending.Rollout)
|
|
}
|
|
if pending.CommandPolicy == nil ||
|
|
pending.CommandPolicy.Desired != fleetStateUnknown ||
|
|
pending.CommandPolicy.Applied != fleetStateUnknown ||
|
|
pending.CommandPolicy.Enforcement != fleetStatePending {
|
|
t.Fatalf("pending agent command policy = %+v", pending.CommandPolicy)
|
|
}
|
|
}
|
|
|
|
func TestBuildConnections_AgentWithoutManagedDesiredConfigDoesNotReportRolloutPending(t *testing.T) {
|
|
now := time.Now()
|
|
in := aggregatorInputs{
|
|
hosts: []models.Host{
|
|
{
|
|
ID: "default-agent",
|
|
Hostname: "default-agent",
|
|
LastSeen: now,
|
|
CommandsEnabled: false,
|
|
},
|
|
},
|
|
agentDesiredConfigs: map[string]connectionAgentDesiredConfig{
|
|
"default-agent": {},
|
|
},
|
|
now: now,
|
|
}
|
|
|
|
got := buildConnections(in)
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected 1 connection, got %d", len(got))
|
|
}
|
|
|
|
fleet := got[0].Fleet
|
|
if fleet.ConfigDrift == nil ||
|
|
fleet.ConfigDrift.Status != fleetConfigDriftNotApplicable ||
|
|
fleet.ConfigDrift.Desired != nil ||
|
|
fleet.ConfigDrift.Applied != nil {
|
|
t.Fatalf("default agent config drift = %+v", fleet.ConfigDrift)
|
|
}
|
|
if fleet.Rollout == nil ||
|
|
fleet.Rollout.Status != fleetStateCurrent ||
|
|
fleet.Rollout.Stage != fleetRolloutStageApplied {
|
|
t.Fatalf("default agent rollout = %+v", fleet.Rollout)
|
|
}
|
|
}
|
|
|
|
func TestBuildConnections_AgentCommandPolicyDesiredAppliedConvergence(t *testing.T) {
|
|
now := time.Now()
|
|
desiredDisabled := desiredAgentConfig(t, ptrBool(false), nil)
|
|
desiredEnabled := desiredAgentConfig(t, ptrBool(true), nil)
|
|
in := aggregatorInputs{
|
|
hosts: []models.Host{
|
|
{
|
|
ID: "desired-disabled-applied-enabled",
|
|
Hostname: "desired-disabled-applied-enabled",
|
|
LastSeen: now,
|
|
CommandsEnabled: true,
|
|
},
|
|
{
|
|
ID: "desired-enabled-applied-disabled",
|
|
Hostname: "desired-enabled-applied-disabled",
|
|
LastSeen: now,
|
|
CommandsEnabled: false,
|
|
},
|
|
{
|
|
ID: "desired-disabled-unreported",
|
|
Hostname: "desired-disabled-unreported",
|
|
},
|
|
},
|
|
agentDesiredConfigs: map[string]connectionAgentDesiredConfig{
|
|
"desired-disabled-applied-enabled": desiredDisabled,
|
|
"desired-enabled-applied-disabled": desiredEnabled,
|
|
"desired-disabled-unreported": desiredDisabled,
|
|
},
|
|
now: now,
|
|
}
|
|
|
|
got := buildConnections(in)
|
|
byID := map[string]Connection{}
|
|
for _, connection := range got {
|
|
byID[connection.ID] = connection
|
|
}
|
|
|
|
disabledDesired := byID["agent:desired-disabled-applied-enabled"].Fleet
|
|
if disabledDesired.RemoteControl != fleetStateEnabled {
|
|
t.Fatalf("remote control for desired-disabled/applied-enabled = %q, want applied enabled", disabledDesired.RemoteControl)
|
|
}
|
|
if disabledDesired.CommandPolicy == nil ||
|
|
disabledDesired.CommandPolicy.Status != fleetStateEnabled ||
|
|
disabledDesired.CommandPolicy.Desired != fleetStateDisabled ||
|
|
disabledDesired.CommandPolicy.Applied != fleetStateEnabled ||
|
|
disabledDesired.CommandPolicy.Enforcement != fleetCommandPolicyDrifted {
|
|
t.Fatalf("desired-disabled/applied-enabled command policy = %+v", disabledDesired.CommandPolicy)
|
|
}
|
|
|
|
enabledDesired := byID["agent:desired-enabled-applied-disabled"].Fleet
|
|
if enabledDesired.RemoteControl != fleetStateDisabled {
|
|
t.Fatalf("remote control for desired-enabled/applied-disabled = %q, want applied disabled", enabledDesired.RemoteControl)
|
|
}
|
|
if enabledDesired.CommandPolicy == nil ||
|
|
enabledDesired.CommandPolicy.Status != fleetStateDisabled ||
|
|
enabledDesired.CommandPolicy.Desired != fleetStateEnabled ||
|
|
enabledDesired.CommandPolicy.Applied != fleetStateDisabled ||
|
|
enabledDesired.CommandPolicy.Enforcement != fleetCommandPolicyDrifted {
|
|
t.Fatalf("desired-enabled/applied-disabled command policy = %+v", enabledDesired.CommandPolicy)
|
|
}
|
|
|
|
unreported := byID["agent:desired-disabled-unreported"].Fleet
|
|
if unreported.RemoteControl != fleetStateUnknown {
|
|
t.Fatalf("remote control for unreported agent = %q, want unknown", unreported.RemoteControl)
|
|
}
|
|
if unreported.CommandPolicy == nil ||
|
|
unreported.CommandPolicy.Status != fleetStateUnknown ||
|
|
unreported.CommandPolicy.Desired != fleetStateDisabled ||
|
|
unreported.CommandPolicy.Applied != fleetStateUnknown ||
|
|
unreported.CommandPolicy.Enforcement != fleetStatePending {
|
|
t.Fatalf("unreported command policy = %+v", unreported.CommandPolicy)
|
|
}
|
|
}
|
|
|
|
func TestBuildConnections_AgentConfigDriftUsesCanonicalDesiredMetadataWithoutSelfComparing(t *testing.T) {
|
|
now := time.Now()
|
|
desiredConfig := desiredAgentConfig(t, ptrBool(true), map[string]interface{}{
|
|
"interval": "10s",
|
|
})
|
|
desired := *desiredConfig.Fingerprint
|
|
in := aggregatorInputs{
|
|
hosts: []models.Host{
|
|
{
|
|
ID: "agent-1",
|
|
Hostname: "agent-1",
|
|
LastSeen: now,
|
|
CommandsEnabled: false,
|
|
DiskExclude: []string{"/dev/loop*"},
|
|
},
|
|
},
|
|
agentDesiredConfigs: map[string]connectionAgentDesiredConfig{
|
|
"agent-1": desiredConfig,
|
|
},
|
|
now: now,
|
|
}
|
|
|
|
got := buildConnections(in)
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected 1 connection, got %d", len(got))
|
|
}
|
|
drift := got[0].Fleet.ConfigDrift
|
|
if drift == nil {
|
|
t.Fatal("expected agent config drift metadata")
|
|
}
|
|
if drift.Status != fleetStatePending {
|
|
t.Fatalf("config drift status = %q, want pending", drift.Status)
|
|
}
|
|
if drift.Desired == nil || *drift.Desired != desired {
|
|
t.Fatalf("desired config drift fingerprint = %+v, want %+v", drift.Desired, desired)
|
|
}
|
|
if drift.Applied != nil {
|
|
t.Fatalf("applied config fingerprint should be absent until agent reports a comparable fingerprint, got %+v", drift.Applied)
|
|
}
|
|
|
|
selfCompared := connectionConfigFingerprint(connectionAgentConfigFingerprintVersion, map[string]any{
|
|
"commandsEnabled": false,
|
|
"diskExclude": []string{"/dev/loop*"},
|
|
})
|
|
if selfCompared == nil {
|
|
t.Fatal("expected local self-comparison fingerprint to be derivable")
|
|
}
|
|
if drift.Desired.Hash == selfCompared.Hash {
|
|
t.Fatalf("desired config hash reused report-field fingerprint %q", drift.Desired.Hash)
|
|
}
|
|
if got[0].Fleet.Rollout == nil || got[0].Fleet.Rollout.Status == fleetStateCurrent {
|
|
t.Fatalf("rollout should not claim current without an applied config comparison, got %+v", got[0].Fleet.Rollout)
|
|
}
|
|
}
|
|
|
|
func TestConnectionAgentDesiredConfigFingerprintsSkipsEmptyDefaultConfig(t *testing.T) {
|
|
monitor, err := monitoring.New(&config.Config{DataPath: t.TempDir()})
|
|
if err != nil {
|
|
t.Fatalf("monitoring.New: %v", err)
|
|
}
|
|
t.Cleanup(func() { monitor.Stop() })
|
|
|
|
hostID := "default-agent"
|
|
got := connectionAgentDesiredConfigFingerprints(monitor, []models.Host{{ID: hostID}})
|
|
desired, ok := got[hostID]
|
|
if !ok {
|
|
t.Fatalf("expected resolved desired config entry for %q, got %+v", hostID, got)
|
|
}
|
|
if desired.Fingerprint != nil {
|
|
t.Fatalf("empty default config should not create rollout fingerprint, got %+v", desired.Fingerprint)
|
|
}
|
|
if desired.CommandsEnabled != nil {
|
|
t.Fatalf("empty default config should not set command override, got %+v", desired.CommandsEnabled)
|
|
}
|
|
|
|
commandsEnabled := true
|
|
if err := monitor.UpdateHostAgentConfig(hostID, &commandsEnabled); err != nil {
|
|
t.Fatalf("UpdateHostAgentConfig: %v", err)
|
|
}
|
|
got = connectionAgentDesiredConfigFingerprints(monitor, []models.Host{{ID: hostID}})
|
|
desired = got[hostID]
|
|
if desired.Fingerprint == nil {
|
|
t.Fatalf("managed command override should create desired config fingerprint")
|
|
}
|
|
if desired.CommandsEnabled == nil || !*desired.CommandsEnabled {
|
|
t.Fatalf("managed command override = %+v, want true", desired.CommandsEnabled)
|
|
}
|
|
}
|
|
|
|
func TestConnectionFleetAgentConfigDriftComparesAppliedFingerprintsWhenAvailable(t *testing.T) {
|
|
now := time.Now()
|
|
conn := Connection{
|
|
Type: ConnectionTypeAgent,
|
|
State: ConnectionStateActive,
|
|
Enabled: true,
|
|
LastSeen: &now,
|
|
}
|
|
desired := &ConnectionFleetConfigFingerprint{Version: connectionAgentConfigFingerprintVersion, Hash: "sha256:desired"}
|
|
applied := &ConnectionFleetConfigFingerprint{Version: connectionAgentConfigFingerprintVersion, Hash: "sha256:applied"}
|
|
|
|
drifted := connectionFleetAgentConfigDriftForFingerprints(conn, desired, applied)
|
|
if drifted.Status != fleetConfigDriftDrifted ||
|
|
drifted.Desired != desired ||
|
|
drifted.Applied != applied ||
|
|
drifted.LastObservedAt == nil {
|
|
t.Fatalf("drifted config comparison = %+v", drifted)
|
|
}
|
|
|
|
matchingApplied := &ConnectionFleetConfigFingerprint{Version: desired.Version, Hash: desired.Hash}
|
|
current := connectionFleetAgentConfigDriftForFingerprints(conn, desired, matchingApplied)
|
|
if current.Status != fleetConfigDriftCurrent ||
|
|
current.Desired != desired ||
|
|
current.Applied != matchingApplied ||
|
|
current.LastObservedAt == nil {
|
|
t.Fatalf("current config comparison = %+v", current)
|
|
}
|
|
}
|
|
|
|
func TestBuildConnections_PlatformFleetGovernance(t *testing.T) {
|
|
now := time.Now()
|
|
lastSuccess := now.Add(-30 * time.Second)
|
|
in := aggregatorInputs{
|
|
pveInstances: []config.PVEInstance{
|
|
{Name: "healthy", Host: "https://healthy.lan:8006"},
|
|
{Name: "bad-token", Host: "https://bad-token.lan:8006"},
|
|
{Name: "paused", Host: "https://paused.lan:8006", Disabled: true},
|
|
},
|
|
instanceHealth: map[string]monitoring.InstanceHealth{
|
|
"pve::healthy": healthEntry(&lastSuccess, "", "", "closed"),
|
|
"pve::bad-token": healthEntry(&lastSuccess, "403 forbidden", "auth", "closed"),
|
|
},
|
|
now: now,
|
|
}
|
|
|
|
got := buildConnections(in)
|
|
byID := map[string]Connection{}
|
|
for _, connection := range got {
|
|
byID[connection.ID] = connection
|
|
}
|
|
|
|
healthy := byID["pve:healthy"].Fleet
|
|
if healthy.EnrollmentState != fleetStateConfigured ||
|
|
healthy.LivenessState != fleetStateActive ||
|
|
healthy.VersionDrift != fleetStateNotApplicable ||
|
|
healthy.AdapterHealth != fleetStateHealthy ||
|
|
healthy.ConfigRollout != fleetStateConfigured ||
|
|
healthy.CredentialStatus != fleetStateVerified ||
|
|
healthy.UpdateStatus != fleetStateNotApplicable ||
|
|
healthy.RemoteControl != fleetStateNotApplicable {
|
|
t.Fatalf("healthy platform fleet governance = %+v", healthy)
|
|
}
|
|
if healthy.ConfigDrift == nil ||
|
|
healthy.ConfigDrift.Status != fleetConfigDriftCurrent ||
|
|
healthy.ConfigDrift.Desired == nil ||
|
|
healthy.ConfigDrift.Applied == nil ||
|
|
healthy.ConfigDrift.Desired.Hash != healthy.ConfigDrift.Applied.Hash {
|
|
t.Fatalf("healthy platform config drift = %+v", healthy.ConfigDrift)
|
|
}
|
|
if healthy.Rollout == nil || healthy.Rollout.Status != fleetStateCurrent {
|
|
t.Fatalf("healthy platform rollout = %+v", healthy.Rollout)
|
|
}
|
|
if healthy.CredentialHealth == nil || healthy.CredentialHealth.Status != fleetStateVerified {
|
|
t.Fatalf("healthy platform credential health = %+v", healthy.CredentialHealth)
|
|
}
|
|
|
|
badToken := byID["pve:bad-token"].Fleet
|
|
if badToken.AdapterHealth != fleetStateBlocked ||
|
|
badToken.CredentialStatus != fleetStateInvalid ||
|
|
badToken.LivenessState != string(ConnectionStateUnauthorized) {
|
|
t.Fatalf("bad token fleet governance = %+v", badToken)
|
|
}
|
|
if badToken.CredentialHealth == nil ||
|
|
badToken.CredentialHealth.Status != fleetStateInvalid ||
|
|
badToken.CredentialHealth.LastFailedAt == nil {
|
|
t.Fatalf("bad token credential health = %+v", badToken.CredentialHealth)
|
|
}
|
|
if badToken.Rollout == nil || badToken.Rollout.Status != fleetRolloutBlocked {
|
|
t.Fatalf("bad token rollout = %+v", badToken.Rollout)
|
|
}
|
|
|
|
paused := byID["pve:paused"].Fleet
|
|
if paused.EnrollmentState != fleetStatePaused ||
|
|
paused.AdapterHealth != fleetStatePaused ||
|
|
paused.ConfigRollout != fleetStatePaused ||
|
|
paused.CredentialStatus != fleetStatePaused {
|
|
t.Fatalf("paused platform fleet governance = %+v", paused)
|
|
}
|
|
if paused.ConfigDrift == nil || paused.ConfigDrift.Status != fleetStatePaused {
|
|
t.Fatalf("paused config drift = %+v", paused.ConfigDrift)
|
|
}
|
|
if paused.Rollout == nil || paused.Rollout.Status != fleetStatePaused {
|
|
t.Fatalf("paused rollout = %+v", paused.Rollout)
|
|
}
|
|
}
|
|
|
|
func TestBuildConnections_VMwareAndTrueNASEnabledFlag(t *testing.T) {
|
|
in := aggregatorInputs{
|
|
vmwareInstances: []config.VMwareVCenterInstance{{
|
|
ID: "vc1", Name: "vc", Host: "vc.lan", Enabled: false,
|
|
MonitorVMs: true, MonitorHosts: true, MonitorDatastores: false,
|
|
}},
|
|
truenasInstances: []config.TrueNASInstance{{
|
|
ID: "tn1", Name: "tn", Host: "tn.lan", Enabled: true, UseHTTPS: true,
|
|
MonitorDatasets: true, MonitorPools: false, MonitorReplication: true,
|
|
}},
|
|
now: time.Now(),
|
|
}
|
|
got := buildConnections(in)
|
|
var vmw, tn Connection
|
|
for _, c := range got {
|
|
switch c.Type {
|
|
case ConnectionTypeVMware:
|
|
vmw = c
|
|
case ConnectionTypeTrueNAS:
|
|
tn = c
|
|
}
|
|
}
|
|
if vmw.State != ConnectionStatePaused || vmw.Enabled {
|
|
t.Fatalf("vmware with Enabled=false should be paused, got state=%q enabled=%v", vmw.State, vmw.Enabled)
|
|
}
|
|
if !vmw.Capabilities.SupportsScope {
|
|
t.Fatal("vmware connections must advertise scope capability")
|
|
}
|
|
if vmw.Scope["datastores"] || !vmw.Scope["vms"] || !vmw.Scope["hosts"] {
|
|
t.Fatalf("vmware scope map should reflect Monitor* fields, got %+v", vmw.Scope)
|
|
}
|
|
if tn.State != ConnectionStatePending {
|
|
t.Fatalf("truenas with no health yet should be pending, got %q", tn.State)
|
|
}
|
|
if !tn.Enabled {
|
|
t.Fatal("truenas with Enabled=true should surface enabled=true")
|
|
}
|
|
if !tn.Capabilities.SupportsScope {
|
|
t.Fatal("truenas connections must advertise scope capability")
|
|
}
|
|
if tn.Scope["pools"] || !tn.Scope["datasets"] || !tn.Scope["replication"] {
|
|
t.Fatalf("truenas scope map should reflect Monitor* fields, got %+v", tn.Scope)
|
|
}
|
|
}
|
|
|
|
func TestBuildConnections_UsesHealthLookup(t *testing.T) {
|
|
now := time.Now()
|
|
ls := now.Add(-15 * time.Second)
|
|
in := aggregatorInputs{
|
|
pveInstances: []config.PVEInstance{{Name: "pve1", Host: "https://pve1.lan:8006"}},
|
|
instanceHealth: map[string]monitoring.InstanceHealth{
|
|
"pve::pve1": healthEntry(&ls, "", "", "closed"),
|
|
},
|
|
now: now,
|
|
}
|
|
got := buildConnections(in)
|
|
if got[0].State != ConnectionStateActive {
|
|
t.Fatalf("expected active, got %q", got[0].State)
|
|
}
|
|
if got[0].LastSeen == nil || !got[0].LastSeen.Equal(ls) {
|
|
t.Fatalf("lastSeen not propagated: %+v", got[0].LastSeen)
|
|
}
|
|
}
|