mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Preserve agent rows across monitor reloads
This commit is contained in:
@@ -238,6 +238,12 @@ back into the canonical read state through the unified-resources-owned overlay
|
||||
path instead of rebuilding registry truth locally. A server restart or v6
|
||||
upgrade must not briefly forget an already admitted standalone host and
|
||||
misclassify its next report as a brand-new counted system.
|
||||
That same standalone-host continuity boundary also owns host snapshot and
|
||||
connection-list continuity during monitor reloads. `internal/monitoring/monitor.go`
|
||||
must apply the same continuity overlay when `HostsSnapshot()` resolves its
|
||||
canonical read state, so settings and other host-list consumers do not blank
|
||||
previously admitted Pulse Agent rows during a config-driven monitor swap while
|
||||
fresh reports are still in flight.
|
||||
That same mock-runtime boundary also owns freshness while demos are running.
|
||||
The mock update loop must keep provider-backed TrueNAS and VMware records plus
|
||||
legacy PBS and PMG summaries on current `LastSeen` and health state each tick,
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
||||
agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host"
|
||||
)
|
||||
|
||||
func TestConnectionsHandleListIncludesContinuityBackedHostAgents(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := &config.Config{DataPath: dir}
|
||||
now := time.Now().UTC()
|
||||
|
||||
seedMonitor, err := monitoring.New(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("monitoring.New seed monitor: %v", err)
|
||||
}
|
||||
t.Cleanup(seedMonitor.Stop)
|
||||
|
||||
_, err = seedMonitor.ApplyHostReport(agentshost.Report{
|
||||
Agent: agentshost.AgentInfo{
|
||||
ID: "agent-1",
|
||||
Version: "6.0.0-rc.1",
|
||||
IntervalSeconds: 30,
|
||||
},
|
||||
Host: agentshost.HostInfo{
|
||||
ID: "machine-1",
|
||||
MachineID: "machine-1",
|
||||
Hostname: "host-1.local",
|
||||
Platform: "linux",
|
||||
},
|
||||
Timestamp: now,
|
||||
}, &config.APITokenRecord{ID: "token-1", Name: "Token One"})
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyHostReport seed continuity: %v", err)
|
||||
}
|
||||
|
||||
reloadedMonitor, err := monitoring.New(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("monitoring.New reloaded monitor: %v", err)
|
||||
}
|
||||
t.Cleanup(reloadedMonitor.Stop)
|
||||
|
||||
handler := NewConnectionsHandlers(
|
||||
func(context.Context) *config.Config { return cfg },
|
||||
func(context.Context) *config.ConfigPersistence { return nil },
|
||||
func(context.Context) *monitoring.Monitor { return reloadedMonitor },
|
||||
)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/connections", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.HandleList(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var resp ConnectionsListResponse
|
||||
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("decode connections response: %v", err)
|
||||
}
|
||||
if len(resp.Connections) != 1 {
|
||||
t.Fatalf("expected 1 continuity-backed connection, got %d", len(resp.Connections))
|
||||
}
|
||||
|
||||
conn := resp.Connections[0]
|
||||
if conn.ID != "agent:machine-1" {
|
||||
t.Fatalf("connection id = %q, want %q", conn.ID, "agent:machine-1")
|
||||
}
|
||||
if conn.Type != ConnectionTypeAgent {
|
||||
t.Fatalf("connection type = %q, want %q", conn.Type, ConnectionTypeAgent)
|
||||
}
|
||||
if conn.Name != "host-1.local" || conn.Address != "host-1.local" {
|
||||
t.Fatalf("unexpected continuity-backed connection identity: name=%q address=%q", conn.Name, conn.Address)
|
||||
}
|
||||
if conn.State != ConnectionStateActive {
|
||||
t.Fatalf("connection state = %q, want %q", conn.State, ConnectionStateActive)
|
||||
}
|
||||
if conn.Source != ConnectionSourceAgent {
|
||||
t.Fatalf("connection source = %q, want %q", conn.Source, ConnectionSourceAgent)
|
||||
}
|
||||
if conn.LastSeen == nil || conn.LastSeen.IsZero() {
|
||||
t.Fatalf("expected continuity-backed connection to carry lastSeen, got %#v", conn.LastSeen)
|
||||
}
|
||||
}
|
||||
@@ -125,6 +125,8 @@ func TestMonitoredSystemUsageReadinessGuardrailsRemainCanonical(t *testing.T) {
|
||||
"SupplementalInventoryReadyAt(m *Monitor, orgID string) (time.Time, bool)",
|
||||
"hostContinuityStore *config.HostContinuityStore",
|
||||
"hostContinuityStore: config.NewHostContinuityStore(cfg.DataPath, nil),",
|
||||
"func (m *Monitor) HostsSnapshot() []models.Host {",
|
||||
"readState = m.readStateWithStandaloneHostContinuity(readState)",
|
||||
},
|
||||
"monitored_system_usage.go": {
|
||||
"MonitoredSystemUsageUnavailableMonitorState",
|
||||
@@ -136,10 +138,10 @@ func TestMonitoredSystemUsageReadinessGuardrailsRemainCanonical(t *testing.T) {
|
||||
"UnavailableReason: MonitoredSystemUsageUnavailableSupplementalInventoryUnsettled,",
|
||||
"if freshness.IsZero() || freshness.Before(readyAt) {",
|
||||
"UnavailableReason: MonitoredSystemUsageUnavailableSupplementalInventoryRebuildPending,",
|
||||
"readState = m.monitoredSystemUsageReadStateWithHostContinuity(readState)",
|
||||
"readState = m.readStateWithStandaloneHostContinuity(readState)",
|
||||
"Count: unifiedresources.MonitoredSystemCount(readState),",
|
||||
"Available: true,",
|
||||
"func (m *Monitor) monitoredSystemUsageReadStateWithHostContinuity(",
|
||||
"func (m *Monitor) readStateWithStandaloneHostContinuity(",
|
||||
"return unifiedresources.ReadStateWithRecords(readState, unifiedresources.SourceAgent, records)",
|
||||
},
|
||||
"truenas_poller.go": {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
unifiedresources "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
||||
)
|
||||
@@ -795,6 +796,44 @@ func TestMonitorHostsSnapshot(t *testing.T) {
|
||||
t.Fatalf("expected empty hosts from live canonical read-state, got %#v", hosts)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("includes recent standalone host continuity when live read state is empty", func(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
store := config.NewHostContinuityStore(t.TempDir(), nil)
|
||||
if err := store.Upsert(config.HostContinuityEntry{
|
||||
HostID: "host-1",
|
||||
ReportHostID: "machine-1",
|
||||
Hostname: "host-1.local",
|
||||
DisplayName: "Host One",
|
||||
MachineID: "machine-1",
|
||||
AgentVersion: "1.2.3",
|
||||
Platform: "linux",
|
||||
LastSeen: now,
|
||||
}); err != nil {
|
||||
t.Fatalf("Upsert continuity: %v", err)
|
||||
}
|
||||
|
||||
m := &Monitor{
|
||||
state: models.NewState(),
|
||||
resourceStore: unifiedresources.NewMonitorAdapter(unifiedresources.NewRegistry(nil)),
|
||||
hostContinuityStore: store,
|
||||
}
|
||||
|
||||
hosts := m.HostsSnapshot()
|
||||
if len(hosts) != 1 {
|
||||
t.Fatalf("expected one continuity-backed host, got %#v", hosts)
|
||||
}
|
||||
host := hosts[0]
|
||||
if host.ID != "host-1" || host.Hostname != "host-1.local" || host.DisplayName != "Host One" {
|
||||
t.Fatalf("unexpected continuity-backed host identity: %#v", host)
|
||||
}
|
||||
if host.MachineID != "machine-1" || host.AgentVersion != "1.2.3" || host.Platform != "linux" {
|
||||
t.Fatalf("unexpected continuity-backed host metadata: %#v", host)
|
||||
}
|
||||
if !host.LastSeen.Equal(now) {
|
||||
t.Fatalf("continuity-backed host lastSeen = %v, want %v", host.LastSeen, now)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonitorDockerHostsSnapshot(t *testing.T) {
|
||||
|
||||
@@ -2776,6 +2776,7 @@ func (m *Monitor) HostsSnapshot() []models.Host {
|
||||
if readState == nil {
|
||||
return nil
|
||||
}
|
||||
readState = m.readStateWithStandaloneHostContinuity(readState)
|
||||
|
||||
hostViews := readState.Hosts()
|
||||
if len(hostViews) == 0 {
|
||||
|
||||
@@ -55,7 +55,7 @@ func (m *Monitor) MonitoredSystemUsage() MonitoredSystemUsageSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
readState = m.monitoredSystemUsageReadStateWithHostContinuity(readState)
|
||||
readState = m.readStateWithStandaloneHostContinuity(readState)
|
||||
|
||||
return MonitoredSystemUsageSnapshot{
|
||||
Count: unifiedresources.MonitoredSystemCount(readState),
|
||||
@@ -64,7 +64,7 @@ func (m *Monitor) MonitoredSystemUsage() MonitoredSystemUsageSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Monitor) monitoredSystemUsageReadStateWithHostContinuity(
|
||||
func (m *Monitor) readStateWithStandaloneHostContinuity(
|
||||
readState unifiedresources.ReadState,
|
||||
) unifiedresources.ReadState {
|
||||
if m == nil || readState == nil {
|
||||
|
||||
Reference in New Issue
Block a user