pkg/finalizers/catalog.go
Rama main · réplica de lectura del repositorio original.
// Package finalizers recognizes finalizer owners from vendor keys and workload
// identity. It performs no cluster reads or issue-policy decisions.
package finalizers
import (
"path"
"slices"
"strings"
"github.com/skyhook-io/radar/pkg/resourceid"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// Owner records controller identity and the reviewed limits of its cleanup advice.
type Owner struct {
// Controller is the human-readable controller name surfaced in
// Issue messages ("argocd-application-controller is CrashLoopBackOff").
Controller string
// Namespace is the conventional install namespace for GitOps enrichment;
// the generic issues inventory searches all readable namespaces.
Namespace string
// SelectorKey + SelectorValue: label selector to find the
// controller's pods.
SelectorKey string
SelectorValue string
// OffClusterOfferings names managed offerings that run this controller
// outside the cluster it serves. For these controllers, finding none of
// their pods in the cluster does not show that they are gone.
OffClusterOfferings string
// ReleasesInfrastructure marks finalizers whose cleanup drains nodes or
// releases cloud resources. Removing one leaves that infrastructure
// behind even when the controller is down.
ReleasesInfrastructure bool
// AllowConditionalCleanup opts into removal advice after a complete negative
// search, subject to the user confirming intentional uninstall or permanent
// scale-down. Recognition alone never enables removal. Leave false for
// unreviewed cleanup semantics, off-cluster controllers or infrastructure.
AllowConditionalCleanup bool
// Images are the repository paths of the controller's published image,
// without registry host, tag or digest, so a mirror that prefixes the path
// still matches. They recognize installs whose names and labels were
// changed (a Helm nameOverride renames both).
Images []string
// matchesImageRole distinguishes components sharing a repository. A nil
// predicate means every container from Images is this controller.
matchesImageRole func(corev1.Container) bool
requireImageMatch bool
}
// ResolveOwner returns a best-guess Owner for the
// given finalizer key + resource. The `root` resource's apiVersion is
// used to disambiguate generic Flux finalizers that span multiple
// controllers (a single `finalizers.fluxcd.io` key is set by every Flux
// controller; the resource kind tells us which one).
//
// Returns nil when the finalizer isn't recognized — caller should
// degrade gracefully (skip the controller-health enrichment, keep the
// rest of the Issue intact). Namespace is a convention, not a search boundary.
func ResolveOwner(finalizer string, root *unstructured.Unstructured) *Owner {
if strings.HasPrefix(finalizer, "karpenter.sh/") || strings.HasPrefix(finalizer, "karpenter.k8s.aws/") || strings.HasPrefix(finalizer, "karpenter.azure.com/") {
return &Owner{
Controller: "karpenter",
SelectorKey: "app.kubernetes.io/name",
SelectorValue: "karpenter",
OffClusterOfferings: "EKS Auto Mode, AKS node auto-provisioning",
ReleasesInfrastructure: true,
AllowConditionalCleanup: false,
// public.ecr.aws/karpenter/controller (AWS provider chart),
// mcr.microsoft.com/aks/karpenter/controller (Azure provider chart).
Images: []string{"karpenter/controller", "aks/karpenter/controller"},
}
}
switch finalizer {
case "apps.victoriametrics.com/finalizer":
if resourceid.GroupFromAPIVersion(root.GetAPIVersion()) != "operator.victoriametrics.com" {
return nil
}
return &Owner{
Controller: "victoria-metrics-operator",
SelectorKey: "app.kubernetes.io/name",
SelectorValue: "victoria-metrics-operator",
Images: []string{"victoriametrics/operator"},
matchesImageRole: runsVictoriaMetricsOperator,
AllowConditionalCleanup: true,
}
// Argo CD: a single Application controller owns all Argo-side
// finalizers, including the deprecated "foreground-cascade" key
// retained for installs that pre-date the rename.
case "resources-finalizer.argocd.argoproj.io",
"foreground-cascade.argocd.argoproj.io",
"resources-finalizer.argocd.argoproj.io/foreground":
return &Owner{
Controller: "argocd-application-controller",
Namespace: "argocd",
SelectorKey: "app.kubernetes.io/name",
SelectorValue: "argocd-application-controller",
OffClusterOfferings: "Amazon EKS Capabilities",
AllowConditionalCleanup: false,
// quay.io/argoproj/argocd also runs the API server, repo server
// and the other Argo CD components.
Images: []string{"argoproj/argocd"},
matchesImageRole: runsArgoApplicationController,
}
// Flux: legacy "finalizers.fluxcd.io" key is shared across all
// Flux controllers. Disambiguate via the resource's apiVersion.
case "finalizers.fluxcd.io":
return resolveFluxOwnerByKind(root)
// Flux: post-1.0 split finalizer keys per controller. Names are
// stable since Flux v2.0+ — newer installs default to these.
case "finalizers.kustomize.toolkit.fluxcd.io":
return &fluxKustomizeController
case "finalizers.helm.toolkit.fluxcd.io":
return &fluxHelmController
case "finalizers.source.toolkit.fluxcd.io":
return &fluxSourceController
case "finalizers.notification.toolkit.fluxcd.io":
return &fluxNotificationController
case "finalizers.image.toolkit.fluxcd.io":
return resolveFluxImageOwner(root)
}
return nil
}
// resolveFluxOwnerByKind disambiguates the generic "finalizers.fluxcd.io"
// finalizer by the resource's API group. Returns nil when the resource
// is from an unfamiliar Flux group — better to skip the enrichment than
// to misattribute to the wrong controller and send the operator
// debugging the wrong pod.
func resolveFluxOwnerByKind(root *unstructured.Unstructured) *Owner {
api := strings.ToLower(root.GetAPIVersion())
switch {
case strings.HasPrefix(api, "kustomize.toolkit.fluxcd.io/"):
return &fluxKustomizeController
case strings.HasPrefix(api, "helm.toolkit.fluxcd.io/"):
return &fluxHelmController
case strings.HasPrefix(api, "source.toolkit.fluxcd.io/"):
return &fluxSourceController
case strings.HasPrefix(api, "notification.toolkit.fluxcd.io/"):
return &fluxNotificationController
case strings.HasPrefix(api, "image.toolkit.fluxcd.io/"):
return resolveFluxImageOwner(root)
}
return nil
}
// Two controllers serve image.toolkit.fluxcd.io: image-automation-controller
// owns ImageUpdateAutomation, image-reflector-controller the rest.
func resolveFluxImageOwner(root *unstructured.Unstructured) *Owner {
if root.GetKind() == "ImageUpdateAutomation" {
return &fluxImageAutomationController
}
return &fluxImageController
}
// Standard Flux controller catalog. Values match the official Helm chart
// defaults for `flux-system`; images are ghcr.io/fluxcd/<controller>.
var (
fluxKustomizeController = Owner{
Controller: "kustomize-controller",
Namespace: "flux-system",
SelectorKey: "app",
SelectorValue: "kustomize-controller",
AllowConditionalCleanup: true,
Images: []string{"fluxcd/kustomize-controller"},
}
fluxHelmController = Owner{
Controller: "helm-controller",
Namespace: "flux-system",
SelectorKey: "app",
SelectorValue: "helm-controller",
AllowConditionalCleanup: true,
Images: []string{"fluxcd/helm-controller"},
// Other projects also publish a controller named helm-controller.
requireImageMatch: true,
}
fluxSourceController = Owner{
Controller: "source-controller",
Namespace: "flux-system",
SelectorKey: "app",
SelectorValue: "source-controller",
AllowConditionalCleanup: true,
Images: []string{"fluxcd/source-controller"},
}
fluxNotificationController = Owner{
Controller: "notification-controller",
Namespace: "flux-system",
SelectorKey: "app",
SelectorValue: "notification-controller",
AllowConditionalCleanup: true,
Images: []string{"fluxcd/notification-controller"},
}
fluxImageController = Owner{
Controller: "image-reflector-controller",
Namespace: "flux-system",
SelectorKey: "app",
SelectorValue: "image-reflector-controller",
AllowConditionalCleanup: true,
Images: []string{"fluxcd/image-reflector-controller"},
}
fluxImageAutomationController = Owner{
Controller: "image-automation-controller",
Namespace: "flux-system",
SelectorKey: "app",
SelectorValue: "image-automation-controller",
AllowConditionalCleanup: true,
Images: []string{"fluxcd/image-automation-controller"},
}
)
// MatchesWorkload reports whether a workload runs this owner's controller,
// recognized by the workload's name or labels, or by its pod template's
// labels or container image.
func (o *Owner) MatchesWorkload(workload metav1.Object, template corev1.PodTemplateSpec) bool {
return (!o.requireImageMatch && o.MatchesWorkloadIdentity(workload, template)) || o.MatchesPod(template.Labels, template.Spec)
}
// MatchesWorkloadIdentity only checks names and labels. When an image is
// required, this resemblance must not establish ownership or absence.
func (o *Owner) MatchesWorkloadIdentity(workload metav1.Object, template corev1.PodTemplateSpec) bool {
return workload.GetLabels()[o.SelectorKey] == o.SelectorValue || controllerNameMatches(workload.GetName(), o.Controller) || template.Labels[o.SelectorKey] == o.SelectorValue
}
// MatchesPod reports whether a pod (or pod template) runs this owner's
// controller, recognized by the catalog label or a container image.
func (o *Owner) MatchesPod(labels map[string]string, spec corev1.PodSpec) bool {
if !o.requireImageMatch && labels[o.SelectorKey] == o.SelectorValue {
return true
}
for _, container := range spec.Containers {
if o.runsImage(container) {
return true
}
}
return false
}
func (o *Owner) runsImage(container corev1.Container) bool {
repository := imageRepositoryPath(container.Image)
if !slices.ContainsFunc(o.Images, func(image string) bool {
return repository == image || strings.HasSuffix(repository, "/"+image)
}) {
return false
}
return o.matchesImageRole == nil || o.matchesImageRole(container)
}
func runsVictoriaMetricsOperator(container corev1.Container) bool {
// The reloader shares the operator's repository; its container name
// still identifies that role when the image is pinned by digest.
image, _, _ := strings.Cut(container.Image, "@")
_, tag, _ := strings.Cut(image[strings.LastIndex(image, "/")+1:], ":")
return container.Name != "config-reloader" && !strings.HasPrefix(tag, "config-reloader")
}
func runsArgoApplicationController(container corev1.Container) bool {
for _, token := range slices.Concat(container.Command, container.Args) {
if path.Base(token) == "argocd-application-controller" {
return true
}
}
return false
}
// imageRepositoryPath returns an image reference's repository path without
// its registry host, tag or digest: "ghcr.io/fluxcd/helm-controller:v1@sha256:…"
// and "fluxcd/helm-controller" both give "fluxcd/helm-controller".
func imageRepositoryPath(image string) string {
ref, _, _ := strings.Cut(image, "@")
if i := strings.LastIndex(ref, ":"); i > strings.LastIndex(ref, "/") {
ref = ref[:i]
}
// Like Docker, the first segment is a registry host only when it has a dot
// or a port, or is localhost; otherwise it is a Docker Hub namespace.
if host, rest, ok := strings.Cut(ref, "/"); ok && (strings.ContainsAny(host, ".:") || host == "localhost") {
ref = rest
}
return ref
}
// ControllerCandidate reports a workload whose name or labels match
// an uncataloged finalizer's API group: the group's first DNS label plus
// "-operator" or "operator-". The match names a candidate only. A short label
// can match an unrelated operator (group cluster.example.io matches
// rabbitmq-cluster-operator), so callers must not treat the candidate as the
// finalizer's owner: not to judge it stopped or gone, not to raise severity,
// not to offer removing the finalizer. Domain alone never matches.
func ControllerCandidate(finalizer string, root *unstructured.Unstructured, workload metav1.Object) bool {
if ResolveOwner(finalizer, root) != nil {
return false
}
domain, _, qualified := strings.Cut(finalizer, "/")
group := resourceid.GroupFromAPIVersion(root.GetAPIVersion())
if !qualified || domain != group || group == "k8s.io" || group == "kubernetes.io" || strings.HasSuffix(group, ".k8s.io") || strings.HasSuffix(group, ".kubernetes.io") {
return false
}
token, _, _ := strings.Cut(group, ".")
if token == "" || token == "operator" || token == "operators" || token == "apps" {
return false
}
for _, value := range []string{workload.GetName(), workload.GetLabels()["app.kubernetes.io/name"], workload.GetLabels()["app.kubernetes.io/part-of"]} {
if controllerNameMatches(value, token+"-operator") || controllerNameMatches(value, "operator-"+token) {
return true
}
}
return false
}
func controllerNameMatches(name, controller string) bool {
return name == controller || strings.HasPrefix(name, controller+"-") || strings.HasSuffix(name, "-"+controller) || strings.Contains(name, "-"+controller+"-")
}