Skip to content

Commit fdc6658

Browse files
authored
Merge pull request #850 from akutz/feature/createOrPatch
✨CreateOrPatch
2 parents be18097 + 196055a commit fdc6658

File tree

2 files changed

+322
-0
lines changed

2 files changed

+322
-0
lines changed

pkg/controller/controllerutil/controllerutil.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ package controllerutil
1919
import (
2020
"context"
2121
"fmt"
22+
"reflect"
2223

2324
"k8s.io/apimachinery/pkg/api/equality"
2425
"k8s.io/apimachinery/pkg/api/errors"
2526
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2628
"k8s.io/apimachinery/pkg/runtime"
2729
"k8s.io/apimachinery/pkg/runtime/schema"
2830
"k8s.io/utils/pointer"
@@ -179,6 +181,10 @@ const ( // They should complete the sentence "Deployment default/foo has been ..
179181
OperationResultCreated OperationResult = "created"
180182
// OperationResultUpdated means that an existing resource is updated
181183
OperationResultUpdated OperationResult = "updated"
184+
// OperationResultUpdatedStatus means that an existing resource and its status is updated
185+
OperationResultUpdatedStatus OperationResult = "updatedStatus"
186+
// OperationResultUpdatedStatusOnly means that only an existing status is updated
187+
OperationResultUpdatedStatusOnly OperationResult = "updatedStatusOnly"
182188
)
183189

184190
// CreateOrUpdate creates or updates the given object in the Kubernetes
@@ -222,6 +228,108 @@ func CreateOrUpdate(ctx context.Context, c client.Client, obj runtime.Object, f
222228
return OperationResultUpdated, nil
223229
}
224230

231+
// CreateOrPatch creates or patches the given object in the Kubernetes
232+
// cluster. The object's desired state must be reconciled with the before
233+
// state inside the passed in callback MutateFn.
234+
//
235+
// The MutateFn is called regardless of creating or updating an object.
236+
//
237+
// It returns the executed operation and an error.
238+
func CreateOrPatch(ctx context.Context, c client.Client, obj runtime.Object, f MutateFn) (OperationResult, error) {
239+
key, err := client.ObjectKeyFromObject(obj)
240+
if err != nil {
241+
return OperationResultNone, err
242+
}
243+
244+
if err := c.Get(ctx, key, obj); err != nil {
245+
if !errors.IsNotFound(err) {
246+
return OperationResultNone, err
247+
}
248+
if f != nil {
249+
if err := mutate(f, key, obj); err != nil {
250+
return OperationResultNone, err
251+
}
252+
}
253+
if err := c.Create(ctx, obj); err != nil {
254+
return OperationResultNone, err
255+
}
256+
return OperationResultCreated, nil
257+
}
258+
259+
// Create patches for the object and its possible status.
260+
objPatch := client.MergeFrom(obj.DeepCopyObject())
261+
statusPatch := client.MergeFrom(obj.DeepCopyObject())
262+
263+
// Create a copy of the original object as well as converting that copy to
264+
// unstructured data.
265+
before, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj.DeepCopyObject())
266+
if err != nil {
267+
return OperationResultNone, err
268+
}
269+
270+
// Attempt to extract the status from the resource for easier comparison later
271+
beforeStatus, hasBeforeStatus, err := unstructured.NestedFieldCopy(before, "status")
272+
if err != nil {
273+
return OperationResultNone, err
274+
}
275+
276+
// If the resource contains a status then remove it from the unstructured
277+
// copy to avoid unnecessary patching later.
278+
if hasBeforeStatus {
279+
unstructured.RemoveNestedField(before, "status")
280+
}
281+
282+
// Mutate the original object.
283+
if f != nil {
284+
if err := mutate(f, key, obj); err != nil {
285+
return OperationResultNone, err
286+
}
287+
}
288+
289+
// Convert the resource to unstructured to compare against our before copy.
290+
after, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
291+
if err != nil {
292+
return OperationResultNone, err
293+
}
294+
295+
// Attempt to extract the status from the resource for easier comparison later
296+
afterStatus, hasAfterStatus, err := unstructured.NestedFieldCopy(after, "status")
297+
if err != nil {
298+
return OperationResultNone, err
299+
}
300+
301+
// If the resource contains a status then remove it from the unstructured
302+
// copy to avoid unnecessary patching later.
303+
if hasAfterStatus {
304+
unstructured.RemoveNestedField(after, "status")
305+
}
306+
307+
result := OperationResultNone
308+
309+
if !reflect.DeepEqual(before, after) {
310+
// Only issue a Patch if the before and after resources (minus status) differ
311+
if err := c.Patch(ctx, obj, objPatch); err != nil {
312+
return result, err
313+
}
314+
result = OperationResultUpdated
315+
}
316+
317+
if (hasBeforeStatus || hasAfterStatus) && !reflect.DeepEqual(beforeStatus, afterStatus) {
318+
// Only issue a Status Patch if the resource has a status and the beforeStatus
319+
// and afterStatus copies differ
320+
if err := c.Status().Patch(ctx, obj, statusPatch); err != nil {
321+
return result, err
322+
}
323+
if result == OperationResultUpdated {
324+
result = OperationResultUpdatedStatus
325+
} else {
326+
result = OperationResultUpdatedStatusOnly
327+
}
328+
}
329+
330+
return result, nil
331+
}
332+
225333
// mutate wraps a MutateFn and applies validation to its result
226334
func mutate(f MutateFn, key client.ObjectKey, obj runtime.Object) error {
227335
if err := f(); err != nil {

pkg/controller/controllerutil/controllerutil_test.go

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,213 @@ var _ = Describe("Controllerutil", func() {
406406
})
407407
})
408408

