Files
2026-06-16 21:52:10 +04:00

87 lines
3.3 KiB
Go

package cluster
import (
"context"
"fmt"
"sort"
"strings"
"time"
)
type DriftIssue struct {
Level string `json:"level"`
Kind string `json:"kind"`
Message string `json:"message"`
}
type DriftReport struct {
Cluster string `json:"cluster"`
Generated time.Time `json:"generated_at"`
Issues []DriftIssue `json:"issues,omitempty"`
}
func (s *Service) DetectDrift(ctx context.Context, selector string) (DriftReport, error) {
c, err := s.Get(selector)
if err != nil {
return DriftReport{}, err
}
rep := DriftReport{Cluster: c.Name, Generated: s.now().UTC()}
expected := strings.TrimSpace(s.ExpectedAgentVersion())
dctrl := normalizeDriftControl(c.Drift)
if !c.Agent.Installed {
rep.Issues = append(rep.Issues, DriftIssue{Level: "warn", Kind: "agent", Message: "agent is not installed"})
return rep, nil
}
pingCtx, cancel := context.WithTimeout(ctx, 2500*time.Millisecond)
ping, pingErr := s.PingAgent(pingCtx, c.ID)
cancel()
if pingErr != nil || !ping.Reachable || ping.StatusCode >= 400 {
rep.Issues = append(rep.Issues, DriftIssue{Level: "crit", Kind: "agent", Message: "agent is unreachable"})
} else {
nodeVersion := strings.TrimSpace(ping.Version)
if nodeVersion == "" {
nodeVersion = strings.TrimSpace(c.Agent.Version)
}
if expected != "" && nodeVersion != "" && nodeVersion != expected {
rep.Issues = append(rep.Issues, DriftIssue{Level: "warn", Kind: "agent_version", Message: fmt.Sprintf("agent version mismatch: node=%s local=%s", nodeVersion, expected)})
}
if nodeVersion != "" {
if ok, latest, known := s.CompareAgentVersion(nodeVersion); known && !ok {
rep.Issues = append(rep.Issues, DriftIssue{Level: "warn", Kind: "agent_outdated", Message: fmt.Sprintf("node version %s is older than latest known %s", nodeVersion, latest)})
}
if dctrl.Baseline.Enabled && strings.TrimSpace(dctrl.Baseline.AgentVersion) != "" && !strings.EqualFold(strings.TrimSpace(dctrl.Baseline.AgentVersion), nodeVersion) {
rep.Issues = append(rep.Issues, DriftIssue{
Level: "warn",
Kind: "baseline_agent_version",
Message: fmt.Sprintf("baseline agent version mismatch: baseline=%s live=%s", dctrl.Baseline.AgentVersion, nodeVersion),
})
}
}
}
softCtx, softCancel := context.WithTimeout(ctx, 7*time.Second)
fresh, softErr := s.probeSoftware(softCtx, c, "", "")
softCancel()
if softErr != nil {
rep.Issues = append(rep.Issues, DriftIssue{Level: "warn", Kind: "software_probe", Message: softErr.Error()})
} else {
oldSet := strings.TrimSpace(c.Software.Summary())
newSet := strings.TrimSpace(fresh.Summary())
if oldSet != "" && oldSet != "-" && newSet != oldSet {
rep.Issues = append(rep.Issues, DriftIssue{Level: "warn", Kind: "software", Message: fmt.Sprintf("software support changed: stored=%s live=%s", oldSet, newSet)})
}
if dctrl.Baseline.Enabled && strings.TrimSpace(dctrl.Baseline.Software) != "" && !strings.EqualFold(strings.TrimSpace(dctrl.Baseline.Software), newSet) {
rep.Issues = append(rep.Issues, DriftIssue{
Level: "warn",
Kind: "baseline_software",
Message: fmt.Sprintf("baseline software mismatch: baseline=%s live=%s", dctrl.Baseline.Software, newSet),
})
}
}
sort.Slice(rep.Issues, func(i, j int) bool { return rep.Issues[i].Kind < rep.Issues[j].Kind })
return rep, nil
}