@@ -29,6 +29,7 @@ import (
2929 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3030 "k8s.io/apimachinery/pkg/runtime"
3131 "k8s.io/apimachinery/pkg/types"
32+ "k8s.io/apimachinery/pkg/util/intstr"
3233 "k8s.io/client-go/kubernetes/scheme"
3334 "k8s.io/utils/ptr"
3435 "sigs.k8s.io/controller-runtime/pkg/client"
@@ -454,6 +455,9 @@ var _ = Describe("Controllerutil", func() {
454455 var deplKey types.NamespacedName
455456 var specr controllerutil.MutateFn
456457
458+ var deplSpecWithDefaults appsv1.DeploymentSpec
459+ var specrWithDefaultsr controllerutil.MutateFn
460+
457461 BeforeEach (func () {
458462 deploy = & appsv1.Deployment {
459463 ObjectMeta : metav1.ObjectMeta {
@@ -483,12 +487,59 @@ var _ = Describe("Controllerutil", func() {
483487 },
484488 }
485489
490+ // deployment with all defaults set
491+ deplSpecWithDefaults = appsv1.DeploymentSpec {
492+ Selector : & metav1.LabelSelector {
493+ MatchLabels : map [string ]string {"foo" : "bar" },
494+ },
495+ Replicas : ptr .To (int32 (1 )),
496+ Template : corev1.PodTemplateSpec {
497+ ObjectMeta : metav1.ObjectMeta {
498+ Labels : map [string ]string {
499+ "foo" : "bar" ,
500+ },
501+ },
502+ Spec : corev1.PodSpec {
503+ RestartPolicy : corev1 .RestartPolicyAlways ,
504+ TerminationGracePeriodSeconds : ptr .To (int64 (30 )),
505+ DNSPolicy : corev1 .DNSClusterFirst ,
506+ SecurityContext : & corev1.PodSecurityContext {},
507+ SchedulerName : "default-scheduler" ,
508+ Containers : []corev1.Container {
509+ {
510+ Name : "busybox" ,
511+ Image : "busybox" ,
512+ TerminationMessagePath : "/dev/termination-log" ,
513+ TerminationMessagePolicy : corev1 .TerminationMessageReadFile ,
514+ ImagePullPolicy : corev1 .PullAlways ,
515+ },
516+ },
517+ },
518+ },
519+ Strategy : appsv1.DeploymentStrategy {
520+ Type : appsv1 .RollingUpdateDeploymentStrategyType ,
521+ RollingUpdate : & appsv1.RollingUpdateDeployment {
522+ MaxUnavailable : & intstr.IntOrString {
523+ Type : intstr .String ,
524+ StrVal : "25%" ,
525+ },
526+ MaxSurge : & intstr.IntOrString {
527+ Type : intstr .String ,
528+ StrVal : "25%" ,
529+ },
530+ },
531+ },
532+ RevisionHistoryLimit : ptr .To (int32 (10 )),
533+ ProgressDeadlineSeconds : ptr .To (int32 (600 )),
534+ }
535+
486536 deplKey = types.NamespacedName {
487537 Name : deploy .Name ,
488538 Namespace : deploy .Namespace ,
489539 }
490540
491541 specr = deploymentSpecr (deploy , deplSpec )
542+ specrWithDefaultsr = deploymentSpecr (deploy , deplSpecWithDefaults )
492543 })
493544
494545 It ("creates a new object if one doesn't exists" , func () {
@@ -543,6 +594,59 @@ var _ = Describe("Controllerutil", func() {
543594 Expect (op ).To (BeEquivalentTo (controllerutil .OperationResultNone ))
544595 })
545596
597+ // The next two tests verify that CreateOrUpdate will always report an update unless the
598+ // object has *all* of its defaults set in the mutate function.
599+
600+ It ("is idempotent if all defaults are set" , func () {
601+ op , err := controllerutil .CreateOrUpdate (context .TODO (), c , deploy , specrWithDefaultsr )
602+
603+ Expect (op ).To (BeEquivalentTo (controllerutil .OperationResultCreated ))
604+ Expect (err ).NotTo (HaveOccurred ())
605+
606+ Expect (deploy .Spec ).To (BeEquivalentTo (deplSpecWithDefaults ))
607+
608+ op , err = controllerutil .CreateOrUpdate (context .TODO (), c , deploy , specrWithDefaultsr )
609+ By ("returning no error" )
610+ Expect (err ).NotTo (HaveOccurred ())
611+
612+ By ("returning OperationResultNone" )
613+ Expect (op ).To (BeEquivalentTo (controllerutil .OperationResultNone ))
614+ })
615+
616+ It ("is idempotent even if defaults aren't set" , func () {
617+ op , err := controllerutil .CreateOrUpdate (context .TODO (), c , deploy , specr )
618+
619+ Expect (op ).To (BeEquivalentTo (controllerutil .OperationResultCreated ))
620+ Expect (err ).NotTo (HaveOccurred ())
621+
622+ op , err = controllerutil .CreateOrUpdate (context .TODO (), c , deploy , specr )
623+ By ("returning no error" )
624+ Expect (err ).NotTo (HaveOccurred ())
625+
626+ By ("returning OperationResultNone" )
627+ Expect (op ).To (BeEquivalentTo (controllerutil .OperationResultNone ))
628+ })
629+
630+ // The following test verifies that CreateOrUpdate will overwrite any changes made outside of
631+ // the mutate function. This is working correctly, but needs to be documented.
632+
633+ It ("modifies objects that had non-identifier values erroneously pre-set" , func () {
634+ op , err := controllerutil .CreateOrUpdate (context .TODO (), c , deploy , specr )
635+
636+ Expect (op ).To (BeEquivalentTo (controllerutil .OperationResultCreated ))
637+ Expect (err ).NotTo (HaveOccurred ())
638+
639+ // Change the object outside of the mutate function. Despite deploymentIdentity being
640+ // a literal no-op, this test fails because Replicas will be overwritten.
641+ deploy .Spec .Replicas = ptr .To (int32 (5 ))
642+ _ , err = controllerutil .CreateOrUpdate (context .TODO (), c , deploy , deploymentIdentity )
643+ // This also fails, but that should be self-evident, as specr replaces the entire Spec.
644+ // _, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, specr)
645+
646+ Expect (err ).NotTo (HaveOccurred ())
647+ Expect (deploy .Spec .Replicas ).To (HaveValue (BeEquivalentTo (int32 (5 ))))
648+ })
649+
546650 It ("errors when MutateFn changes object name on creation" , func () {
547651 op , err := controllerutil .CreateOrUpdate (context .TODO (), c , deploy , func () error {
548652 Expect (specr ()).To (Succeed ())
0 commit comments