409+
Describe("CreateOrPatch", func() {
410+
var deploy *appsv1.Deployment
411+
var deplSpec appsv1.DeploymentSpec
412+
var deplKey types.NamespacedName
413+
var specr controllerutil.MutateFn
414+
415+
BeforeEach(func() {
416+
deploy = &appsv1.Deployment{
417+
ObjectMeta: metav1.ObjectMeta{
418+
Name: fmt.Sprintf("deploy-%d", rand.Int31()),
419+
Namespace: "default",
420+
},
421+
}
422+
423+
deplSpec = appsv1.DeploymentSpec{
424+
Selector: &metav1.LabelSelector{
425+
MatchLabels: map[string]string{"foo": "bar"},
426+
},
427+
Template: corev1.PodTemplateSpec{
428+
ObjectMeta: metav1.ObjectMeta{
429+
Labels: map[string]string{
430+
"foo": "bar",
431+
},
432+
},
433+
Spec: corev1.PodSpec{
434+
Containers: []corev1.Container{
435+
{
436+
Name: "busybox",
437+
Image: "busybox",
438+
},
439+
},
440+
},
441+
},
442+
}
443+
444+
deplKey = types.NamespacedName{
445+
Name: deploy.Name,
446+
Namespace: deploy.Namespace,
447+
}
448+
449+
specr = deploymentSpecr(deploy, deplSpec)
450+
})
451+
452+
assertLocalDeployWasUpdated := func(fetched *appsv1.Deployment) {
453+
By("local deploy object was updated during patch & has same spec, status, resource version as fetched")
454+
if fetched == nil {
455+
fetched = &appsv1.Deployment{}
456+
ExpectWithOffset(1, c.Get(context.TODO(), deplKey, fetched)).To(Succeed())
457+
}
458+
ExpectWithOffset(1, fetched.ResourceVersion).To(Equal(deploy.ResourceVersion))
459+
ExpectWithOffset(1, fetched.Spec).To(BeEquivalentTo(deploy.Spec))
460+
ExpectWithOffset(1, fetched.Status).To(BeEquivalentTo(deploy.Status))
461+
}
462+
463+
It("creates a new object if one doesn't exists", func() {
464+
op, err := controllerutil.CreateOrPatch(context.TODO(), c, deploy, specr)
465+
466+
By("returning no error")
467+
Expect(err).NotTo(HaveOccurred())
468+
469+
By("returning OperationResultCreated")
470+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
471+
472+
By("actually having the deployment created")
473+
fetched := &appsv1.Deployment{}
474+
Expect(c.Get(context.TODO(), deplKey, fetched)).To(Succeed())
475+
476+
By("being mutated by MutateFn")
477+
Expect(fetched.Spec.Template.Spec.Containers).To(HaveLen(1))
478+
Expect(fetched.Spec.Template.Spec.Containers[0].Name).To(Equal(deplSpec.Template.Spec.Containers[0].Name))
479+
Expect(fetched.Spec.Template.Spec.Containers[0].Image).To(Equal(deplSpec.Template.Spec.Containers[0].Image))
480+
})
481+
482+
It("patches existing object", func() {
483+
var scale int32 = 2
484+
op, err := controllerutil.CreateOrPatch(context.TODO(), c, deploy, specr)
485+
Expect(err).NotTo(HaveOccurred())
486+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
487+
488+
op, err = controllerutil.CreateOrPatch(context.TODO(), c, deploy, deploymentScaler(deploy, scale))
489+
By("returning no error")
490+
Expect(err).NotTo(HaveOccurred())
491+
492+
By("returning OperationResultUpdated")
493+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultUpdated))
494+
495+
By("actually having the deployment scaled")
496+
fetched := &appsv1.Deployment{}
497+
Expect(c.Get(context.TODO(), deplKey, fetched)).To(Succeed())
498+
Expect(*fetched.Spec.Replicas).To(Equal(scale))
499+
assertLocalDeployWasUpdated(fetched)
500+
})
501+
502+
It("patches only changed objects", func() {
503+
op, err := controllerutil.CreateOrPatch(context.TODO(), c, deploy, specr)
504+
505+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
506+
Expect(err).NotTo(HaveOccurred())
507+
508+
op, err = controllerutil.CreateOrPatch(context.TODO(), c, deploy, deploymentIdentity)
509+
By("returning no error")
510+
Expect(err).NotTo(HaveOccurred())
511+
512+
By("returning OperationResultNone")
513+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone))
514+
515+
assertLocalDeployWasUpdated(nil)
516+
})
517+
518+
It("patches only changed status", func() {
519+
op, err := controllerutil.CreateOrPatch(context.TODO(), c, deploy, specr)
520+
521+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
522+
Expect(err).NotTo(HaveOccurred())
523+
524+
deployStatus := appsv1.DeploymentStatus{
525+
ReadyReplicas: 1,
526+
Replicas: 3,
527+
}
528+
op, err = controllerutil.CreateOrPatch(context.TODO(), c, deploy, deploymentStatusr(deploy, deployStatus))
529+
By("returning no error")
530+
Expect(err).NotTo(HaveOccurred())
531+
532+
By("returning OperationResultUpdatedStatusOnly")
533+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultUpdatedStatusOnly))
534+
535+
assertLocalDeployWasUpdated(nil)
536+
})
537+
538+
It("patches resource and status", func() {
539+
op, err := controllerutil.CreateOrPatch(context.TODO(), c, deploy, specr)
540+
541+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
542+
Expect(err).NotTo(HaveOccurred())
543+
544+
replicas := int32(3)
545+
deployStatus := appsv1.DeploymentStatus{
546+
ReadyReplicas: 1,
547+
Replicas: replicas,
548+
}
549+
op, err = controllerutil.CreateOrPatch(context.TODO(), c, deploy, func() error {
550+
Expect(deploymentScaler(deploy, replicas)()).To(Succeed())
551+
return deploymentStatusr(deploy, deployStatus)()
552+
})
553+
By("returning no error")
554+
Expect(err).NotTo(HaveOccurred())
555+
556+
By("returning OperationResultUpdatedStatus")
557+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultUpdatedStatus))
558+
559+
assertLocalDeployWasUpdated(nil)
560+
})
561+
562+
It("errors when MutateFn changes object name on creation", func() {
563+
op, err := controllerutil.CreateOrPatch(context.TODO(), c, deploy, func() error {
564+
Expect(specr()).To(Succeed())
565+
return deploymentRenamer(deploy)()
566+
})
567+
568+
By("returning error")
569+
Expect(err).To(HaveOccurred())
570+
571+
By("returning OperationResultNone")
572+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone))
573+
})
574+
575+
It("errors when MutateFn renames an object", func() {
576+
op, err := controllerutil.CreateOrPatch(context.TODO(), c, deploy, specr)
577+
578+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
579+
Expect(err).NotTo(HaveOccurred())
580+
581+
op, err = controllerutil.CreateOrPatch(context.TODO(), c, deploy, deploymentRenamer(deploy))
582+
583+
By("returning error")
584+
Expect(err).To(HaveOccurred())
585+
586+
By("returning OperationResultNone")
587+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone))
588+
})
589+
590+
It("errors when object namespace changes", func() {
591+
op, err := controllerutil.CreateOrPatch(context.TODO(), c, deploy, specr)
592+
593+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
594+
Expect(err).NotTo(HaveOccurred())
595+
596+
op, err = controllerutil.CreateOrPatch(context.TODO(), c, deploy, deploymentNamespaceChanger(deploy))
597+
598+
By("returning error")
599+
Expect(err).To(HaveOccurred())
600+
601+
By("returning OperationResultNone")
602+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone))
603+
})
604+
605+
It("aborts immediately if there was an error initially retrieving the object", func() {
606+
op, err := controllerutil.CreateOrPatch(context.TODO(), errorReader{c}, deploy, func() error {
607+
Fail("Mutation method should not run")
608+
return nil
609+
})
610+
611+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone))
612+
Expect(err).To(HaveOccurred())
613+
})
614+
})
615+
409616
Describe("Finalizers", func() {
410617
var deploy *appsv1.Deployment
411618

@@ -478,6 +685,13 @@ func deploymentSpecr(deploy *appsv1.Deployment, spec appsv1.DeploymentSpec) cont
478685
}
479686
}
480687

688+
func deploymentStatusr(deploy *appsv1.Deployment, status appsv1.DeploymentStatus) controllerutil.MutateFn {
689+
return func() error {
690+
deploy.Status = status
691+
return nil
692+
}
693+
}
694+
481695
var deploymentIdentity controllerutil.MutateFn = func() error {
482696
return nil
483697
}

0 commit comments

Comments
 (0)