finalizer

package
v0.13.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 29, 2025 License: Apache-2.0 Imports: 12 Imported by: 0

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)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL