mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
fix(mock): keep real configured sources out of the mock connections ledger
Mock mode never initialises real PVE/PBS/PMG clients and does not run the platform pollers, so every configured real source sat in the connections ledger at "awaiting first poll" forever. That published real connection names and addresses through /api/connections while the rest of the payload was authored fixtures, and surfaced them on monitoring copy: the Proxmox workloads empty state rendered "Collection pending: minipc" next to three mock nodes. /api/config/nodes already substitutes mock entries wholesale in mock mode and rejects node mutations outright, so the ledger was the one surface that had not been brought in line. Move the mock-mode input shaping into applyMockLedgerInputs in platform_mock_connections.go, which already owns the mock vSphere, TrueNAS, and availability ledger fixtures, and drop the config and persistence derived sources there. PULSE_MOCK_KEEP_REAL_POLLING keeps the previous behaviour, since those sources do collect under it. Proof is at the payload level rather than the aggregator inputs: the new handler test asserts the served /api/connections body contains no real source by name or address, and was verified red before the fix. Contract-Neutral: agent-lifecycle is named only by the broad internal/api/ Extension Points prefix and this change does not move that boundary: agent rows come from the monitor hosts snapshot, which is untouched. The api-contracts and storage-recovery deltas cover the boundaries actually moved. No payload field was added or changed, so the backend-API-payload proof list does not apply; the handler-level payload proof is TestConnectionsHandleListDropsRealSourcesInMockMode in internal/api/connections_handlers_mock_test.go, verified red without the fix.
This commit is contained in:
@@ -1971,6 +1971,16 @@ a new API state machine, queue contract, or verification-accounting field.
|
||||
only a fetch lifecycle rule; it must not synthesize rows, downgrade backend
|
||||
fleet state, or replace the shared connection projection with page-local
|
||||
placeholders.
|
||||
In mock mode that ledger is a clean room, composed in
|
||||
`internal/api/platform_mock_connections.go`: real configured PVE, PBS, PMG,
|
||||
vSphere, TrueNAS, and availability sources are dropped from the aggregator
|
||||
inputs and only authored fixtures compose the payload, matching the
|
||||
substitution `/api/config/nodes` already performs. Real clients are never
|
||||
initialised while mock mode is on, so retaining those rows would publish
|
||||
real connection names and addresses through an otherwise synthetic payload
|
||||
and report a collection state that mock mode itself suspended. The single
|
||||
exception is the `PULSE_MOCK_KEEP_REAL_POLLING` opt-in, where the configured
|
||||
sources genuinely do collect and remain in the ledger.
|
||||
2b. Route agentless availability target kind changes through
|
||||
`internal/api/availability_handlers.go`,
|
||||
`internal/api/platform_mock_connections.go`,
|
||||
|
||||
@@ -1864,6 +1864,14 @@ must not treat starter
|
||||
data remains inventory-only context and must not be treated as proof of
|
||||
restore capability, recovery artifacts, or widened platform recovery
|
||||
support.
|
||||
The mock connections ledger composed in
|
||||
`internal/api/platform_mock_connections.go` is bounded the same way: while
|
||||
mock mode is on, real configured TrueNAS and vSphere sources are dropped
|
||||
from the aggregator inputs and only authored fixtures compose the storage
|
||||
source rows, so a demo payload never advertises a real storage appliance
|
||||
that mock mode has suspended from collection. The
|
||||
`PULSE_MOCK_KEEP_REAL_POLLING` opt-in is the single exception, because
|
||||
those sources genuinely do collect under it.
|
||||
13. Keep runtime mock platform context derived from one shared fixture graph.
|
||||
When shared `internal/api/` and monitoring wiring surface mock
|
||||
storage/recovery-adjacent inventory or recovery artifacts, that data must
|
||||
|
||||
@@ -71,18 +71,7 @@ func buildAggregatorInputsWithRuntimeSources(
|
||||
inputs.availabilityStatuses = map[string]monitoring.AvailabilityProbeStatus{}
|
||||
}
|
||||
if mock.IsMockEnabled() {
|
||||
mockTargets, mockStatuses := mockAvailabilityConnectionInputs()
|
||||
inputs.availabilityTargets = mergeAvailabilityTargets(inputs.availabilityTargets, mockTargets)
|
||||
inputs.availabilityStatuses = mergeAvailabilityStatuses(inputs.availabilityStatuses, mockStatuses)
|
||||
// Mock vSphere/TrueNAS pollers feed the fabric but never persistence,
|
||||
// so without these the mock ledger has no platform source rows for
|
||||
// the machines those integrations monitor.
|
||||
if len(inputs.vmwareInstances) == 0 {
|
||||
inputs.vmwareInstances, inputs.vmwareSummaries = mockVMwareLedgerInputs()
|
||||
}
|
||||
if len(inputs.truenasInstances) == 0 {
|
||||
inputs.truenasInstances, inputs.truenasSummaries = mockTrueNASLedgerInputs()
|
||||
}
|
||||
inputs = applyMockLedgerInputs(inputs)
|
||||
}
|
||||
inputs.expectedAgentVersion = currentAgentTargetVersion()
|
||||
_ = ctx
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
@@ -68,3 +69,73 @@ func TestConnectionsHandleListIncludesMockAvailabilityTargets(t *testing.T) {
|
||||
t.Fatalf("expected availability error metadata, got %+v", door.LastError)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConnectionsHandleListDropsRealSourcesInMockMode asserts the served
|
||||
// /api/connections payload, not just the aggregator inputs: mock mode is a
|
||||
// clean room, so no real configured source may reach the wire, by name or by
|
||||
// address.
|
||||
func TestConnectionsHandleListDropsRealSourcesInMockMode(t *testing.T) {
|
||||
previous := mock.IsMockEnabled()
|
||||
if err := mock.SetEnabled(true); err != nil {
|
||||
t.Fatalf("enable mock mode: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = mock.SetEnabled(previous) })
|
||||
|
||||
cfg := &config.Config{
|
||||
PVEInstances: []config.PVEInstance{{Name: "minipc", Host: "https://minipc:8006"}},
|
||||
PBSInstances: []config.PBSInstance{{Name: "backup-vault", Host: "https://backup-vault:8007"}},
|
||||
PMGInstances: []config.PMGInstance{{Name: "mail-relay", Host: "https://mail-relay:8006"}},
|
||||
}
|
||||
|
||||
handler := NewConnectionsHandlers(
|
||||
func(context.Context) *config.Config { return cfg },
|
||||
func(context.Context) *config.ConfigPersistence { return nil },
|
||||
func(context.Context) *monitoring.Monitor { return nil },
|
||||
)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/connections", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.HandleList(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("HandleList status = %d, body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
body := rec.Body.String()
|
||||
for _, secret := range []string{"minipc", "backup-vault", "mail-relay"} {
|
||||
if strings.Contains(body, secret) {
|
||||
t.Fatalf("real source %q leaked into the mock connections payload: %s", secret, body)
|
||||
}
|
||||
}
|
||||
|
||||
var response ConnectionsListResponse
|
||||
if err := json.NewDecoder(strings.NewReader(body)).Decode(&response); err != nil {
|
||||
t.Fatalf("decode connections response: %v", err)
|
||||
}
|
||||
for _, conn := range response.Connections {
|
||||
switch conn.Type {
|
||||
case ConnectionTypePVE, ConnectionTypePBS, ConnectionTypePMG:
|
||||
t.Fatalf("mock ledger must not carry configured platform rows, got %+v", conn)
|
||||
}
|
||||
}
|
||||
if len(response.Connections) == 0 {
|
||||
t.Fatal("mock ledger must still compose authored fixture rows")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectionsLedgerKeepsRealSourcesWhenRealPollingRetained(t *testing.T) {
|
||||
previous := mock.IsMockEnabled()
|
||||
if err := mock.SetEnabled(true); err != nil {
|
||||
t.Fatalf("enable mock mode: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = mock.SetEnabled(previous) })
|
||||
t.Setenv("PULSE_MOCK_KEEP_REAL_POLLING", "true")
|
||||
|
||||
cfg := &config.Config{
|
||||
PVEInstances: []config.PVEInstance{{Name: "minipc", Host: "https://minipc:8006"}},
|
||||
}
|
||||
|
||||
inputs := buildAggregatorInputsWithRuntimeSources(context.Background(), cfg, nil, nil, aggregatorRuntimeSources{})
|
||||
if len(inputs.pveInstances) != 1 {
|
||||
t.Fatalf("real polling opt-in must keep configured sources, got %+v", inputs.pveInstances)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
@@ -9,6 +11,60 @@ import (
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/truenas"
|
||||
)
|
||||
|
||||
// mockKeepsRealPolling reports whether the operator opted real PVE/PBS/PMG
|
||||
// polling back in while mock mode is enabled. The monitor reads the same
|
||||
// variable to decide whether to build real clients at all (see
|
||||
// keepRealPollingInMockMode in internal/monitoring/monitor.go); the ledger
|
||||
// needs the same answer to tell a source that mock mode suspended apart from
|
||||
// one that is genuinely still waiting on its first poll.
|
||||
func mockKeepsRealPolling() bool {
|
||||
switch strings.TrimSpace(strings.ToLower(os.Getenv("PULSE_MOCK_KEEP_REAL_POLLING"))) {
|
||||
case "1", "true", "yes", "on":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// applyMockLedgerInputs shapes the connections aggregator inputs for mock mode.
|
||||
//
|
||||
// Mock mode is a clean room: real PVE/PBS/PMG clients are never initialised
|
||||
// while it is on and the platform pollers do not run, so every configured real
|
||||
// source can only sit at "awaiting first poll" forever. Leaving those rows in
|
||||
// the ledger publishes real connection names and addresses through a payload
|
||||
// that is otherwise entirely authored fixtures, and reports a collection state
|
||||
// that mock mode itself suspended. /api/config/nodes already substitutes mock
|
||||
// entries wholesale and rejects node mutations outright, so the ledger is held
|
||||
// to the same rule. The one exception is the real-polling opt-in, where the
|
||||
// configured sources genuinely do collect and belong in the ledger.
|
||||
func applyMockLedgerInputs(inputs aggregatorInputs) aggregatorInputs {
|
||||
if !mockKeepsRealPolling() {
|
||||
inputs.pveInstances = nil
|
||||
inputs.pbsInstances = nil
|
||||
inputs.pmgInstances = nil
|
||||
inputs.vmwareInstances = nil
|
||||
inputs.vmwareSummaries = nil
|
||||
inputs.truenasInstances = nil
|
||||
inputs.truenasSummaries = nil
|
||||
inputs.availabilityTargets = nil
|
||||
}
|
||||
|
||||
mockTargets, mockStatuses := mockAvailabilityConnectionInputs()
|
||||
inputs.availabilityTargets = mergeAvailabilityTargets(inputs.availabilityTargets, mockTargets)
|
||||
inputs.availabilityStatuses = mergeAvailabilityStatuses(inputs.availabilityStatuses, mockStatuses)
|
||||
|
||||
// Mock vSphere/TrueNAS pollers feed the fabric but never persistence, so
|
||||
// without these the mock ledger has no platform source rows for the
|
||||
// machines those integrations monitor.
|
||||
if len(inputs.vmwareInstances) == 0 {
|
||||
inputs.vmwareInstances, inputs.vmwareSummaries = mockVMwareLedgerInputs()
|
||||
}
|
||||
if len(inputs.truenasInstances) == 0 {
|
||||
inputs.truenasInstances, inputs.truenasSummaries = mockTrueNASLedgerInputs()
|
||||
}
|
||||
return inputs
|
||||
}
|
||||
|
||||
func mockTrueNASConnectionResponses() []trueNASConnectionResponse {
|
||||
fixture := mock.DefaultTrueNASConnectionFixture()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user