From e6fc517697720470de7398ac07fa61115b6deaca Mon Sep 17 00:00:00 2001 From: Anitha Natarajan Date: Tue, 16 Sep 2025 15:03:03 +0530 Subject: [PATCH] Add annotation and label-based selector support for pruning configuration This commit introduces support for configuring pruning policies using annotation and label selectors, enabling fine-grained control over resource cleanup based on resource metadata. Previously, pruning configuration was limited to name-based matching. This enhancement allows users to define pruning policies that apply to resources matching specific annotations or labels, enabling different cleanup strategies for different types of workloads within the same namespace. Key features added: - Annotation-based selectors for pruning configuration - Label-based selectors for pruning configuration - Selector-based field isolation (no fallback when selector matches) - Enhanced configuration hierarchy: selector > namespace > global Changes made: - Add getFromPrunerConfigResourceLevelwithSelector function to support selector-based configuration lookup with proper fallback logic - Add checkIfResourceMatchesAnySelector function for determining if resources match any configured selector - Implement field isolation to prevent namespace/global fallback when resource matches a selector but specific field is undefined - Fix annotation-based resource filtering in history limiter by using in-memory filtering instead of invalid label selectors - Add support for fallback from specific history limits to generic historyLimit field in all matching scenarios --- pkg/config/config.go | 102 ++++++++++++++++++++++++---------- pkg/config/history_limiter.go | 35 +++++++++--- 2 files changed, 100 insertions(+), 37 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 9edb08bb..6ec532b1 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -158,9 +158,14 @@ func (ps *prunerConfigStore) WorkerCount(ctx context.Context, configMap *corev1. } func getFromPrunerConfigResourceLevelwithSelector(namespacesSpec map[string]NamespaceSpec, namespace, name string, selector SelectorSpec, resourceType PrunerResourceType, fieldType PrunerFieldType) (*int32, string) { + fieldData, identifiedBy, _ := getFromPrunerConfigResourceLevelwithSelectorAndMatch(namespacesSpec, namespace, name, selector, resourceType, fieldType) + return fieldData, identifiedBy +} + +func getFromPrunerConfigResourceLevelwithSelectorAndMatch(namespacesSpec map[string]NamespaceSpec, namespace, name string, selector SelectorSpec, resourceType PrunerResourceType, fieldType PrunerFieldType) (*int32, string, bool) { prunerResourceSpec, found := namespacesSpec[namespace] if !found { - return nil, "identifiedBy_global" + return nil, "identifiedBy_global", false } var resourceSpecs []ResourceSpec @@ -174,31 +179,42 @@ func getFromPrunerConfigResourceLevelwithSelector(namespacesSpec map[string]Name } // First, check if name is provided, and use it to match exactly - if name != "" && (len(selector.MatchAnnotations) == 0 || len(selector.MatchLabels) == 0) { + if name != "" { for _, resourceSpec := range resourceSpecs { if resourceSpec.Name == name { // Return the field value from the matched resourceSpec switch fieldType { case PrunerFieldTypeTTLSecondsAfterFinished: - return resourceSpec.TTLSecondsAfterFinished, "identifiedBy_resource_name" + return resourceSpec.TTLSecondsAfterFinished, "identifiedBy_resource_name", true case PrunerFieldTypeSuccessfulHistoryLimit: - return resourceSpec.SuccessfulHistoryLimit, "identifiedBy_resource_name" + if resourceSpec.SuccessfulHistoryLimit != nil { + return resourceSpec.SuccessfulHistoryLimit, "identifiedBy_resource_name", true + } else { + return resourceSpec.HistoryLimit, "identifiedBy_resource_name", true + } case PrunerFieldTypeFailedHistoryLimit: - return resourceSpec.FailedHistoryLimit, "identifiedBy_resource_name" + if resourceSpec.FailedHistoryLimit != nil { + return resourceSpec.FailedHistoryLimit, "identifiedBy_resource_name", true + } else { + return resourceSpec.HistoryLimit, "identifiedBy_resource_name", true + } } } } - } else if len(selector.MatchAnnotations) > 0 || len(selector.MatchLabels) > 0 { + } + + // If name-based matching didn't find a match, try selector-based matching + if len(selector.MatchAnnotations) > 0 || len(selector.MatchLabels) > 0 { // If name is not provided, we proceed with selector matching for _, resourceSpec := range resourceSpecs { - // Check if the resourceSpec matches the provided selector by annotations or labels + // Check if the resourceSpec's selector matches the provided resource annotations/labels for _, selectorSpec := range resourceSpec.Selector { - // Match by annotations if provided in the selector - if len(selector.MatchAnnotations) > 0 { + // Match by annotations if the resourceSpec has annotation selectors + if len(selectorSpec.MatchAnnotations) > 0 { match := true - for key, value := range selector.MatchAnnotations { - if resourceAnnotationValue, exists := selectorSpec.MatchAnnotations[key]; !exists || resourceAnnotationValue != value { + for key, value := range selectorSpec.MatchAnnotations { + if resourceAnnotationValue, exists := selector.MatchAnnotations[key]; !exists || resourceAnnotationValue != value { match = false break } @@ -207,27 +223,27 @@ func getFromPrunerConfigResourceLevelwithSelector(namespacesSpec map[string]Name // Return the field value if annotations match switch fieldType { case PrunerFieldTypeTTLSecondsAfterFinished: - return resourceSpec.TTLSecondsAfterFinished, "identifiedBy_resource_ann" + return resourceSpec.TTLSecondsAfterFinished, "identifiedBy_resource_ann", true case PrunerFieldTypeSuccessfulHistoryLimit: if resourceSpec.SuccessfulHistoryLimit != nil { - return resourceSpec.SuccessfulHistoryLimit, "identifiedBy_resource_ann" + return resourceSpec.SuccessfulHistoryLimit, "identifiedBy_resource_ann", true } else { - return resourceSpec.HistoryLimit, "identifiedBy_resource_ann" + return resourceSpec.HistoryLimit, "identifiedBy_resource_ann", true } case PrunerFieldTypeFailedHistoryLimit: - if resourceSpec.SuccessfulHistoryLimit != nil { - return resourceSpec.FailedHistoryLimit, "identifiedBy_resource_ann" + if resourceSpec.FailedHistoryLimit != nil { + return resourceSpec.FailedHistoryLimit, "identifiedBy_resource_ann", true } else { - return resourceSpec.HistoryLimit, "identifiedBy_resource_ann" + return resourceSpec.HistoryLimit, "identifiedBy_resource_ann", true } } } } - // Match by labels if provided in the selector - if len(selector.MatchLabels) > 0 { + // Match by labels if the resourceSpec has label selectors + if len(selectorSpec.MatchLabels) > 0 { match := true - for key, value := range selector.MatchLabels { - if resourceLabelValue, exists := selectorSpec.MatchLabels[key]; !exists || resourceLabelValue != value { + for key, value := range selectorSpec.MatchLabels { + if resourceLabelValue, exists := selector.MatchLabels[key]; !exists || resourceLabelValue != value { match = false break } @@ -236,18 +252,18 @@ func getFromPrunerConfigResourceLevelwithSelector(namespacesSpec map[string]Name // Return the field value if labels match switch fieldType { case PrunerFieldTypeTTLSecondsAfterFinished: - return resourceSpec.TTLSecondsAfterFinished, "identifiedBy_resource_label" + return resourceSpec.TTLSecondsAfterFinished, "identifiedBy_resource_label", true case PrunerFieldTypeSuccessfulHistoryLimit: if resourceSpec.SuccessfulHistoryLimit != nil { - return resourceSpec.SuccessfulHistoryLimit, "identifiedBy_resource_label" + return resourceSpec.SuccessfulHistoryLimit, "identifiedBy_resource_label", true } else { - return resourceSpec.HistoryLimit, "identifiedBy_resource_label" + return resourceSpec.HistoryLimit, "identifiedBy_resource_label", true } case PrunerFieldTypeFailedHistoryLimit: - if resourceSpec.SuccessfulHistoryLimit != nil { - return resourceSpec.FailedHistoryLimit, "identifiedBy_resource_label" + if resourceSpec.FailedHistoryLimit != nil { + return resourceSpec.FailedHistoryLimit, "identifiedBy_resource_label", true } else { - return resourceSpec.HistoryLimit, "identifiedBy_resource_label" + return resourceSpec.HistoryLimit, "identifiedBy_resource_label", true } } } @@ -257,7 +273,25 @@ func getFromPrunerConfigResourceLevelwithSelector(namespacesSpec map[string]Name } // If no match found, return nil - return nil, "" + return nil, "", false +} + +// checkIfResourceMatchesAnySelector checks if a resource matches any selector in the namespace config +func checkIfResourceMatchesAnySelector(namespacesSpec map[string]NamespaceSpec, namespace, name string, selector SelectorSpec, resourceType PrunerResourceType) bool { + // Check multiple field types to see if ANY selector matches, regardless of which fields it defines + fieldTypes := []PrunerFieldType{ + PrunerFieldTypeTTLSecondsAfterFinished, + PrunerFieldTypeSuccessfulHistoryLimit, + PrunerFieldTypeFailedHistoryLimit, + } + + for _, fieldType := range fieldTypes { + _, _, matched := getFromPrunerConfigResourceLevelwithSelectorAndMatch(namespacesSpec, namespace, name, selector, resourceType, fieldType) + if matched { + return true + } + } + return false } func getResourceFieldData(globalSpec GlobalConfig, namespace, name string, selector SelectorSpec, resourceType PrunerResourceType, fieldType PrunerFieldType, enforcedConfigLevel EnforcedConfigLevel) (*int32, string) { @@ -266,12 +300,22 @@ func getResourceFieldData(globalSpec GlobalConfig, namespace, name string, selec switch enforcedConfigLevel { case EnforcedConfigLevelResource: + // Check if the resource matches any selector + resourceMatchesSelector := checkIfResourceMatchesAnySelector(globalSpec.Namespaces, namespace, name, selector, resourceType) + // First try resource level fieldData, identified_by = getFromPrunerConfigResourceLevelwithSelector(globalSpec.Namespaces, namespace, name, selector, resourceType, fieldType) if fieldData != nil { return fieldData, identified_by } - // If no resource level config found, try namespace level + + // If resource matches a selector but the specific field is not defined in that selector, + // don't fall back to namespace/global - return nil to indicate the field is not configured + if resourceMatchesSelector { + return nil, "identifiedBy_resource_selector_no_value" + } + + // If no resource level config found and no selector matched, try namespace level spec, found := globalSpec.Namespaces[namespace] if found { switch fieldType { diff --git a/pkg/config/history_limiter.go b/pkg/config/history_limiter.go index 9296360f..7e321d30 100644 --- a/pkg/config/history_limiter.go +++ b/pkg/config/history_limiter.go @@ -23,6 +23,7 @@ import ( "math" "slices" "strconv" + "strings" "time" "github.com/tektoncd/pruner/pkg/metrics" @@ -275,14 +276,9 @@ func (hl *HistoryLimiter) doResourceCleanup(ctx context.Context, resource metav1 label := fmt.Sprintf("%s=%s", labelKey, resourceName) resources, err = hl.resourceFn.List(ctx, resource.GetNamespace(), label) case "identifiedBy_resource_ann": - labelSelector := "" - for k, v := range resourceAnnotations { - if labelSelector != "" { - labelSelector += "," - } - labelSelector += fmt.Sprintf("%s=%s", k, v) - } - resources, err = hl.resourceFn.List(ctx, resource.GetNamespace(), labelSelector) + // For annotation-based selectors, we cannot use label selectors + // Instead, list all resources and filter by annotations in memory + resources, err = hl.resourceFn.List(ctx, resource.GetNamespace(), "") case "identifiedBy_resource_label": labelSelector := "" for k, v := range resourceLabels { @@ -304,6 +300,29 @@ func (hl *HistoryLimiter) doResourceCleanup(ctx context.Context, resource metav1 return err } + // For annotation-based selectors, filter resources by annotations + if enforcedConfigLevel == EnforcedConfigLevelResource && identifiedBy == "identifiedBy_resource_ann" { + annotationFiltered := []metav1.Object{} + for _, res := range resources { + resAnnotations := res.GetAnnotations() + match := true + for k, v := range resourceAnnotations { + // Skip pruner internal annotations + if strings.HasPrefix(k, "pruner.tekton.dev/") { + continue + } + if resAnnotations == nil || resAnnotations[k] != v { + match = false + break + } + } + if match { + annotationFiltered = append(annotationFiltered, res) + } + } + resources = annotationFiltered + } + // Filter resources by status (success/failed) resourcesFiltered := []metav1.Object{} for _, res := range resources {