mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2026-05-04 22:26:11 +02:00
* refactor(source/crd): migrate CRD source to controller-runtime cache Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactor(source/crd): migrate CRD source to controller-runtime cache Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactor(source/crd): migrate CRD source to controller-runtime cache Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactor(source/crd): migrate CRD source to controller-runtime cache Co-authored-by: vflaux <38909103+vflaux@users.noreply.github.com> * refactor(source/crd): migrate CRD source to controller-runtime cache Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactor(source/crd): migrate CRD source to controller-runtime cache Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactor(source/crd): migrate CRD source to controller-runtime cache Co-authored-by: vflaux <38909103+vflaux@users.noreply.github.com> * refactor(source/crd): migrate CRD source to controller-runtime cache Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactor(source/crd): migrate CRD source to controller-runtime cache Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactor(source/crd): migrate CRD source to controller-runtime cache Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactor(source/crd): migrate CRD source to controller-runtime cache Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactor(source/crd): migrate CRD source to controller-runtime cache Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactor(source/crd): migrate CRD source to controller-runtime cache Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactor(source/crd): migrate CRD source to controller-runtime cache Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactor(source/crd): migrate CRD source to controller-runtime cache Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactor(source/crd): migrate CRD source to controller-runtime cache Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactor(source/crd): migrate CRD source to controller-runtime cache Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactor(source/crd): migrate CRD source to controller-runtime cache Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> --------- Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> Co-authored-by: vflaux <38909103+vflaux@users.noreply.github.com>
204 lines
7.2 KiB
Go
204 lines
7.2 KiB
Go
/*
|
|
Copyright 2025 The Kubernetes Authors.
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package informers
|
|
|
|
import (
|
|
"fmt"
|
|
"maps"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/client-go/kubernetes/scheme"
|
|
"k8s.io/client-go/tools/cache"
|
|
)
|
|
|
|
// TransformOptions holds the configuration for TransformerWithOptions.
|
|
// All options operate on the metav1.Object interface (or via reflection for Status
|
|
// fields) and are therefore applicable to any Kubernetes resource type.
|
|
type TransformOptions struct {
|
|
removeManagedFields bool
|
|
removeLastAppliedConfig bool
|
|
removeStatusConditions bool
|
|
keepAnnotationPrefixes []string
|
|
requireAnnotationSelector labels.Selector
|
|
}
|
|
|
|
// TransformRemoveManagedFields strips managedFields from the object's metadata.
|
|
// managedFields are written by server-side apply and can be megabytes per object.
|
|
func TransformRemoveManagedFields() func(*TransformOptions) {
|
|
return func(o *TransformOptions) {
|
|
o.removeManagedFields = true
|
|
}
|
|
}
|
|
|
|
// TransformRemoveLastAppliedConfig removes the kubectl.kubernetes.io/last-applied-configuration
|
|
// annotation, which stores a full JSON snapshot of the resource and can be very large.
|
|
func TransformRemoveLastAppliedConfig() func(*TransformOptions) {
|
|
return func(o *TransformOptions) {
|
|
o.removeLastAppliedConfig = true
|
|
}
|
|
}
|
|
|
|
// TransformRemoveStatusConditions clears the Status.Conditions field if the object has one.
|
|
// Conditions vary by type (metav1.Condition, corev1.PodCondition, corev1.NodeCondition, …)
|
|
// so this is applied via reflection and is a no-op for types without Status.Conditions.
|
|
func TransformRemoveStatusConditions() func(*TransformOptions) {
|
|
return func(o *TransformOptions) {
|
|
o.removeStatusConditions = true
|
|
}
|
|
}
|
|
|
|
// TransformKeepAnnotationPrefix retains only annotations whose keys match at least one of
|
|
// the given prefixes, discarding all others. Multiple prefixes use OR logic; multiple calls
|
|
// also accumulate (OR logic).
|
|
func TransformKeepAnnotationPrefix(prefixes ...string) func(*TransformOptions) {
|
|
return func(o *TransformOptions) {
|
|
o.keepAnnotationPrefixes = append(o.keepAnnotationPrefixes, prefixes...)
|
|
}
|
|
}
|
|
|
|
// TransformRequireAnnotation is a local guard against annotation mutation:
|
|
// the Kubernetes API does not support annotation selectors in List/Watch.
|
|
// Do not use when an indexer handles annotation filtering.
|
|
// A nil or empty selector is a no-op.
|
|
func TransformRequireAnnotation(selector labels.Selector) func(*TransformOptions) {
|
|
return func(o *TransformOptions) {
|
|
o.requireAnnotationSelector = selector
|
|
}
|
|
}
|
|
|
|
// TransformerWithOptions returns a cache.TransformFunc that modifies objects of type T
|
|
// in place to reduce the memory footprint of the informer cache. All options operate
|
|
// on the metav1.Object interface or via reflection, making the transformer applicable
|
|
// to any Kubernetes resource type without type-specific logic.
|
|
//
|
|
// The transformer also populates TypeMeta (Kind/APIVersion) on every object. Kubernetes
|
|
// informers strip TypeMeta when returning objects because the client already knows the
|
|
// type — populating it here makes cached objects self-describing for templates and logging.
|
|
//
|
|
// The transform is naturally idempotent: nil-ing an already-nil field and filtering an
|
|
// already-filtered map are both no-ops, so calling it multiple times on the same object
|
|
// is safe.
|
|
//
|
|
// Example:
|
|
//
|
|
// serviceInformer.Informer().SetTransform(informers.TransformerWithOptions[*corev1.Service](
|
|
// informers.TransformRemoveManagedFields(),
|
|
// informers.TransformRemoveLastAppliedConfig(),
|
|
// ))
|
|
func TransformerWithOptions[T interface {
|
|
metav1.Object
|
|
runtime.Object
|
|
}](optFns ...func(*TransformOptions)) cache.TransformFunc {
|
|
options := TransformOptions{}
|
|
for _, fn := range optFns {
|
|
fn(&options)
|
|
}
|
|
return func(obj any) (any, error) {
|
|
entity, ok := obj.(T)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
if sel := options.requireAnnotationSelector; sel != nil && !sel.Empty() {
|
|
if !sel.Matches(labels.Set(entity.GetAnnotations())) {
|
|
return nil, nil
|
|
}
|
|
}
|
|
populateGVK(entity)
|
|
if options.removeManagedFields {
|
|
entity.SetManagedFields(nil)
|
|
}
|
|
if len(options.keepAnnotationPrefixes) > 0 || options.removeLastAppliedConfig {
|
|
anns := entity.GetAnnotations()
|
|
if options.removeLastAppliedConfig {
|
|
delete(anns, corev1.LastAppliedConfigAnnotation)
|
|
}
|
|
if len(options.keepAnnotationPrefixes) > 0 {
|
|
maps.DeleteFunc(anns, func(k, _ string) bool {
|
|
return !slices.ContainsFunc(options.keepAnnotationPrefixes, func(prefix string) bool {
|
|
return strings.HasPrefix(k, prefix)
|
|
})
|
|
})
|
|
}
|
|
entity.SetAnnotations(anns)
|
|
}
|
|
if options.removeStatusConditions {
|
|
clearStatusConditions(entity)
|
|
}
|
|
return entity, nil
|
|
}
|
|
}
|
|
|
|
// MustSetTransform calls SetTransform on the informer and panics on error.
|
|
// SetTransform only errors if the informer has already been started, which is a
|
|
// programming error — callers must invoke it before factory.Start().
|
|
func MustSetTransform(informer cache.SharedInformer, fn cache.TransformFunc) {
|
|
if err := informer.SetTransform(fn); err != nil {
|
|
panic(fmt.Sprintf("SetTransform called on started informer: %v", err))
|
|
}
|
|
}
|
|
|
|
// populateGVK restores TypeMeta (Kind/APIVersion) stripped by the Kubernetes informer.
|
|
func populateGVK(obj runtime.Object) {
|
|
gvk := obj.GetObjectKind().GroupVersionKind()
|
|
if gvk.Kind != "" {
|
|
return
|
|
}
|
|
gvks, _, err := scheme.Scheme.ObjectKinds(obj)
|
|
if err == nil && len(gvks) > 0 {
|
|
gvk = gvks[0]
|
|
} else {
|
|
// Fallback to reflection for types not registered in the scheme.
|
|
gvk = schema.GroupVersionKind{Kind: reflect.TypeOf(obj).Elem().Name()}
|
|
}
|
|
obj.GetObjectKind().SetGroupVersionKind(gvk)
|
|
}
|
|
|
|
// clearStatusConditions zeroes out the Status.Conditions field on obj if it exists.
|
|
// It handles all condition types (metav1.Condition, corev1.PodCondition, etc.) uniformly.
|
|
// Reflection is used because Status.Conditions is a structural convention shared by all
|
|
// Kubernetes types but not codified in any interface — element types differ per resource.
|
|
// The reflection cost is negligible: paid once per object at cache-population time,
|
|
// not on the hot path of every endpoint reconciliation.
|
|
func clearStatusConditions(obj any) {
|
|
val := reflect.ValueOf(obj)
|
|
if val.Kind() == reflect.Pointer {
|
|
val = val.Elem()
|
|
}
|
|
if !val.IsValid() {
|
|
return
|
|
}
|
|
statusField := val.FieldByName("Status")
|
|
if !statusField.IsValid() {
|
|
return
|
|
}
|
|
// Status may itself be a pointer (dereference if so)
|
|
if statusField.Kind() == reflect.Pointer {
|
|
if statusField.IsNil() {
|
|
return
|
|
}
|
|
statusField = statusField.Elem()
|
|
}
|
|
condField := statusField.FieldByName("Conditions")
|
|
if condField.IsValid() && condField.CanSet() {
|
|
condField.Set(reflect.Zero(condField.Type()))
|
|
}
|
|
}
|