package k8s import ( "fmt" "sort" "strings" "tower/internal/model" ) // RollupKey groups similar issues to reduce UI noise. // Required grouping per prompt: (namespace, reason, kind). type RollupKey struct { Namespace string Reason string Kind string } // Rollup groups issues by (namespace, reason, kind). For any group with size >= // threshold, it emits a single rollup issue and removes the individual issues // from the output. // // Rollup issues use Priority of the max priority in the group. func Rollup(issues []model.Issue, threshold int, sampleN int) []model.Issue { if threshold <= 0 { threshold = 20 } if sampleN <= 0 { sampleN = 5 } groups := make(map[RollupKey][]model.Issue, 32) ungrouped := make([]model.Issue, 0, len(issues)) for _, iss := range issues { kind := strings.TrimSpace(iss.Evidence["kind"]) reason := strings.TrimSpace(iss.Evidence["reason"]) ns := strings.TrimSpace(iss.Evidence["namespace"]) if kind == "" || reason == "" { ungrouped = append(ungrouped, iss) continue } k := RollupKey{Namespace: ns, Reason: reason, Kind: kind} groups[k] = append(groups[k], iss) } rolled := make([]model.Issue, 0, len(issues)) rolled = append(rolled, ungrouped...) // Stable order for determinism. keys := make([]RollupKey, 0, len(groups)) for k := range groups { keys = append(keys, k) } sort.Slice(keys, func(i, j int) bool { if keys[i].Namespace != keys[j].Namespace { return keys[i].Namespace < keys[j].Namespace } if keys[i].Kind != keys[j].Kind { return keys[i].Kind < keys[j].Kind } return keys[i].Reason < keys[j].Reason }) for _, k := range keys { grp := groups[k] if len(grp) < threshold { rolled = append(rolled, grp...) continue } // determine max priority maxP := model.PriorityP3 for _, iss := range grp { if iss.Priority.Weight() > maxP.Weight() { maxP = iss.Priority } } titleNS := "" if k.Namespace != "" { titleNS = fmt.Sprintf(" (ns=%s)", k.Namespace) } title := fmt.Sprintf("%d %ss %s%s", len(grp), strings.ToLower(k.Kind), k.Reason, titleNS) samples := make([]string, 0, sampleN) for i := 0; i < len(grp) && i < sampleN; i++ { s := grp[i].Title if s == "" { s = grp[i].ID } samples = append(samples, s) } rolled = append(rolled, model.Issue{ ID: fmt.Sprintf("k8s:rollup:%s:%s:%s", k.Namespace, k.Kind, k.Reason), Category: model.CategoryKubernetes, Priority: maxP, Title: title, Details: "Many similar Kubernetes issues were aggregated into this rollup.", Evidence: map[string]string{ "kind": k.Kind, "reason": k.Reason, "namespace": k.Namespace, "count": fmt.Sprintf("%d", len(grp)), "samples": strings.Join(samples, " | "), }, SuggestedFix: "Filter events/pods and inspect samples with kubectl describe.", }) } return rolled } // CapIssues enforces a hard cap after rollups. This should be applied after // sorting by default sort order (priority desc, recency desc), but we keep this // helper pure and simple. func CapIssues(issues []model.Issue, max int) []model.Issue { if max <= 0 { max = 200 } if len(issues) <= max { return issues } out := make([]model.Issue, max) copy(out, issues[:max]) return out }