Skip to content

Commit 984a121

Browse files
✨ Add reconcile.ObjectReconciler (#2592)
* Add ObjectReconciler * Add type hints in reconcile_test.go * Remove Go 1.20 type hints
1 parent bf3d274 commit 984a121

File tree

2 files changed

+115
-0
lines changed

2 files changed

+115
-0
lines changed

pkg/reconcile/reconcile.go

+32
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ package reconcile
1919
import (
2020
"context"
2121
"errors"
22+
"reflect"
2223
"time"
2324

2425
"k8s.io/apimachinery/pkg/types"
26+
"sigs.k8s.io/controller-runtime/pkg/client"
2527
)
2628

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

115+
// ObjectReconciler is a specialized version of Reconciler that acts on instances of client.Object. Each reconciliation
116+
// event gets the associated object from Kubernetes before passing it to Reconcile. An ObjectReconciler can be used in
117+
// Builder.Complete by calling AsReconciler. See Reconciler for more details.
118+
type ObjectReconciler[T client.Object] interface {
119+
Reconcile(context.Context, T) (Result, error)
120+
}
121+
122+
// AsReconciler creates a Reconciler based on the given ObjectReconciler.
123+
func AsReconciler[T client.Object](client client.Client, rec ObjectReconciler[T]) Reconciler {
124+
return &objectReconcilerAdapter[T]{
125+
objReconciler: rec,
126+
client: client,
127+
}
128+
}
129+
130+
type objectReconcilerAdapter[T client.Object] struct {
131+
objReconciler ObjectReconciler[T]
132+
client client.Client
133+
}
134+
135+
// Reconcile implements Reconciler.
136+
func (a *objectReconcilerAdapter[T]) Reconcile(ctx context.Context, req Request) (Result, error) {
137+
o := reflect.New(reflect.TypeOf(*new(T)).Elem()).Interface().(T)
138+
if err := a.client.Get(ctx, req.NamespacedName, o); err != nil {
139+
return Result{}, client.IgnoreNotFound(err)
140+
}
141+
142+
return a.objReconciler.Reconcile(ctx, o)
143+
}
144+
113145
// TerminalError is an error that will not be retried but still be logged
114146
// and recorded in metrics.
115147
func TerminalError(wrapped error) error {

pkg/reconcile/reconcile_test.go

+83
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,23 @@ import (
2323

2424
. "github.com/onsi/ginkgo/v2"
2525
. "github.com/onsi/gomega"
26+
corev1 "k8s.io/api/core/v1"
2627
apierrors "k8s.io/apimachinery/pkg/api/errors"
28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2729
"k8s.io/apimachinery/pkg/types"
30+
"sigs.k8s.io/controller-runtime/pkg/client"
31+
"sigs.k8s.io/controller-runtime/pkg/envtest"
2832
"sigs.k8s.io/controller-runtime/pkg/reconcile"
2933
)
3034

35+
type mockObjectReconciler struct {
36+
reconcileFunc func(context.Context, *corev1.ConfigMap) (reconcile.Result, error)
37+
}
38+
39+
func (r *mockObjectReconciler) Reconcile(ctx context.Context, cm *corev1.ConfigMap) (reconcile.Result, error) {
40+
return r.reconcileFunc(ctx, cm)
41+
}
42+
3143
var _ = Describe("reconcile", func() {
3244
Describe("Result", func() {
3345
It("IsZero should return true if empty", func() {
@@ -102,4 +114,75 @@ var _ = Describe("reconcile", func() {
102114
Expect(err.Error()).To(Equal("nil terminal error"))
103115
})
104116
})
117+
118+
Describe("AsReconciler", func() {
119+
var testenv *envtest.Environment
120+
var testClient client.Client
121+
122+
BeforeEach(func() {
123+
testenv = &envtest.Environment{}
124+
125+
cfg, err := testenv.Start()
126+
Expect(err).NotTo(HaveOccurred())
127+
128+
testClient, err = client.New(cfg, client.Options{})
129+
Expect(err).NotTo(HaveOccurred())
130+
})
131+
132+
AfterEach(func() {
133+
Expect(testenv.Stop()).NotTo(HaveOccurred())
134+
})
135+
136+
Context("with an existing object", func() {
137+
var key client.ObjectKey
138+
139+
BeforeEach(func() {
140+
cm := &corev1.ConfigMap{
141+
ObjectMeta: metav1.ObjectMeta{
142+
Namespace: "default",
143+
Name: "test",
144+
},
145+
}
146+
key = client.ObjectKeyFromObject(cm)
147+
148+
err := testClient.Create(context.Background(), cm)
149+
Expect(err).NotTo(HaveOccurred())
150+
})
151+
152+
It("should Get the object and call the ObjectReconciler", func() {
153+
var actual *corev1.ConfigMap
154+
reconciler := reconcile.AsReconciler(testClient, &mockObjectReconciler{
155+
reconcileFunc: func(ctx context.Context, cm *corev1.ConfigMap) (reconcile.Result, error) {
156+
actual = cm
157+
return reconcile.Result{}, nil
158+
},
159+
})
160+
161+
res, err := reconciler.Reconcile(context.Background(), reconcile.Request{NamespacedName: key})
162+
Expect(err).NotTo(HaveOccurred())
163+
Expect(res).To(BeZero())
164+
Expect(actual).NotTo(BeNil())
165+
Expect(actual.ObjectMeta.Name).To(Equal(key.Name))
166+
Expect(actual.ObjectMeta.Namespace).To(Equal(key.Namespace))
167+
})
168+
})
169+
170+
Context("with an object that doesn't exist", func() {
171+
It("should not call the ObjectReconciler", func() {
172+
called := false
173+
reconciler := reconcile.AsReconciler(testClient, &mockObjectReconciler{
174+
reconcileFunc: func(ctx context.Context, cm *corev1.ConfigMap) (reconcile.Result, error) {
175+
called = true
176+
return reconcile.Result{}, nil
177+
},
178+
})
179+
180+
key := types.NamespacedName{Namespace: "default", Name: "fake-obj"}
181+
res, err := reconciler.Reconcile(context.Background(), reconcile.Request{NamespacedName: key})
182+
Expect(err).NotTo(HaveOccurred())
183+
Expect(res).To(BeZero())
184+
Expect(called).To(BeFalse())
185+
})
186+
})
187+
})
105188
})

0 commit comments

Comments
 (0)