 Documentation
      ¶
      Documentation
      ¶
    
    
  
    
  
    Overview ¶
Package finalizer provides utilities for managing Kubernetes finalizers in controller applications.
The core functions GetAddFunc and GetRemoveFunc return functions that can add or remove finalizers from Kubernetes objects. These use get-modify-write instead of apply/patch operations to ensure they cannot recreate objects that have been marked for deletion.
The SetFinalizerHandler provides a handler that automatically adds finalizers to objects that don't already have them, excluding objects with deletion timestamps.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type AddFunc ¶
type AddFunc[K component.KubeObject] func(ctx context.Context, nn types.NamespacedName) (K, error)
func GetAddFunc ¶
func GetAddFunc[K component.KubeObject](client dynamic.Interface, gvr schema.GroupVersionResource, finalizer, fieldManager string) AddFunc[K]
GetAddFunc returns a function that does a get-modify-write instead of an apply, so that it can't accidentally re-create a deleted object (apply does an upsert).
Example ¶
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Create a test object without a finalizer
testObj := &unstructured.Unstructured{
	Object: map[string]interface{}{
		"apiVersion": "example.com/v1",
		"kind":       "MyObject",
		"metadata": map[string]interface{}{
			"name":      "my-object",
			"namespace": "default",
		},
	},
}
// Create a fake dynamic client with a basic scheme
scheme := runtime.NewScheme()
dynamicClient := fake.NewSimpleDynamicClient(scheme, testObj)
// Define the GVR for our resource
gvr := schema.GroupVersionResource{
	Group:    "example.com",
	Version:  "v1",
	Resource: "myobjects",
}
// Create an add function for finalizers
addFunc := GetAddFunc[*metav1.PartialObjectMetadata](
	dynamicClient,
	gvr,
	"my-controller.example.com/finalizer",
	"my-controller",
)
// Use the function to add a finalizer
result, err := addFunc(ctx, types.NamespacedName{
	Name:      "my-object",
	Namespace: "default",
})
if err != nil {
	fmt.Printf("Error: %v", err)
	return
}
fmt.Printf("Has finalizer: %v", len(result.GetFinalizers()) > 0)
Output: Has finalizer: true
Example ¶
package main
import (
	"context"
	"fmt"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/dynamic/fake"
	"github.com/authzed/controller-idioms/finalizer"
)
func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	// Create a test object without a finalizer
	testObj := &unstructured.Unstructured{
		Object: map[string]interface{}{
			"apiVersion": "example.com/v1",
			"kind":       "MyObject",
			"metadata": map[string]interface{}{
				"name":      "test-object",
				"namespace": "default",
			},
		},
	}
	// Create a fake dynamic client with the test object
	scheme := runtime.NewScheme()
	dynamicClient := fake.NewSimpleDynamicClient(scheme, testObj)
	// Define the GVR for our resource
	gvr := schema.GroupVersionResource{
		Group:    "example.com",
		Version:  "v1",
		Resource: "myobjects",
	}
	// Create an add function for finalizers
	addFunc := finalizer.GetAddFunc[*metav1.PartialObjectMetadata](
		dynamicClient,
		gvr,
		"my-controller.example.com/finalizer",
		"my-controller",
	)
	// Use the function to add a finalizer
	result, err := addFunc(ctx, types.NamespacedName{
		Name:      "test-object",
		Namespace: "default",
	})
	if err != nil {
		fmt.Printf("Error adding finalizer: %v\n", err)
		return
	}
	fmt.Printf("Finalizer added: %v\n", len(result.GetFinalizers()) > 0)
	fmt.Printf("Finalizer name: %s\n", result.GetFinalizers()[0])
}
Output: Finalizer added: true Finalizer name: my-controller.example.com/finalizer
Example (AlreadyHasFinalizer) ¶
package main
import (
	"context"
	"fmt"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/dynamic/fake"
	"github.com/authzed/controller-idioms/finalizer"
)
func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	// Create a test object that already has the finalizer
	testObj := &unstructured.Unstructured{
		Object: map[string]interface{}{
			"apiVersion": "example.com/v1",
			"kind":       "MyObject",
			"metadata": map[string]interface{}{
				"name":      "test-object",
				"namespace": "default",
				"finalizers": []interface{}{
					"my-controller.example.com/finalizer",
				},
			},
		},
	}
	// Create a fake dynamic client with the test object
	scheme := runtime.NewScheme()
	dynamicClient := fake.NewSimpleDynamicClient(scheme, testObj)
	// Define the GVR for our resource
	gvr := schema.GroupVersionResource{
		Group:    "example.com",
		Version:  "v1",
		Resource: "myobjects",
	}
	// Create an add function for finalizers
	addFunc := finalizer.GetAddFunc[*metav1.PartialObjectMetadata](
		dynamicClient,
		gvr,
		"my-controller.example.com/finalizer",
		"my-controller",
	)
	// Use the function - it should be a no-op since finalizer already exists
	result, err := addFunc(ctx, types.NamespacedName{
		Name:      "test-object",
		Namespace: "default",
	})
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	fmt.Printf("Finalizer count: %d\n", len(result.GetFinalizers()))
	fmt.Printf("Still has finalizer: %v\n", result.GetFinalizers()[0] == "my-controller.example.com/finalizer")
}
Output: Finalizer count: 1 Still has finalizer: true
type RemoveFunc ¶
type RemoveFunc[K component.KubeObject] func(ctx context.Context, nn types.NamespacedName) (K, error)
func GetRemoveFunc ¶
func GetRemoveFunc[K component.KubeObject](client dynamic.Interface, gvr schema.GroupVersionResource, finalizer, fieldManager string) RemoveFunc[K]
GetRemoveFunc returns a function that does a get-modify-write instead of an apply. Unlike when adding, this won't be called in cases that could potentially re-create a deleted object, but we do it this way so that the field manager operation matches what created the finalizer (`Update`).
Example ¶
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
now := metav1.Now()
// Create a test object with a finalizer and deletion timestamp
testObj := &unstructured.Unstructured{
	Object: map[string]interface{}{
		"apiVersion": "example.com/v1",
		"kind":       "MyObject",
		"metadata": map[string]interface{}{
			"name":      "my-object",
			"namespace": "default",
			"finalizers": []interface{}{
				"my-controller.example.com/finalizer",
			},
			"deletionTimestamp": now.Format("2006-01-02T15:04:05Z"),
		},
	},
}
// Create a fake dynamic client with a basic scheme
scheme := runtime.NewScheme()
dynamicClient := fake.NewSimpleDynamicClient(scheme, testObj)
// Define the GVR for our resource
gvr := schema.GroupVersionResource{
	Group:    "example.com",
	Version:  "v1",
	Resource: "myobjects",
}
// Create a remove function for finalizers
removeFunc := GetRemoveFunc[*metav1.PartialObjectMetadata](
	dynamicClient,
	gvr,
	"my-controller.example.com/finalizer",
	"my-controller",
)
// Use the function to remove a finalizer
result, err := removeFunc(ctx, types.NamespacedName{
	Name:      "my-object",
	Namespace: "default",
})
if err != nil {
	fmt.Printf("Error: %v", err)
	return
}
fmt.Printf("Finalizer removed: %v", len(result.GetFinalizers()) == 0)
Output: Finalizer removed: true
Example ¶
package main
import (
	"context"
	"fmt"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/dynamic/fake"
	"github.com/authzed/controller-idioms/finalizer"
)
func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	now := metav1.Now()
	// Create a test object with a finalizer and deletion timestamp
	testObj := &unstructured.Unstructured{
		Object: map[string]interface{}{
			"apiVersion": "example.com/v1",
			"kind":       "MyObject",
			"metadata": map[string]interface{}{
				"name":      "test-object",
				"namespace": "default",
				"finalizers": []interface{}{
					"my-controller.example.com/finalizer",
				},
				"deletionTimestamp": now.Format("2006-01-02T15:04:05Z"),
			},
		},
	}
	// Create a fake dynamic client with the test object
	scheme := runtime.NewScheme()
	dynamicClient := fake.NewSimpleDynamicClient(scheme, testObj)
	// Define the GVR for our resource
	gvr := schema.GroupVersionResource{
		Group:    "example.com",
		Version:  "v1",
		Resource: "myobjects",
	}
	// Create a remove function for finalizers
	removeFunc := finalizer.GetRemoveFunc[*metav1.PartialObjectMetadata](
		dynamicClient,
		gvr,
		"my-controller.example.com/finalizer",
		"my-controller",
	)
	// Use the function to remove a finalizer
	result, err := removeFunc(ctx, types.NamespacedName{
		Name:      "test-object",
		Namespace: "default",
	})
	if err != nil {
		fmt.Printf("Error removing finalizer: %v\n", err)
		return
	}
	fmt.Printf("Finalizer removed: %v\n", len(result.GetFinalizers()) == 0)
	fmt.Printf("Has deletion timestamp: %v\n", result.GetDeletionTimestamp() != nil)
}
Output: Finalizer removed: true Has deletion timestamp: true
type SetFinalizerHandler ¶
type SetFinalizerHandler[K component.KubeObject] struct { FinalizeableObjectCtxKey ctxKey[K] Finalizer string AddFinalizer AddFunc[K] RequeueAPIErr func(context.Context, error) Next handler.Handler }
Example ¶
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Create context key for our object
CtxObject := typedctx.WithDefault[*metav1.PartialObjectMetadata](nil)
// Mock object without finalizer
obj := &metav1.PartialObjectMetadata{
	ObjectMeta: metav1.ObjectMeta{
		Name:      "my-object",
		Namespace: "default",
	},
}
// Add object to context
ctx = CtxObject.WithValue(ctx, obj)
// Create the handler
handler := &SetFinalizerHandler[*metav1.PartialObjectMetadata]{
	FinalizeableObjectCtxKey: CtxObject,
	Finalizer:                "my-controller.example.com/finalizer",
	AddFinalizer: func(_ context.Context, nn types.NamespacedName) (*metav1.PartialObjectMetadata, error) {
		// Mock successful finalizer addition
		return &metav1.PartialObjectMetadata{
			ObjectMeta: metav1.ObjectMeta{
				Name:       nn.Name,
				Namespace:  nn.Namespace,
				Finalizers: []string{"my-controller.example.com/finalizer"},
			},
		}, nil
	},
	RequeueAPIErr: func(_ context.Context, err error) {
		fmt.Printf("Requeue due to error: %v", err)
	},
	Next: handler.NewHandlerFromFunc(func(_ context.Context) {
		fmt.Println("Processing next handler")
	}, "next"),
}
// Execute the handler
handler.Handle(ctx)
Output: Processing next handler
func (*SetFinalizerHandler[K]) Handle ¶
func (s *SetFinalizerHandler[K]) Handle(ctx context.Context)