Skip to content

Commit 6100e07

Browse files
authored
Merge pull request #319 from presslabs/fix-create-or-update
⚠️ Call MutateFn on CreateOrUpdate regardless of the operation
2 parents 68ae79e + d98fd5b commit 6100e07

File tree

3 files changed

+92
-68
lines changed

3 files changed

+92
-68
lines changed

pkg/controller/controllerutil/controllerutil.go

+27-20
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,11 @@ const ( // They should complete the sentence "Deployment default/foo has been ..
114114
OperationResultUpdated OperationResult = "updated"
115115
)
116116

117-
// CreateOrUpdate creates or updates the given object obj in the Kubernetes
118-
// cluster. The object's desired state should be reconciled with the existing
119-
// state using the passed in ReconcileFn. obj must be a struct pointer so that
120-
// obj can be updated with the content returned by the Server.
117+
// CreateOrUpdate creates or updates the given object in the Kubernetes
118+
// cluster. The object's desired state must be reconciled with the existing
119+
// state inside the passed in callback MutateFn.
120+
//
121+
// The MutateFn is called regardless of creating or updating an object.
121122
//
122123
// It returns the executed operation and an error.
123124
func CreateOrUpdate(ctx context.Context, c client.Client, obj runtime.Object, f MutateFn) (OperationResult, error) {
@@ -127,37 +128,43 @@ func CreateOrUpdate(ctx context.Context, c client.Client, obj runtime.Object, f
127128
}
128129

129130
if err := c.Get(ctx, key, obj); err != nil {
130-
if errors.IsNotFound(err) {
131-
if err := c.Create(ctx, obj); err != nil {
132-
return OperationResultNone, err
133-
}
134-
return OperationResultCreated, nil
131+
if !errors.IsNotFound(err) {
132+
return OperationResultNone, err
135133
}
136-
return OperationResultNone, err
134+
if err := mutate(f, key, obj); err != nil {
135+
return OperationResultNone, err
136+
}
137+
if err := c.Create(ctx, obj); err != nil {
138+
return OperationResultNone, err
139+
}
140+
return OperationResultCreated, nil
137141
}
138142

139143
existing := obj.DeepCopyObject()
140-
if err := f(obj); err != nil {
144+
if err := mutate(f, key, obj); err != nil {
141145
return OperationResultNone, err
142146
}
143147

144148
if reflect.DeepEqual(existing, obj) {
145149
return OperationResultNone, nil
146150
}
147151

148-
newKey, err := client.ObjectKeyFromObject(obj)
149-
if err != nil {
150-
return OperationResultNone, err
151-
}
152-
if key != newKey {
153-
return OperationResultNone, fmt.Errorf("MutateFn cannot mutate object namespace and/or object name")
154-
}
155-
156152
if err := c.Update(ctx, obj); err != nil {
157153
return OperationResultNone, err
158154
}
159155
return OperationResultUpdated, nil
160156
}
161157

158+
// mutate wraps a MutateFn and applies validation to its result
159+
func mutate(f MutateFn, key client.ObjectKey, obj runtime.Object) error {
160+
if err := f(); err != nil {
161+
return err
162+
}
163+
if newKey, err := client.ObjectKeyFromObject(obj); err != nil || key != newKey {
164+
return fmt.Errorf("MutateFn cannot mutate object name and/or object namespace")
165+
}
166+
return nil
167+
}
168+
162169
// MutateFn is a function which mutates the existing object into it's desired state.
163-
type MutateFn func(existing runtime.Object) error
170+
type MutateFn func() error

pkg/controller/controllerutil/controllerutil_test.go

+62-43
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ var _ = Describe("Controllerutil", func() {
7171
It("should return an error if object is already owned by another controller", func() {
7272
t := true
7373
rsOwners := []metav1.OwnerReference{
74-
metav1.OwnerReference{
74+
{
7575
Name: "bar",
7676
Kind: "Deployment",
7777
APIVersion: "extensions/v1beta1",
@@ -93,7 +93,7 @@ var _ = Describe("Controllerutil", func() {
9393
f := false
9494
t := true
9595
rsOwners := []metav1.OwnerReference{
96-
metav1.OwnerReference{
96+
{
9797
Name: "foo",
9898
Kind: "Deployment",
9999
APIVersion: "extensions/v1beta1",
@@ -121,6 +121,7 @@ var _ = Describe("Controllerutil", func() {
121121
var deploy *appsv1.Deployment
122122
var deplSpec appsv1.DeploymentSpec
123123
var deplKey types.NamespacedName
124+
var specr controllerutil.MutateFn
124125

125126
BeforeEach(func() {
126127
deploy = &appsv1.Deployment{
@@ -151,94 +152,111 @@ var _ = Describe("Controllerutil", func() {
151152
},
152153
}
153154

154-
deploy.Spec = deplSpec
155-
156155
deplKey = types.NamespacedName{
157156
Name: deploy.Name,
158157
Namespace: deploy.Namespace,
159158
}
159+
160+
specr = deploymentSpecr(deploy, deplSpec)
160161
})
161162

162163
It("creates a new object if one doesn't exists", func() {
163-
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentSpecr(deplSpec))
164-
165-
By("returning OperationResultCreated")
166-
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
164+
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, specr)
167165

168166
By("returning no error")
169167
Expect(err).NotTo(HaveOccurred())
170168

169+
By("returning OperationResultCreated")
170+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
171+
171172
By("actually having the deployment created")
172173
fetched := &appsv1.Deployment{}
173174
Expect(c.Get(context.TODO(), deplKey, fetched)).To(Succeed())
175+
176+
By("being mutated by MutateFn")
177+
Expect(fetched.Spec.Template.Spec.Containers).To(HaveLen(1))
178+
Expect(fetched.Spec.Template.Spec.Containers[0].Name).To(Equal(deplSpec.Template.Spec.Containers[0].Name))
179+
Expect(fetched.Spec.Template.Spec.Containers[0].Image).To(Equal(deplSpec.Template.Spec.Containers[0].Image))
174180
})
175181

176182
It("updates existing object", func() {
177183
var scale int32 = 2
178-
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentSpecr(deplSpec))
184+
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, specr)
179185
Expect(err).NotTo(HaveOccurred())
180186
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
181187

182-
op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentScaler(scale))
183-
By("returning OperationResultUpdated")
184-
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultUpdated))
185-
188+
op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentScaler(deploy, scale))
186189
By("returning no error")
187190
Expect(err).NotTo(HaveOccurred())
188191

192+
By("returning OperationResultUpdated")
193+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultUpdated))
194+
189195
By("actually having the deployment scaled")
190196
fetched := &appsv1.Deployment{}
191197
Expect(c.Get(context.TODO(), deplKey, fetched)).To(Succeed())
192198
Expect(*fetched.Spec.Replicas).To(Equal(scale))
193199
})
194200

195201
It("updates only changed objects", func() {
196-
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentSpecr(deplSpec))
202+
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, specr)
197203

198204
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
199205
Expect(err).NotTo(HaveOccurred())
200206

201207
op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentIdentity)
208+
By("returning no error")
209+
Expect(err).NotTo(HaveOccurred())
202210

203211
By("returning OperationResultNone")
204212
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone))
213+
})
205214

206-
By("returning no error")
207-
Expect(err).NotTo(HaveOccurred())
215+
It("errors when MutateFn changes objct name on creation", func() {
216+
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, func() error {
217+
specr()
218+
return deploymentRenamer(deploy)()
219+
})
220+
221+
By("returning error")
222+
Expect(err).To(HaveOccurred())
223+
224+
By("returning OperationResultNone")
225+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone))
208226
})
209227

