Files
pulse/internal/monitoring/docker_shared_token_test.go
T
rcourtman 3c31aa4805 Stop Docker records flip-flopping under shared same-hostname tokens
The Docker analog of the #1753 estate was still broken: two live
machines reusing one short hostname and one pasted unified install
token collapsed into a single flip-flopping DockerHost record, because
the hostname+token identity fallback adopts a record whose machine ID
disagrees with the report's. That fold is deliberate for recreated
containers (whose /etc/machine-id regenerates), so it cannot simply be
guarded by machine-ID inequality - the discriminator is a revisit: a
recreated container transitions to its new machine ID exactly once,
while two live machines alternate. Removing the collapsed record then
revoked the shared token unconditionally, rejecting every surviving
module - host reports included, since a unified install shares one
credential - with 401 "Unauthorized access attempt".

Consult the identity flap tracker before the hostname fallbacks adopt a
machine-ID-disagreeing record: a report whose machine ID returns to a
value already seen behind that identity is a second live machine and is
not folded. The machine whose identifiers minted the record reclaims it,
so the first site keeps its record and history, and the other site falls
through to the token binding check, converging on the documented "Each
Docker / Podman module must use a unique API token" rejection instead of
silently overwriting the record every cycle. RemoveDockerHost now skips
token revocation while any host or Docker record still authenticates
with the credential, mirroring the host-agent removal guard.

Regression coverage: an end-to-end router test walking the two-site
shared-token estate (host + Docker reports, alternating cycles, removal)
asserting the first site's identity stays stable, the second site gets
the unique-token guidance, and the shared token survives removal; a
router test proving removal of a machine's Docker record keeps the
unified token its host record still uses; and a state-layer test pinning
the reclaim/no-flip-flop convergence. Recreated-container adoption and
the existing token-uniqueness rejections keep their tests unchanged.
2026-09-01 15:26:48 +01:00

100 lines
3.9 KiB
Go

package monitoring
import (
"strings"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
agentsdocker "github.com/rcourtman/pulse-go-rewrite/pkg/agents/docker"
)
func sharedTokenSiteReport(machineID string, at time.Time) agentsdocker.Report {
return agentsdocker.Report{
Agent: agentsdocker.AgentInfo{
// The unified agent's Docker module reports the machine ID as its
// agent ID (the hostagent fallback chain, #985/#986).
ID: machineID,
Version: "6.4.2",
Type: "unified",
IntervalSeconds: 30,
},
Host: agentsdocker.HostInfo{
Hostname: "docker01",
MachineID: machineID,
},
Timestamp: at,
}
}
// Docker analog of #1753: two live machines reuse one short hostname and one
// shared install token. The hostname+token fallback may fold the second
// machine's first report (indistinguishable from a recreated container until
// the first machine reports again), but the identities must not keep
// flip-flopping: the machine that minted the record reclaims it, and the other
// machine converges to the documented unique-token rejection instead of
// silently overwriting the record every cycle.
func TestApplyDockerReportSharedTokenSameHostnameDoesNotFlipFlop(t *testing.T) {
monitor := newTestMonitor(t)
token := &config.APITokenRecord{ID: "shared-install-token", Name: "Shared install"}
base := time.Now().UTC()
siteA, err := monitor.ApplyDockerReport(sharedTokenSiteReport("machine-id-site-a", base), token)
if err != nil {
t.Fatalf("site A first report: %v", err)
}
// Site B's first report is the one ambiguous cycle and may fold into
// site A's record.
if _, err := monitor.ApplyDockerReport(sharedTokenSiteReport("machine-id-site-b", base.Add(time.Second)), token); err != nil {
t.Logf("site B first report rejected immediately: %v", err)
}
// Site A's next report proves two live machines are alternating: it must
// reclaim its own record, not mint a new identity.
reclaimed, err := monitor.ApplyDockerReport(sharedTokenSiteReport("machine-id-site-a", base.Add(2*time.Second)), token)
if err != nil {
t.Fatalf("site A reclaim report: %v", err)
}
if reclaimed.ID != siteA.ID {
t.Fatalf("site A lost its record: acknowledged %q, want %q", reclaimed.ID, siteA.ID)
}
if reclaimed.MachineID != "machine-id-site-a" {
t.Fatalf("site A record carries machine ID %q after reclaim", reclaimed.MachineID)
}
// From here on, site B must be steered to a unique token, and site A must
// stay stable - the silent alternation is what collapsed the reporter's
// estate and let a removal revoke the shared credential.
for cycle := 0; cycle < 3; cycle++ {
at := base.Add(time.Duration(3+cycle*2) * time.Second)
_, err := monitor.ApplyDockerReport(sharedTokenSiteReport("machine-id-site-b", at), token)
if err == nil {
t.Fatalf("cycle %d: site B silently adopted an identity instead of the unique-token rejection", cycle)
}
if !strings.Contains(err.Error(), "unique API token") {
t.Fatalf("cycle %d: site B rejection lacks the unique-token guidance: %v", cycle, err)
}
steady, err := monitor.ApplyDockerReport(sharedTokenSiteReport("machine-id-site-a", at.Add(time.Second)), token)
if err != nil {
t.Fatalf("cycle %d: site A report rejected: %v", cycle, err)
}
if steady.ID != siteA.ID || steady.MachineID != "machine-id-site-a" {
t.Fatalf("cycle %d: site A flapped to ID %q machine %q", cycle, steady.ID, steady.MachineID)
}
}
hosts := monitor.state.GetDockerHosts()
if len(hosts) != 1 {
ids := make([]string, 0, len(hosts))
for _, h := range hosts {
ids = append(ids, h.ID+"@"+h.MachineID)
}
t.Fatalf("expected exactly site A's record to survive, got %d: %v", len(hosts), ids)
}
if hosts[0].ID != siteA.ID || hosts[0].MachineID != "machine-id-site-a" {
t.Fatalf("surviving record is %q@%q, want %q@machine-id-site-a", hosts[0].ID, hosts[0].MachineID, siteA.ID)
}
}