mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 20:52:51 +00:00
134f2e5ada
Every accepted agent report refreshed the canonical resource store, and every refresh covers the whole estate, so refresh work grew with agent count times estate size (#2199). The previous commit made each pass linear and cheaper; this bounds how many passes reports can trigger. Accepted host, Docker and Kubernetes reports, including rejected-report liveness, now refresh through a throttle. The first report after a quiet window is published before its handler returns. Reports within 2 seconds of the previous refresh fold into one trailing refresh at the window's end, which reads the latest state and broadcasts it. Two seconds matches readPathRegistryFreshness, the staleness read paths already accept. Agent removal and host-agent evaluation still refresh immediately. Monitor.Stop cancels a pending trailing refresh and waits for a running one before closing stores. Monitors built without New keep a zero window and refresh on every report. At 2,080 resources a refresh costs about 82 ms, so 200 agents reporting every 30 seconds went from roughly 55% of a core in report-driven refreshes to at most about 4%, however many agents report. This relaxes the agent-lifecycle guarantee that a report is visible when its handler returns: it is now visible within the window. The founder approved that trade on 24 September 2026 and the contract records it. The issue-1753 end-to-end test posts two reports back to back, so it now waits for the published rows instead of reading them at once. Refs #2199
144 lines
5.6 KiB
Go
144 lines
5.6 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"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/unifiedresources"
|
|
agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host"
|
|
)
|
|
|
|
// Issue #1753 crossed three runtime boundaries: report authentication, host to
|
|
// provider-node linking, and canonical presentation. Keep those boundaries in
|
|
// one regression test so two valid agents from independent sites cannot be
|
|
// mistaken for an auth failure or collapsed merely because both PVE machines
|
|
// report the same native short hostname.
|
|
func TestIssue1753SameNameProxmoxAgentsAuthenticateAndStayDistinctEndToEnd(t *testing.T) {
|
|
const (
|
|
stagingToken = "issue-1753-staging-agent-token.12345678"
|
|
productionToken = "issue-1753-production-agent-token.12345678"
|
|
)
|
|
stagingRecord := newTokenRecord(t, stagingToken, []string{config.ScopeAgentReport}, nil)
|
|
productionRecord := newTokenRecord(t, productionToken, []string{config.ScopeAgentReport}, nil)
|
|
|
|
dataPath := t.TempDir()
|
|
cfg := &config.Config{
|
|
DataPath: dataPath,
|
|
ConfigPath: dataPath,
|
|
APITokens: []config.APITokenRecord{stagingRecord, productionRecord},
|
|
}
|
|
monitor, err := monitoring.New(cfg)
|
|
if err != nil {
|
|
t.Fatalf("monitoring.New: %v", err)
|
|
}
|
|
t.Cleanup(monitor.Stop)
|
|
|
|
now := time.Date(2026, 9, 1, 10, 0, 0, 0, time.UTC)
|
|
monitorState(t, monitor).UpdateNodes([]models.Node{
|
|
{
|
|
ID: "staging-pve", NodeIdentity: "staging-pve", Name: "pve",
|
|
DisplayName: "Staging", Instance: "staging", Host: "https://pve.staging.example:8006",
|
|
Status: "online", LastSeen: now,
|
|
NetworkInterfaces: []models.HostNetworkInterface{{Name: "vmbr0", Addresses: []string{"192.0.2.11/24"}}},
|
|
},
|
|
{
|
|
ID: "production-pve", NodeIdentity: "production-pve", Name: "pve",
|
|
DisplayName: "Production", Instance: "production", Host: "https://pve.production.example:8006",
|
|
Status: "online", LastSeen: now,
|
|
NetworkInterfaces: []models.HostNetworkInterface{{Name: "vmbr0", Addresses: []string{"198.51.100.21/24"}}},
|
|
},
|
|
})
|
|
|
|
router := NewRouter(cfg, monitor, nil, nil, func() error { return nil }, "6.4.2")
|
|
t.Cleanup(router.shutdownBackgroundWorkers)
|
|
|
|
postReport := func(rawToken, agentID, machineID, address string, at time.Time) {
|
|
t.Helper()
|
|
report := agentshost.Report{
|
|
Agent: agentshost.AgentInfo{ID: agentID, Version: "6.4.2", Type: "unified"},
|
|
Host: agentshost.HostInfo{
|
|
ID: machineID, MachineID: machineID, Hostname: "pve",
|
|
Platform: "linux", OSName: "Proxmox VE",
|
|
},
|
|
Network: []agentshost.NetworkInterface{{Name: "vmbr0", Addresses: []string{address}}},
|
|
Timestamp: at,
|
|
}
|
|
body, err := json.Marshal(report)
|
|
if err != nil {
|
|
t.Fatalf("marshal report: %v", err)
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/agents/agent/report", bytes.NewReader(body))
|
|
req.Header.Set("Authorization", "Bearer "+rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("agent %q report status = %d, want 200: %s", agentID, rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
postReport(stagingToken, "agent-staging", "machine-staging", "192.0.2.11/24", now.Add(time.Second))
|
|
postReport(productionToken, "agent-production", "machine-production", "198.51.100.21/24", now.Add(2*time.Second))
|
|
|
|
snapshot := monitor.GetLiveStateSnapshot()
|
|
if len(snapshot.Hosts) != 2 || len(snapshot.Nodes) != 2 {
|
|
t.Fatalf("live topology = %d hosts / %d nodes, want 2 / 2: %+v", len(snapshot.Hosts), len(snapshot.Nodes), snapshot)
|
|
}
|
|
linkedByInstance := make(map[string]string, len(snapshot.Nodes))
|
|
for _, node := range snapshot.Nodes {
|
|
linkedByInstance[node.Instance] = node.LinkedAgentID
|
|
}
|
|
if linkedByInstance["staging"] != "machine-staging" || linkedByInstance["production"] != "machine-production" {
|
|
t.Fatalf("provider links = %+v, want each site linked to its own authenticated agent", linkedByInstance)
|
|
}
|
|
|
|
wantRows := map[string]struct {
|
|
agentID string
|
|
name string
|
|
}{
|
|
"staging": {agentID: "machine-staging", name: "Staging"},
|
|
"production": {agentID: "machine-production", name: "Production"},
|
|
}
|
|
presentationRows := func() ([]unifiedresources.Resource, map[string]unifiedresources.Resource) {
|
|
resources, _ := monitor.UnifiedResourceSnapshot()
|
|
byInstance := make(map[string]unifiedresources.Resource)
|
|
for _, resource := range resources {
|
|
if resource.Proxmox != nil && resource.Proxmox.NodeName != "" {
|
|
byInstance[resource.Proxmox.Instance] = resource
|
|
}
|
|
}
|
|
return resources, byInstance
|
|
}
|
|
rowsLinked := func(byInstance map[string]unifiedresources.Resource) bool {
|
|
for instance, want := range wantRows {
|
|
resource := byInstance[instance]
|
|
if resource.Agent == nil || resource.Agent.AgentID != want.agentID {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
// Reports inside one refresh window are published together by a trailing
|
|
// refresh (agent-lifecycle contract, #2199), so wait for the canonical rows.
|
|
resources, byInstance := presentationRows()
|
|
for deadline := time.Now().Add(5 * time.Second); !rowsLinked(byInstance) && time.Now().Before(deadline); {
|
|
time.Sleep(20 * time.Millisecond)
|
|
resources, byInstance = presentationRows()
|
|
}
|
|
if len(byInstance) != 2 {
|
|
t.Fatalf("presentation provider rows = %d, want 2: %+v", len(byInstance), resources)
|
|
}
|
|
for instance, want := range wantRows {
|
|
resource := byInstance[instance]
|
|
if resource.Agent == nil || resource.Agent.AgentID != want.agentID || resource.Name != want.name {
|
|
t.Fatalf("%s presentation row = %+v, want agent %q and name %q", instance, resource, want.agentID, want.name)
|
|
}
|
|
}
|
|
}
|