Keep default monitor token inventory current

This commit is contained in:
rcourtman 2026-08-16 21:54:17 +01:00
parent f6aa8db93a
commit 8dc771f09b
3 changed files with 58 additions and 4 deletions

View file

@ -2726,6 +2726,13 @@ would mutate. Distinct rows are retained so Agent Doctor does not disguise two
installations as one healthy machine. Mock mode has no authoritative token
inventory for its synthetic hosts and therefore must not turn fixture token IDs
into missing-credential incidents.
The default-org monitor retains the canonical server configuration pointer
rather than a tenant-isolation copy, so tokens minted after startup become part
of that current inventory immediately and cannot produce a false
`agent_credential_missing` diagnosis. `MultiTenantMonitor` deep-copies only
non-default tenant configuration; those tenant copies remain isolated from the
primary runtime's mutable token state.
`internal/fleethealth/agent_test.go` and
`internal/monitoring/agent_fleet_doctor_test.go` are the focused runtime proofs.

View file

@ -2630,6 +2630,48 @@ func TestTenantMonitorWiresOrgIdentityIntoNotifications(t *testing.T) {
}
}
func TestDefaultOrgMonitorSharesCanonicalRuntimeTokenInventory(t *testing.T) {
dataDir := t.TempDir()
baseCfg := &config.Config{DataPath: dataDir, ConfigPath: dataDir}
mtm := NewMultiTenantMonitor(baseCfg, config.NewMultiTenantPersistence(dataDir), nil)
t.Cleanup(mtm.Stop)
monitor, err := mtm.GetMonitor("default")
if err != nil {
t.Fatalf("GetMonitor(default) error = %v", err)
}
if monitor.GetConfig() != baseCfg {
t.Fatal("default monitor must retain the canonical server config pointer")
}
now := time.Now().UTC()
config.Mu.Lock()
baseCfg.APITokens = []config.APITokenRecord{{
ID: "fresh-agent-token",
Name: "Fresh agent token",
CreatedAt: now,
Scopes: []string{config.ScopeAgentExec},
}}
config.Mu.Unlock()
monitor.mu.Lock()
monitor.state.Hosts = []models.Host{{
ID: "agent-fresh-token",
Hostname: "fresh-token-host",
Status: "online",
LastSeen: now,
AgentVersion: "6.2.2",
TokenID: "fresh-agent-token",
CommandsEnabled: true,
}}
monitor.mu.Unlock()
diagnostics := monitor.GetAgentFleetDiagnostics("6.2.2", now)
agent := requireAgentDiagnostic(t, diagnostics, "agent-agent-fresh-token")
if diagnosticHasAnyReason(agent.Reasons, AgentFleetReasonCredentialMissing, AgentFleetReasonExecScopeMissing) {
t.Fatalf("fresh canonical token was diagnosed as unavailable: %+v", agent.Reasons)
}
}
// Instance-wide notification settings (webhook security allowlist, public
// URL) propagate through ForEachMonitor; it must visit every live tenant
// monitor so no org's manager is left observing stale security settings.

View file

@ -184,10 +184,15 @@ func (mtm *MultiTenantMonitor) GetMonitor(orgID string) (*Monitor, error) {
log.Info().Str("org_id", orgID).Msg("initializing tenant monitor")
// 1. Load Tenant Config
// Deep copy the base config to ensure tenant isolation.
// Each tenant gets its own independent config that won't share
// credential slices or other mutable state with other tenants.
tenantConfig := mtm.baseConfig.DeepCopy()
// The default org is the primary runtime and must retain the canonical
// configuration pointer owned by the server. Runtime auth mutations such as
// freshly minted agent tokens need to become visible to monitoring-backed
// diagnostics immediately. Non-default tenants remain isolated deep copies
// so their mutable state and credentials cannot alias the primary runtime.
tenantConfig := mtm.baseConfig
if orgID != "default" {
tenantConfig = mtm.baseConfig.DeepCopy()
}
// Clear inherited credentials - tenants must load their own
// This prevents credential leakage between tenants