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/models" 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 []models.DockerHost) (string, []string, models.DockerHost, 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 existing.ID, 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, models.DockerHost{}, false } // findMatchingDockerHost searches for an existing host that matches the report. func findMatchingDockerHost(hosts []models.DockerHost, report agentsdocker.Report, tokenRecord *config.APITokenRecord) (models.DockerHost, 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 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 strings.TrimSpace(host.MachineID) == machineID && strings.TrimSpace(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 strings.TrimSpace(host.Hostname) == hostname && strings.TrimSpace(host.TokenID) == tokenID { return host, true } } } if machineID != "" && tokenID == "" { for _, host := range hosts { if strings.TrimSpace(host.MachineID) == machineID && strings.TrimSpace(host.TokenID) == "" { return host, true } } } if hostname != "" && tokenID == "" { for _, host := range hosts { if strings.TrimSpace(host.Hostname) == hostname && strings.TrimSpace(host.TokenID) == "" { return host, true } } } return models.DockerHost{}, false } // dockerHostIdentityConflicts reports whether an incoming report is clearly from // a different physical/swarm node than the existing Docker host record. func dockerHostIdentityConflicts(existing models.DockerHost, report agentsdocker.Report) bool { existingMachineID := strings.TrimSpace(existing.MachineID) reportMachineID := strings.TrimSpace(report.Host.MachineID) if existingMachineID != "" && reportMachineID != "" && existingMachineID != reportMachineID { return true } existingSwarmNodeID := "" if existing.Swarm != nil { existingSwarmNodeID = strings.TrimSpace(existing.Swarm.NodeID) } reportSwarmNodeID := "" if report.Host.Swarm != nil { reportSwarmNodeID = strings.TrimSpace(report.Host.Swarm.NodeID) } if existingSwarmNodeID != "" && reportSwarmNodeID != "" && existingSwarmNodeID != reportSwarmNodeID { return true } // When we do not have 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 != "" && !strings.EqualFold(existingHostname, reportHostname) { return true } return false } // dockerHostIDExists checks if a host ID is already in use. func dockerHostIDExists(id string, hosts []models.DockerHost) bool { if strings.TrimSpace(id) == "" { return false } for _, host := range hosts { if host.ID == id { return true } } return false } // generateDockerHostIdentifier creates a unique identifier by appending suffixes. func generateDockerHostIdentifier(base string, report agentsdocker.Report, tokenRecord *config.APITokenRecord, hosts []models.DockerHost) 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 { used[host.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 }