Skip to content

Commit

Permalink
✨ Add reconcile.ObjectReconciler (#2592)
Browse files Browse the repository at this point in the history
* Add ObjectReconciler

* Add type hints in reconcile_test.go

* Remove Go 1.20 type hints
  • Loading branch information
JamesOwenHall committed Jan 9, 2024
1 parent bf3d274 commit 984a121
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 0 deletions.
32 changes: 32 additions & 0 deletions pkg/reconcile/reconcile.go
Expand Up @@ -19,9 +19,11 @@ package reconcile
import (
"context"
"errors"
"reflect"
"time"

"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// Result contains the result of a Reconciler invocation.
Expand Down Expand Up @@ -110,6 +112,36 @@ var _ Reconciler = Func(nil)
// Reconcile implements Reconciler.
func (r Func) Reconcile(ctx context.Context, o Request) (Result, error) { return r(ctx, o) }

// ObjectReconciler is a specialized version of Reconciler that acts on instances of client.Object. Each reconciliation
// event gets the associated object from Kubernetes before passing it to Reconcile. An ObjectReconciler can be used in
// Builder.Complete by calling AsReconciler. See Reconciler for more details.
type ObjectReconciler[T client.Object] interface {
Reconcile(context.Context, T) (Result, error)
}

// AsReconciler creates a Reconciler based on the given ObjectReconciler.
func AsReconciler[T client.Object](client client.Client, rec ObjectReconciler[T]) Reconciler {
return &objectReconcilerAdapter[T]{
objReconciler: rec,
client: client,
}
}

type objectReconcilerAdapter[T client.Object] struct {
objReconciler ObjectReconciler[T]
client client.Client
}

// Reconcile implements Reconciler.
func (a *objectReconcilerAdapter[T]) Reconcile(ctx context.Context, req Request) (Result, error) {
o := reflect.New(reflect.TypeOf(*new(T)).Elem()).Interface().(T)
if err := a.client.Get(ctx, req.NamespacedName, o); err != nil {
return Result{}, client.IgnoreNotFound(err)
}

return a.objReconciler.Reconcile(ctx, o)
}

// TerminalError is an error that will not be retried but still be logged
// and recorded in metrics.
func TerminalError(wrapped error) error {
Expand Down
83 changes: 83 additions & 0 deletions pkg/reconcile/reconcile_test.go
Expand Up @@ -23,11 +23,23 @@ import (

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

type mockObjectReconciler struct {
reconcileFunc func(context.Context, *corev1.ConfigMap) (reconcile.Result, error)
}

func (r *mockObjectReconciler) Reconcile(ctx context.Context, cm *corev1.ConfigMap) (reconcile.Result, error) {
return r.reconcileFunc(ctx, cm)
}

var _ = Describe("reconcile", func() {
Describe("Result", func() {
It("IsZero should return true if empty", func() {
Expand Down Expand Up @@ -102,4 +114,75 @@ var _ = Describe("reconcile", func() {
Expect(err.Error()).To(Equal("nil terminal error"))
})
})

Describe("AsReconciler", func() {
var testenv *envtest.Environment
var testClient client.Client

BeforeEach(func() {
testenv = &envtest.Environment{}

cfg, err := testenv.Start()
Expect(err).NotTo(HaveOccurred())

testClient, err = client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
})

AfterEach(func() {
Expect(testenv.Stop()).NotTo(HaveOccurred())
})

Context("with an existing object", func() {
var key client.ObjectKey

BeforeEach(func() {
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "test",
},
}
key = client.ObjectKeyFromObject(cm)

err := testClient.Create(context.Background(), cm)
Expect(err).NotTo(HaveOccurred())
})

It("should Get the object and call the ObjectReconciler", func() {
var actual *corev1.ConfigMap
reconciler := reconcile.AsReconciler(testClient, &mockObjectReconciler{
reconcileFunc: func(ctx context.Context, cm *corev1.ConfigMap) (reconcile.Result, error) {
actual = cm
return reconcile.Result{}, nil
},
})

res, err := reconciler.Reconcile(context.Background(), reconcile.Request{NamespacedName: key})
Expect(err).NotTo(HaveOccurred())
Expect(res).To(BeZero())
Expect(actual).NotTo(BeNil())
Expect(actual.ObjectMeta.Name).To(Equal(key.Name))
Expect(actual.ObjectMeta.Namespace).To(Equal(key.Namespace))
})
})

Context("with an object that doesn't exist", func() {
It("should not call the ObjectReconciler", func() {
called := false
reconciler := reconcile.AsReconciler(testClient, &mockObjectReconciler{
reconcileFunc: func(ctx context.Context, cm *corev1.ConfigMap) (reconcile.Result, error) {
called = true
return reconcile.Result{}, nil
},
})

key := types.NamespacedName{Namespace: "default", Name: "fake-obj"}
res, err := reconciler.Reconcile(context.Background(), reconcile.Request{NamespacedName: key})
Expect(err).NotTo(HaveOccurred())
Expect(res).To(BeZero())
Expect(called).To(BeFalse())
})
})
})
})

0 comments on commit 984a121

Please sign in to comment.