210-
It("errors when reconcile renames an object", func() {
211-
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentSpecr(deplSpec))
228+
It("errors when MutateFn renames an object", func() {
229+
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, specr)
212230

213231
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
214232
Expect(err).NotTo(HaveOccurred())
215233

216-
op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentRenamer)
217-
218-
By("returning OperationResultNone")
219-
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone))
234+
op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentRenamer(deploy))
220235

221236
By("returning error")
222237
Expect(err).To(HaveOccurred())
238+
239+
By("returning OperationResultNone")
240+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone))
223241
})
224242

225243
It("errors when object namespace changes", func() {
226-
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentSpecr(deplSpec))
244+
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, specr)
227245

228246
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
229247
Expect(err).NotTo(HaveOccurred())
230248

231-
op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentNamespaceChanger)
232-
233-
By("returning OperationResultNone")
234-
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone))
249+
op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentNamespaceChanger(deploy))
235250

236251
By("returning error")
237252
Expect(err).To(HaveOccurred())
253+
254+
By("returning OperationResultNone")
255+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone))
238256
})
239257

240258
It("aborts immediately if there was an error initially retrieving the object", func() {
241-
op, err := controllerutil.CreateOrUpdate(context.TODO(), errorReader{c}, deploy, func(_ runtime.Object) error {
259+
op, err := controllerutil.CreateOrUpdate(context.TODO(), errorReader{c}, deploy, func() error {
242260
Fail("Mutation method should not run")
243261
return nil
244262
})
@@ -255,33 +273,34 @@ type errMetaObj struct {
255273
metav1.ObjectMeta
256274
}
257275

258-
func deploymentSpecr(spec appsv1.DeploymentSpec) controllerutil.MutateFn {
259-
return func(obj runtime.Object) error {
260-
deploy := obj.(*appsv1.Deployment)
276+
func deploymentSpecr(deploy *appsv1.Deployment, spec appsv1.DeploymentSpec) controllerutil.MutateFn {
277+
return func() error {
261278
deploy.Spec = spec
262279
return nil
263280
}
264281
}
265282

266-
var deploymentIdentity controllerutil.MutateFn = func(obj runtime.Object) error {
283+
var deploymentIdentity controllerutil.MutateFn = func() error {
267284
return nil
268285
}
269286

270-
var deploymentRenamer controllerutil.MutateFn = func(obj runtime.Object) error {
271-
deploy := obj.(*appsv1.Deployment)
272-
deploy.Name = fmt.Sprintf("%s-1", deploy.Name)
273-
return nil
287+
func deploymentRenamer(deploy *appsv1.Deployment) controllerutil.MutateFn {
288+
return func() error {
289+
deploy.Name = fmt.Sprintf("%s-1", deploy.Name)
290+
return nil
291+
}
274292
}
275293

276-
var deploymentNamespaceChanger controllerutil.MutateFn = func(obj runtime.Object) error {
277-
deploy := obj.(*appsv1.Deployment)
278-
deploy.Namespace = fmt.Sprintf("%s-1", deploy.Namespace)
279-
return nil
294+
func deploymentNamespaceChanger(deploy *appsv1.Deployment) controllerutil.MutateFn {
295+
return func() error {
296+
deploy.Namespace = fmt.Sprintf("%s-1", deploy.Namespace)
297+
return nil
298+
299+
}
280300
}
281301

282-
func deploymentScaler(replicas int32) controllerutil.MutateFn {
283-
fn := func(obj runtime.Object) error {
284-
deploy := obj.(*appsv1.Deployment)
302+
func deploymentScaler(deploy *appsv1.Deployment, replicas int32) controllerutil.MutateFn {
303+
fn := func() error {
285304
deploy.Spec.Replicas = &replicas
286305
return nil
287306
}

pkg/controller/controllerutil/example_test.go

+3-5
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
appsv1 "k8s.io/api/apps/v1"
2323
corev1 "k8s.io/api/core/v1"
2424
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25-
"k8s.io/apimachinery/pkg/runtime"
2625

2726
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
2827
logf "sigs.k8s.io/controller-runtime/pkg/log"
@@ -37,10 +36,9 @@ func ExampleCreateOrUpdate() {
3736
// c is client.Client
3837

3938
// Create or Update the deployment default/foo
40-
deployment := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"}}
39+
deploy := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"}}
4140

42-
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deployment, func(existing runtime.Object) error {
43-
deploy := existing.(*appsv1.Deployment)
41+
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, func() error {
4442

4543
// Deployment selector is immutable so we set this value only if
4644
// a new object is going to be created
@@ -59,7 +57,7 @@ func ExampleCreateOrUpdate() {
5957
},
6058
Spec: corev1.PodSpec{
6159
Containers: []corev1.Container{
62-
corev1.Container{
60+
{
6361
Name: "busybox",
6462
Image: "busybox",
6563
},

0 commit comments

Comments
 (0)