monitoring: keep host IDs stable via token+hostname binding

This commit is contained in:
rcourtman 2025-12-17 20:16:27 +00:00
parent 93ac3232c8
commit 71940ae6e3
2 changed files with 63 additions and 47 deletions

View file

@ -1977,58 +1977,61 @@ func (m *Monitor) ApplyHostReport(report agentshost.Report, tokenRecord *config.
identifier := baseIdentifier
if tokenRecord != nil && strings.TrimSpace(tokenRecord.ID) != "" {
tokenID := strings.TrimSpace(tokenRecord.ID)
bindingID := baseIdentifier
for _, candidate := range existingHosts {
if candidate.ID != bindingID {
continue
}
if strings.TrimSpace(candidate.Hostname) == hostname && strings.TrimSpace(candidate.TokenID) == tokenID {
break
}
seed := strings.Join([]string{tokenID, hostname, bindingID}, "|")
sum := sha1.Sum([]byte(seed))
suffix := hex.EncodeToString(sum[:4])
base := bindingID
if base == "" {
base = "host"
}
if len(base) > 40 {
base = base[:40]
}
bindingID = fmt.Sprintf("%s-%s", base, suffix)
break
}
bindingKey := fmt.Sprintf("%s:%s", tokenID, hostname)
m.mu.Lock()
if m.hostTokenBindings == nil {
m.hostTokenBindings = make(map[string]string)
}
// Bind tokens by hostname rather than agent ID. This allows:
// - Same host to reconnect after agent reinstall (agent ID changes but hostname doesn't)
// - Multiple hosts to use the same token (each hostname gets its own binding entry)
bindingKey := fmt.Sprintf("%s:%s", tokenID, hostname)
if boundID, exists := m.hostTokenBindings[bindingKey]; exists && boundID != bindingID {
log.Info().
Str("tokenID", tokenID).
Str("hostname", hostname).
Str("previousHostID", boundID).
Str("newHostID", bindingID).
Msg("Host agent identity changed for token binding; updating binding")
m.hostTokenBindings[bindingKey] = bindingID
} else if !exists {
m.hostTokenBindings[bindingKey] = bindingID
log.Debug().
Str("tokenID", tokenID).
Str("hostID", bindingID).
Str("hostname", hostname).
Msg("Bound host agent token to hostname")
}
boundID := strings.TrimSpace(m.hostTokenBindings[bindingKey])
m.mu.Unlock()
identifier = bindingID
// If we already have a binding for this token+hostname, use it to keep host IDs stable
// even if another colliding host disappears later.
if boundID != "" {
identifier = boundID
} else {
bindingID := baseIdentifier
for _, candidate := range existingHosts {
if candidate.ID != bindingID {
continue
}
if strings.TrimSpace(candidate.Hostname) == hostname && strings.TrimSpace(candidate.TokenID) == tokenID {
break
}
seed := strings.Join([]string{tokenID, hostname, bindingID}, "|")
sum := sha1.Sum([]byte(seed))
suffix := hex.EncodeToString(sum[:4])
base := bindingID
if base == "" {
base = "host"
}
if len(base) > 40 {
base = base[:40]
}
bindingID = fmt.Sprintf("%s-%s", base, suffix)
break
}
m.mu.Lock()
if m.hostTokenBindings == nil {
m.hostTokenBindings = make(map[string]string)
}
if existing := strings.TrimSpace(m.hostTokenBindings[bindingKey]); existing != "" {
identifier = existing
} else {
m.hostTokenBindings[bindingKey] = bindingID
log.Debug().
Str("tokenID", tokenID).
Str("hostID", bindingID).
Str("hostname", hostname).
Msg("Bound host agent token to hostname")
identifier = bindingID
}
m.mu.Unlock()
}
}
var previous models.Host

View file

@ -244,9 +244,22 @@ func TestApplyHostReportDisambiguatesCollidingIdentifiersAcrossTokens(t *testing
t.Fatalf("expected stable host ID for repeated reports, got %q want %q", hostTwoRepeat.ID, hostTwo.ID)
}
// Removing the first host should not cause the second host to change identity.
if _, err := monitor.RemoveHostAgent(hostOne.ID); err != nil {
t.Fatalf("RemoveHostAgent hostOne: %v", err)
}
hostTwoAfterRemoval, err := monitor.ApplyHostReport(secondReport, &config.APITokenRecord{ID: "token-two"})
if err != nil {
t.Fatalf("ApplyHostReport hostTwo after removal: %v", err)
}
if hostTwoAfterRemoval.ID != hostTwo.ID {
t.Fatalf("expected stable host ID after removal, got %q want %q", hostTwoAfterRemoval.ID, hostTwo.ID)
}
snapshot := monitor.state.GetSnapshot()
if got := len(snapshot.Hosts); got != 2 {
t.Fatalf("expected 2 hosts in state, got %d", got)
if got := len(snapshot.Hosts); got != 1 {
t.Fatalf("expected 1 host in state after removal, got %d", got)
}
}