monitoring: keep host IDs stable via token+hostname binding
This commit is contained in:
parent
93ac3232c8
commit
71940ae6e3
2 changed files with 63 additions and 47 deletions
|
|
@ -1977,58 +1977,61 @@ func (m *Monitor) ApplyHostReport(report agentshost.Report, tokenRecord *config.
|
||||||
identifier := baseIdentifier
|
identifier := baseIdentifier
|
||||||
if tokenRecord != nil && strings.TrimSpace(tokenRecord.ID) != "" {
|
if tokenRecord != nil && strings.TrimSpace(tokenRecord.ID) != "" {
|
||||||
tokenID := strings.TrimSpace(tokenRecord.ID)
|
tokenID := strings.TrimSpace(tokenRecord.ID)
|
||||||
|
bindingKey := fmt.Sprintf("%s:%s", tokenID, hostname)
|
||||||
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()
|
m.mu.Lock()
|
||||||
if m.hostTokenBindings == nil {
|
if m.hostTokenBindings == nil {
|
||||||
m.hostTokenBindings = make(map[string]string)
|
m.hostTokenBindings = make(map[string]string)
|
||||||
}
|
}
|
||||||
// Bind tokens by hostname rather than agent ID. This allows:
|
boundID := strings.TrimSpace(m.hostTokenBindings[bindingKey])
|
||||||
// - 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")
|
|
||||||
}
|
|
||||||
m.mu.Unlock()
|
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
|
var previous models.Host
|
||||||
|
|
|
||||||
|
|
@ -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)
|
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()
|
snapshot := monitor.state.GetSnapshot()
|
||||||
if got := len(snapshot.Hosts); got != 2 {
|
if got := len(snapshot.Hosts); got != 1 {
|
||||||
t.Fatalf("expected 2 hosts in state, got %d", got)
|
t.Fatalf("expected 1 host in state after removal, got %d", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue