mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-08-22 23:43:30 +00:00
Back-port v5 fix 333e66a8e to v6, adapted to v6's DockerHostView accessors.
findMatchingDockerHost now skips an agentID/token match when
dockerHostIdentityConflicts detects a different MachineID, Swarm NodeID, or
(absent stronger IDs) hostname, so two physically different Docker nodes
that share a token+agentID are not collapsed into one host record. Adds a
regression test.
412 lines
11 KiB
Go
412 lines
11 KiB
Go
package monitoring
|
|
|
|
import (
|
|
"crypto/sha1"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
agentsdocker "github.com/rcourtman/pulse-go-rewrite/pkg/agents/docker"
|
|
)
|
|
|
|
// tokenHintFromRecord returns a redacted token hint for display purposes.
|
|
func tokenHintFromRecord(record *config.APITokenRecord) string {
|
|
if record == nil {
|
|
return ""
|
|
}
|
|
switch {
|
|
case record.Prefix != "" && record.Suffix != "":
|
|
return fmt.Sprintf("%s…%s", record.Prefix, record.Suffix)
|
|
case record.Prefix != "":
|
|
return record.Prefix + "…"
|
|
case record.Suffix != "":
|
|
return "…" + record.Suffix
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// resolveDockerHostIdentifier determines a unique identifier for a Docker host
|
|
// based on its report and existing hosts. Returns the identifier, fallback identifiers,
|
|
// the existing host (if matched), and whether a match was found.
|
|
func resolveDockerHostIdentifier(report agentsdocker.Report, tokenRecord *config.APITokenRecord, hosts []*unifiedresources.DockerHostView) (string, []string, *unifiedresources.DockerHostView, bool) {
|
|
base := strings.TrimSpace(report.AgentKey())
|
|
fallbacks := uniqueNonEmptyStrings(
|
|
base,
|
|
strings.TrimSpace(report.Agent.ID),
|
|
strings.TrimSpace(report.Host.MachineID),
|
|
strings.TrimSpace(report.Host.Hostname),
|
|
)
|
|
|
|
if existing, ok := findMatchingDockerHost(hosts, report, tokenRecord); ok {
|
|
return dockerHostStableID(existing), fallbacks, existing, true
|
|
}
|
|
|
|
identifier := base
|
|
if identifier == "" {
|
|
identifier = strings.TrimSpace(report.Host.MachineID)
|
|
}
|
|
if identifier == "" {
|
|
identifier = strings.TrimSpace(report.Host.Hostname)
|
|
}
|
|
if identifier == "" {
|
|
identifier = strings.TrimSpace(report.Agent.ID)
|
|
}
|
|
if identifier == "" {
|
|
identifier = fallbackDockerHostID(report, tokenRecord)
|
|
}
|
|
if identifier == "" {
|
|
identifier = "docker-host"
|
|
}
|
|
|
|
if dockerHostIDExists(identifier, hosts) {
|
|
identifier = generateDockerHostIdentifier(identifier, report, tokenRecord, hosts)
|
|
}
|
|
|
|
return identifier, fallbacks, nil, false
|
|
}
|
|
|
|
// findMatchingDockerHost searches for an existing host that matches the report.
|
|
func findMatchingDockerHost(hosts []*unifiedresources.DockerHostView, report agentsdocker.Report, tokenRecord *config.APITokenRecord) (*unifiedresources.DockerHostView, bool) {
|
|
agentID := strings.TrimSpace(report.Agent.ID)
|
|
tokenID := ""
|
|
if tokenRecord != nil {
|
|
tokenID = strings.TrimSpace(tokenRecord.ID)
|
|
}
|
|
machineID := strings.TrimSpace(report.Host.MachineID)
|
|
hostname := strings.TrimSpace(report.Host.Hostname)
|
|
|
|
if agentID != "" {
|
|
for _, host := range hosts {
|
|
if host == nil || strings.TrimSpace(host.AgentID()) != agentID {
|
|
continue
|
|
}
|
|
|
|
existingToken := strings.TrimSpace(host.TokenID())
|
|
if tokenID == "" || existingToken == tokenID {
|
|
if dockerHostIdentityConflicts(host, report) {
|
|
continue
|
|
}
|
|
return host, true
|
|
}
|
|
}
|
|
}
|
|
|
|
if machineID != "" && hostname != "" {
|
|
for _, host := range hosts {
|
|
if host == nil {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(host.MachineID()) == machineID &&
|
|
unifiedresources.HostnamesEquivalent(host.Hostname(), hostname) {
|
|
if tokenID == "" || strings.TrimSpace(host.TokenID()) == tokenID {
|
|
return host, true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: match by Hostname and Token only (when MachineID is missing)
|
|
// This fixes issues where containerized agents without persistent machine-id
|
|
// reconnect with the same token but are treated as new agents.
|
|
if hostname != "" && tokenID != "" {
|
|
for _, host := range hosts {
|
|
if host == nil {
|
|
continue
|
|
}
|
|
if unifiedresources.HostnamesEquivalent(host.Hostname(), hostname) &&
|
|
strings.TrimSpace(host.TokenID()) == tokenID {
|
|
return host, true
|
|
}
|
|
}
|
|
}
|
|
|
|
if machineID != "" && tokenID == "" {
|
|
for _, host := range hosts {
|
|
if host == nil {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(host.MachineID()) == machineID && strings.TrimSpace(host.TokenID()) == "" {
|
|
return host, true
|
|
}
|
|
}
|
|
}
|
|
|
|
if hostname != "" && tokenID == "" {
|
|
for _, host := range hosts {
|
|
if host == nil {
|
|
continue
|
|
}
|
|
if unifiedresources.HostnamesEquivalent(host.Hostname(), hostname) &&
|
|
strings.TrimSpace(host.TokenID()) == "" {
|
|
return host, true
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
// dockerHostIdentityConflicts reports whether an incoming report is clearly from
|
|
// a different physical/swarm node than the existing Docker host record, so a
|
|
// shared token+agentID reused from another node is not collapsed into one host.
|
|
func dockerHostIdentityConflicts(existing *unifiedresources.DockerHostView, report agentsdocker.Report) bool {
|
|
if existing == nil {
|
|
return false
|
|
}
|
|
|
|
existingMachineID := strings.TrimSpace(existing.MachineID())
|
|
reportMachineID := strings.TrimSpace(report.Host.MachineID)
|
|
if existingMachineID != "" && reportMachineID != "" && existingMachineID != reportMachineID {
|
|
return true
|
|
}
|
|
|
|
existingSwarmNodeID := ""
|
|
if sw := existing.Swarm(); sw != nil {
|
|
existingSwarmNodeID = strings.TrimSpace(sw.NodeID)
|
|
}
|
|
reportSwarmNodeID := ""
|
|
if report.Host.Swarm != nil {
|
|
reportSwarmNodeID = strings.TrimSpace(report.Host.Swarm.NodeID)
|
|
}
|
|
if existingSwarmNodeID != "" && reportSwarmNodeID != "" && existingSwarmNodeID != reportSwarmNodeID {
|
|
return true
|
|
}
|
|
|
|
// Without stronger stable IDs, a hostname change is ambiguous and should not
|
|
// be treated as the same reporting host automatically.
|
|
existingHostname := strings.TrimSpace(existing.Hostname())
|
|
reportHostname := strings.TrimSpace(report.Host.Hostname)
|
|
if existingMachineID == "" && reportMachineID == "" &&
|
|
existingSwarmNodeID == "" && reportSwarmNodeID == "" &&
|
|
existingHostname != "" && reportHostname != "" &&
|
|
!unifiedresources.HostnamesEquivalent(existingHostname, reportHostname) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func dockerHostStableID(host *unifiedresources.DockerHostView) string {
|
|
if host == nil {
|
|
return ""
|
|
}
|
|
if id := strings.TrimSpace(host.HostSourceID()); id != "" {
|
|
return id
|
|
}
|
|
return strings.TrimSpace(host.ID())
|
|
}
|
|
|
|
func resolveDockerTokenBindingIdentity(identifier string, report agentsdocker.Report, previous *unifiedresources.DockerHostView, hasPrevious bool) (string, []string) {
|
|
preferred := ""
|
|
if hasPrevious {
|
|
preferred = dockerHostStableID(previous)
|
|
}
|
|
if preferred == "" {
|
|
preferred = strings.TrimSpace(report.Agent.ID)
|
|
}
|
|
if preferred == "" {
|
|
preferred = strings.TrimSpace(identifier)
|
|
}
|
|
if preferred == "" {
|
|
preferred = strings.TrimSpace(report.Host.MachineID)
|
|
}
|
|
if preferred == "" {
|
|
preferred = strings.TrimSpace(report.Host.Hostname)
|
|
}
|
|
|
|
aliases := uniqueNonEmptyStrings(
|
|
preferred,
|
|
identifier,
|
|
report.Agent.ID,
|
|
report.Host.MachineID,
|
|
report.Host.Hostname,
|
|
)
|
|
if previous != nil {
|
|
aliases = uniqueNonEmptyStrings(append(aliases,
|
|
dockerHostStableID(previous),
|
|
previous.AgentID(),
|
|
previous.MachineID(),
|
|
previous.Hostname(),
|
|
)...)
|
|
}
|
|
|
|
return preferred, aliases
|
|
}
|
|
|
|
func dockerTokenBindingMatches(boundAgentID string, aliases []string) bool {
|
|
boundAgentID = strings.TrimSpace(boundAgentID)
|
|
if boundAgentID == "" {
|
|
return false
|
|
}
|
|
for _, alias := range aliases {
|
|
if boundAgentID == strings.TrimSpace(alias) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// dockerHostIDExists checks if a host ID is already in use.
|
|
func dockerHostIDExists(id string, hosts []*unifiedresources.DockerHostView) bool {
|
|
if strings.TrimSpace(id) == "" {
|
|
return false
|
|
}
|
|
for _, host := range hosts {
|
|
if dockerHostStableID(host) == id {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// generateDockerHostIdentifier creates a unique identifier by appending suffixes.
|
|
func generateDockerHostIdentifier(base string, report agentsdocker.Report, tokenRecord *config.APITokenRecord, hosts []*unifiedresources.DockerHostView) string {
|
|
if strings.TrimSpace(base) == "" {
|
|
base = fallbackDockerHostID(report, tokenRecord)
|
|
}
|
|
if strings.TrimSpace(base) == "" {
|
|
base = "docker-host"
|
|
}
|
|
|
|
used := make(map[string]struct{}, len(hosts))
|
|
for _, host := range hosts {
|
|
if id := dockerHostStableID(host); id != "" {
|
|
used[id] = struct{}{}
|
|
}
|
|
}
|
|
|
|
suffixes := dockerHostSuffixCandidates(report, tokenRecord)
|
|
for _, suffix := range suffixes {
|
|
candidate := fmt.Sprintf("%s::%s", base, suffix)
|
|
if _, exists := used[candidate]; !exists {
|
|
return candidate
|
|
}
|
|
}
|
|
|
|
seed := strings.Join(suffixes, "|")
|
|
if strings.TrimSpace(seed) == "" {
|
|
seed = base
|
|
}
|
|
sum := sha1.Sum([]byte(seed))
|
|
hashSuffix := fmt.Sprintf("hash-%s", hex.EncodeToString(sum[:6]))
|
|
candidate := fmt.Sprintf("%s::%s", base, hashSuffix)
|
|
if _, exists := used[candidate]; !exists {
|
|
return candidate
|
|
}
|
|
|
|
for idx := 2; ; idx++ {
|
|
candidate = fmt.Sprintf("%s::%d", base, idx)
|
|
if _, exists := used[candidate]; !exists {
|
|
return candidate
|
|
}
|
|
}
|
|
}
|
|
|
|
// dockerHostSuffixCandidates returns candidate suffixes for generating unique IDs.
|
|
func dockerHostSuffixCandidates(report agentsdocker.Report, tokenRecord *config.APITokenRecord) []string {
|
|
candidates := make([]string, 0, 5)
|
|
|
|
if tokenRecord != nil {
|
|
if sanitized := sanitizeDockerHostSuffix(tokenRecord.ID); sanitized != "" {
|
|
candidates = append(candidates, "token-"+sanitized)
|
|
}
|
|
}
|
|
|
|
if agentID := sanitizeDockerHostSuffix(report.Agent.ID); agentID != "" {
|
|
candidates = append(candidates, "agent-"+agentID)
|
|
}
|
|
|
|
if machineID := sanitizeDockerHostSuffix(report.Host.MachineID); machineID != "" {
|
|
candidates = append(candidates, "machine-"+machineID)
|
|
}
|
|
|
|
hostNameSanitized := sanitizeDockerHostSuffix(report.Host.Hostname)
|
|
if hostNameSanitized != "" {
|
|
candidates = append(candidates, "host-"+hostNameSanitized)
|
|
}
|
|
|
|
hostDisplay := sanitizeDockerHostSuffix(report.Host.Name)
|
|
if hostDisplay != "" && hostDisplay != hostNameSanitized {
|
|
candidates = append(candidates, "name-"+hostDisplay)
|
|
}
|
|
|
|
return uniqueNonEmptyStrings(candidates...)
|
|
}
|
|
|
|
// sanitizeDockerHostSuffix cleans a string for use as a host ID suffix.
|
|
func sanitizeDockerHostSuffix(value string) string {
|
|
value = strings.ToLower(strings.TrimSpace(value))
|
|
if value == "" {
|
|
return ""
|
|
}
|
|
|
|
var builder strings.Builder
|
|
builder.Grow(len(value))
|
|
lastHyphen := false
|
|
runeCount := 0
|
|
|
|
for _, r := range value {
|
|
if runeCount >= 48 {
|
|
break
|
|
}
|
|
|
|
switch {
|
|
case unicode.IsLetter(r) || unicode.IsDigit(r):
|
|
builder.WriteRune(r)
|
|
lastHyphen = false
|
|
runeCount++
|
|
default:
|
|
if !lastHyphen {
|
|
builder.WriteRune('-')
|
|
lastHyphen = true
|
|
runeCount++
|
|
}
|
|
}
|
|
}
|
|
|
|
result := strings.Trim(builder.String(), "-")
|
|
if result == "" {
|
|
return ""
|
|
}
|
|
return result
|
|
}
|
|
|
|
// fallbackDockerHostID generates a hash-based ID when no better identifier exists.
|
|
func fallbackDockerHostID(report agentsdocker.Report, tokenRecord *config.APITokenRecord) string {
|
|
seedParts := dockerHostSuffixCandidates(report, tokenRecord)
|
|
if len(seedParts) == 0 {
|
|
seedParts = uniqueNonEmptyStrings(
|
|
report.Host.Hostname,
|
|
report.Host.MachineID,
|
|
report.Agent.ID,
|
|
)
|
|
}
|
|
if len(seedParts) == 0 {
|
|
return ""
|
|
}
|
|
seed := strings.Join(seedParts, "|")
|
|
sum := sha1.Sum([]byte(seed))
|
|
return fmt.Sprintf("docker-host-%s", hex.EncodeToString(sum[:6]))
|
|
}
|
|
|
|
// uniqueNonEmptyStrings returns unique non-empty strings in order of first appearance.
|
|
func uniqueNonEmptyStrings(values ...string) []string {
|
|
seen := make(map[string]struct{}, len(values))
|
|
result := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[value]; ok {
|
|
continue
|
|
}
|
|
seen[value] = struct{}{}
|
|
result = append(result, value)
|
|
}
|
|
return result
|
|
}
|