k8s-operator/reconciler/reconciler.go
Rama main · réplica de lectura del repositorio original.
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !plan9
// Package reconciler provides utilities for working with Kubernetes resources within controller reconciliation
// loops.
package reconciler
import (
"context"
"fmt"
"slices"
"strings"
"sync"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"tailscale.com/kube/kubetypes"
"tailscale.com/util/clientmetric"
"tailscale.com/util/set"
)
const (
// Finalizer is the common finalizer used across all Tailscale Kubernetes resources.
Finalizer = "tailscale.com/finalizer"
// LabelParentType identifies which Tailscale CRD kind owns a managed resource. Every resource that a Tailscale
// CRD reconciler creates should carry this label alongside LabelParentName.
LabelParentType = "tailscale.com/parent-resource-type"
// LabelParentName identifies the name of the Tailscale CRD that owns a managed resource. Combined with
// LabelParentType, this uniquely identifies the parent within its scope.
LabelParentName = "tailscale.com/parent-resource"
// LabelParentNamespace identifies the namespace of the owning Tailscale CRD. It is only stamped when the parent
// CRD is namespaced; cluster-scoped parents omit it.
LabelParentNamespace = "tailscale.com/parent-resource-ns"
// optimisticLockErrorMsg is the message the API server returns when an Update is rejected because the object
// has been modified since it was read. The API server does not give us a typed error for the conflict in all
// cases, so callers match on the message via IsOptimisticLockError.
optimisticLockErrorMsg = "the object has been modified; please apply your changes to the latest version and try again"
)
// IsOptimisticLockError reports whether err was caused by an optimistic locking conflict, i.e. the object being
// updated was modified after the reconciler read it. Such an error is transient: the reconciler should requeue and
// retry against a freshly read object rather than surface the error.
func IsOptimisticLockError(err error) bool {
return err != nil && strings.Contains(err.Error(), optimisticLockErrorMsg)
}
// SetFinalizer adds name to obj's finalizers if not already present. Most CRD reconcilers pass Finalizer; per-
// reconciler names are used when a single resource may carry finalizers from multiple reconcilers that each need to run
// their own cleanup.
func SetFinalizer(obj client.Object, name string) {
if idx := slices.Index(obj.GetFinalizers(), name); idx >= 0 {
return
}
obj.SetFinalizers(append(obj.GetFinalizers(), name))
}
// RemoveFinalizer removes name from obj's finalizers if present.
func RemoveFinalizer(obj client.Object, name string) {
idx := slices.Index(obj.GetFinalizers(), name)
if idx < 0 {
return
}
finalizers := obj.GetFinalizers()
obj.SetFinalizers(append(finalizers[:idx], finalizers[idx+1:]...))
}
// EnsureFinalizer adds name to obj (if not already present) and persists the change via cl.Update. It is a no-op when
// the finalizer is already set. Callers should invoke it early in a create/update reconcile so cleanup logic gets a
// chance to run before the resource is garbage collected. Most callers pass Finalizer; per-reconciler names are
// used when a single resource may carry finalizers from multiple reconcilers.
func EnsureFinalizer(ctx context.Context, cl client.Client, obj client.Object, name string) error {
if slices.Contains(obj.GetFinalizers(), name) {
return nil
}
SetFinalizer(obj, name)
return cl.Update(ctx, obj)
}
// ClearFinalizer removes name from obj (if present) and persists the change via cl.Update. It is a no-op when the
// finalizer is already absent. Callers should invoke it at the end of delete handling, once all owned resources are
// gone, so the API server can garbage collect obj.
func ClearFinalizer(ctx context.Context, cl client.Client, obj client.Object, name string) error {
if !slices.Contains(obj.GetFinalizers(), name) {
return nil
}
RemoveFinalizer(obj, name)
return cl.Update(ctx, obj)
}
// Labels returns the standard ownership labels stamped on every resource a Tailscale CRD reconciler creates:
// tailscale.com/managed=true, plus LabelParentType and LabelParentName. If parentNamespace is non-empty (i.e. the
// parent CRD is namespaced) it is stamped as LabelParentNamespace; cluster-scoped parents pass "".
func Labels(parentType, parentName, parentNamespace string) map[string]string {
labels := map[string]string{
kubetypes.LabelManaged: "true",
LabelParentType: parentType,
LabelParentName: parentName,
}
if parentNamespace != "" {
labels[LabelParentNamespace] = parentNamespace
}
return labels
}
// EnqueueForChild returns a handler.MapFunc that enqueues a reconcile.Request for the parent CRD of a managed child
// resource. It filters by tailscale.com/managed=true and LabelParentType, and derives the request's NamespacedName
// from LabelParentName plus LabelParentNamespace (blank namespace for cluster-scoped parents). Use it on Watches of
// child resources so drift or cloud-controller updates propagate to the owning CRD's reconciler.
func EnqueueForChild(parentType string) handler.MapFunc {
return func(_ context.Context, o client.Object) []reconcile.Request {
if !IsManagedByType(o, parentType) {
return nil
}
parent := ParentFromObjectLabels(o)
if parent.Name == "" {
return nil
}
return []reconcile.Request{{NamespacedName: parent}}
}
}
// IsManagedResource reports whether obj carries the tailscale.com/managed=true label — i.e. it is a child resource
// stamped by a Tailscale CRD reconciler.
func IsManagedResource(obj client.Object) bool {
return obj.GetLabels()[kubetypes.LabelManaged] == "true"
}
// IsManagedByType reports whether obj is a managed child resource whose LabelParentType matches parentType.
func IsManagedByType(obj client.Object, parentType string) bool {
return IsManagedResource(obj) && obj.GetLabels()[LabelParentType] == parentType
}
// SelectorForType returns the label selector that matches every managed child resource with the given parent type.
// Pass it to client.MatchingLabels when Listing children by type; it is the label-map counterpart of the IsManagedByType
// predicate.
func SelectorForType(parentType string) map[string]string {
return map[string]string{
kubetypes.LabelManaged: "true",
LabelParentType: parentType,
}
}
// ParentFromObjectLabels returns the NamespacedName of the parent CRD encoded in obj's LabelParentName /
// LabelParentNamespace labels. The Namespace is empty for children of cluster-scoped parents.
func ParentFromObjectLabels(obj client.Object) types.NamespacedName {
labels := obj.GetLabels()
return types.NamespacedName{
Name: labels[LabelParentName],
Namespace: labels[LabelParentNamespace],
}
}
// ResourceTracker tracks the set of CRD UIDs a reconciler currently manages and mirrors the set's size to a
// clientmetric gauge. Callers Add on each successful reconcile and Remove on cleanup; the gauge stays in sync with
// the finalizer lifecycle so it reflects the live count of managed resources across restarts. ResourceTracker is
// safe for concurrent use.
type ResourceTracker struct {
gauge *clientmetric.Metric
mu sync.Mutex // protects uids
uids set.Slice[types.UID]
}
// NewResourceTracker returns a ResourceTracker that mirrors its size to the given gauge.
func NewResourceTracker(gauge *clientmetric.Metric) *ResourceTracker {
return &ResourceTracker{gauge: gauge}
}
// Add records uid as managed. Duplicate adds are a no-op.
func (t *ResourceTracker) Add(uid types.UID) {
t.mu.Lock()
defer t.mu.Unlock()
if t.uids.Contains(uid) {
return
}
t.uids.Add(uid)
t.gauge.Set(int64(t.uids.Len()))
}
// Remove drops uid from the tracked set. It is a no-op if uid is not currently tracked.
func (t *ResourceTracker) Remove(uid types.UID) {
t.mu.Lock()
defer t.mu.Unlock()
t.uids.Remove(uid)
t.gauge.Set(int64(t.uids.Len()))
}
// Len returns the number of UIDs currently tracked.
func (t *ResourceTracker) Len() int {
t.mu.Lock()
defer t.mu.Unlock()
return t.uids.Len()
}
// PtrObject is a type constraint for pointer types that implement client.Object.
type PtrObject[T any] interface {
client.Object
*T
}
// CreateOrMaybeUpdate adds obj to the k8s cluster, unless the object already exists,
// in which case update is called to make changes to it. If update is nil or returns
// an error, the object is returned unmodified.
//
// obj is looked up by its Name and Namespace if Name is set, otherwise it's
// looked up by labels.
func CreateOrMaybeUpdate[T any, O PtrObject[T]](ctx context.Context, c client.Client, ns string, obj O, update func(O) error) (O, error) {
var (
existing O
err error
)
if obj.GetName() != "" {
existing = new(T)
existing.SetName(obj.GetName())
existing.SetNamespace(obj.GetNamespace())
err = c.Get(ctx, client.ObjectKeyFromObject(obj), existing)
} else {
existing, err = GetSingleObject[T, O](ctx, c, ns, obj.GetLabels())
}
if err == nil && existing != nil {
if update != nil {
if err := update(existing); err != nil {
return nil, err
}
if err := c.Update(ctx, existing); err != nil {
return nil, err
}
}
return existing, nil
}
if err != nil && !apierrors.IsNotFound(err) {
return nil, fmt.Errorf("failed to get object: %w", err)
}
if err := c.Create(ctx, obj); err != nil {
return nil, err
}
return obj, nil
}
// CreateOrUpdate adds obj to the k8s cluster, unless the object already exists,
// in which case update is called to make changes to it. If update is nil, the
// existing object is returned unmodified.
//
// obj is looked up by its Name and Namespace if Name is set, otherwise it's
// looked up by labels.
func CreateOrUpdate[T any, O PtrObject[T]](ctx context.Context, c client.Client, ns string, obj O, update func(O)) (O, error) {
return CreateOrMaybeUpdate(ctx, c, ns, obj, func(o O) error {
if update != nil {
update(o)
}
return nil
})
}
// GetSingleObject searches for k8s objects of type T (e.g. corev1.Service) with
// the given labels, and returns it. Returns nil if no objects match the labels,
// and an error if more than one object matches.
func GetSingleObject[T any, O PtrObject[T]](ctx context.Context, c client.Client, ns string, labels map[string]string) (O, error) {
ret := O(new(T))
kinds, _, err := c.Scheme().ObjectKinds(ret)
if err != nil {
return nil, err
}
if len(kinds) != 1 {
// TODO: the runtime package apparently has a "pick the best
// GVK" function somewhere that might be good enough?
return nil, fmt.Errorf("more than 1 GroupVersionKind for %T", ret)
}
gvk := kinds[0]
gvk.Kind += "List"
lst := unstructured.UnstructuredList{}
lst.SetGroupVersionKind(gvk)
if err := c.List(ctx, &lst, client.InNamespace(ns), client.MatchingLabels(labels)); err != nil {
return nil, err
}
if len(lst.Items) == 0 {
return nil, nil
}
if len(lst.Items) > 1 {
return nil, fmt.Errorf("found multiple matching %T objects", ret)
}
if err := c.Scheme().Convert(&lst.Items[0], ret, nil); err != nil {
return nil, err
}
return ret, nil
}