pkg/packages/addons.go
Rama main · réplica de lectura del repositorio original.
package packages
import "strings"
// The add-on catalog — the single Go-side source of truth for "is this 3rd-party
// platform machinery?". It is derived from two parts:
//
// 1. crdGroupToChart's canonical chart names — so any operator we already
// recognize by its CRD group also classifies as an add-on, with zero extra
// maintenance: add a CRD group, its chart joins the catalog automatically.
// 2. addonChartsExtra — platform/security charts that ship NO CRD group of
// their own (ingress controllers, DNS, metrics, logging, secrets managers),
// so (1) can't catch them.
//
// The Applications and Add-ons surfaces share this classifier rather than each
// carrying a private allowlist. The frontend's interim KNOWN_ADDON_CHARTS
// (radar-hub-web packagesModel.ts) mirrors this list until the hub forwards the
// classification on the wire; keep the two in sync until then.
// addonChartsExtra lists add-on charts not keyed by a crdGroupToChart entry.
// Kept in lockstep with radar-hub-web's KNOWN_ADDON_CHARTS extras.
var addonChartsExtra = []string{
// Argo family beyond the argoproj.io→argo-cd catalog entry
"argocd", "argo-rollouts", "argo-workflows", "argo-events", "flux2", "capi",
// observability / logging / cost
"prometheus", "prometheus-operator", "grafana", "loki", "tempo", "mimir",
"victoria-metrics", "caretta", "alloy", "pyroscope", "promtail", "thanos",
"jaeger", "kiali",
"fluent-bit", "fluentd", "vector", "opencost", "kubecost",
"kube-state-metrics", "node-problem-detector", "datadog", "newrelic-bundle",
// service mesh / ingress / dns
"istiod", "istio-base", "ingress-nginx", "nginx-ingress", "external-dns",
"linkerd", "kong", "contour", "cilium",
// gateway-helm is Envoy Gateway's actual chart name
"envoy-gateway", "gateway-helm",
// secrets / policy / security
"sealed-secrets", "vault", "vault-secrets-operator", "gatekeeper",
"falco", "trivy-operator",
// autoscaling / scheduling / ops
"metrics-server", "cluster-autoscaler", "vertical-pod-autoscaler", "vpa",
"reloader", "descheduler", "aws-load-balancer-controller", "keda",
"kured", "kube-fledged", "kubernetes-dashboard",
// platform / storage / networking / serverless
"cnpg", "cloudnative-pg", "opentelemetry-collector", "opentelemetry-operator",
"crossplane", "crossplane-rbac-manager", "coredns", "calico", "longhorn",
"metallb", "knative-serving", "knative-eventing", "knative-operator",
"cluster-api",
}
// knownAddonCharts is the resolved catalog: every canonical chart name
// crdGroupToChart maps to, unioned with addonChartsExtra. Built once at init so
// the catalog stays a derivative of crdGroupToChart, not a parallel copy.
var knownAddonCharts = buildAddonCatalog()
// genericAddonNames are real add-ons, but common enough words that name-only
// evidence can misclassify a user app literally named "vector" or "vault".
// Chart/addon-manager/image evidence still classifies them.
var genericAddonNames = map[string]bool{
"alloy": true, "contour": true, "kong": true, "vault": true, "vector": true,
}
var addonImageRepos = map[string][]string{
"alloy": {"grafana/alloy"},
"contour": {"projectcontour/contour"},
"kong": {"kong/kubernetes-ingress-controller", "kong/kong-gateway", "kong/kong"},
"vault": {"hashicorp/vault", "hashicorp/vault-k8s", "hashicorp/vault-csi-provider"},
"vector": {"timberio/vector", "vectordotdev/vector"},
}
func buildAddonCatalog() map[string]bool {
m := make(map[string]bool, len(crdGroupToChart)+len(addonChartsExtra))
for _, chart := range crdGroupToChart {
m[chart] = true
}
for _, chart := range addonChartsExtra {
m[chart] = true
}
return m
}
// matchAddonChart reports the catalog entry a value matches — exact, or
// hyphen-prefixed so a versioned/sub-named value ("kube-prometheus-stack-45",
// "cert-manager-webhook") still hits its base. Returns ("", false) on no match.
func matchAddonChart(value string) (base string, ok bool) {
v := strings.ToLower(strings.TrimSpace(value))
if v == "" {
return "", false
}
if knownAddonCharts[v] {
return v, true
}
best := ""
for known := range knownAddonCharts {
if strings.HasPrefix(v, known+"-") {
if len(known) > len(best) || (len(known) == len(best) && known < best) {
best = known
}
}
}
if best != "" {
return best, true
}
return "", false
}
func matchAddonImage(image string) (base string, ok bool) {
repo := imageRepo(image)
if repo == "" {
return "", false
}
best := ""
for addon, suffixes := range addonImageRepos {
for _, suffix := range suffixes {
if repo == suffix || strings.HasSuffix(repo, "/"+suffix) {
if len(suffix) > len(best) || (len(suffix) == len(best) && suffix < best) {
best = suffix
base = addon
}
}
}
}
return base, base != ""
}
func imageRepo(image string) string {
v := strings.ToLower(strings.TrimSpace(image))
if v == "" {
return ""
}
if at := strings.Index(v, "@"); at >= 0 {
v = v[:at]
}
slash := strings.LastIndex(v, "/")
colon := strings.LastIndex(v, ":")
if colon > slash {
v = v[:colon]
}
return v
}
// ClassifyAddon reports whether a workload belongs to 3rd-party platform
// machinery, matching its helm.sh/chart base, primary image repo,
// app.kubernetes.io/{name,part-of}, or workload name against the shared add-on catalog. Returns the evidence
// ("field=value", with the matched catalog base in parens on a prefix hit) when
// classified.
//
// This is a CLASSIFIER, never a drop filter: callers keep the row visible
// (raw-always) and tag it, so the UI can fold add-ons away while still
// explaining WHY each was tagged.
func ClassifyAddon(chart, appName, partOf, name, addonManagerMode, image string) (addon bool, why string) {
// The upstream addon-manager label is the platform itself declaring
// "managed addon" (GKE stamps it on managed components, upstream
// addon-manager on its manifests) — stronger than any catalog match, and
// no user app carries it.
if v := strings.TrimSpace(addonManagerMode); v != "" {
return true, "addonmanager.kubernetes.io/mode=" + v
}
chartBase, _ := SplitChart(chart)
if addon, why := classifyAddonValue("chart", chartBase, true); addon {
return addon, why
}
if base, ok := matchAddonImage(image); ok {
return true, "image=" + imageRepo(image) + " (" + base + ")"
}
candidates := []struct{ field, value string }{
{"app.kubernetes.io/name", appName},
{"app.kubernetes.io/part-of", partOf},
{"name", name},
}
for _, c := range candidates {
if addon, why := classifyAddonValue(c.field, c.value, false); addon {
return addon, why
}
}
return false, ""
}
func classifyAddonValue(field, value string, strong bool) (bool, string) {
v := strings.ToLower(strings.TrimSpace(value))
if v == "" {
return false, ""
}
base, ok := matchAddonChart(v)
if !ok || (!strong && genericAddonNames[base]) {
return false, ""
}
if base == v {
return true, field + "=" + v
}
return true, field + "=" + v + " (" + base + ")"
}