Skip to content

Commit d47ce17

Browse files
committed
Add CreateOrUpdate utility method
1 parent eac6912 commit d47ce17

File tree

4 files changed

+339
-0
lines changed

4 files changed

+339
-0
lines changed

pkg/controller/controllerutil/controllerutil.go

+79
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@ limitations under the License.
1717
package controllerutil
1818

1919
import (
20+
"context"
2021
"fmt"
22+
"reflect"
2123

24+
"k8s.io/apimachinery/pkg/api/errors"
2225
"k8s.io/apimachinery/pkg/apis/meta/v1"
2326
"k8s.io/apimachinery/pkg/runtime"
2427
"k8s.io/apimachinery/pkg/runtime/schema"
28+
"sigs.k8s.io/controller-runtime/pkg/client"
2529
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
2630
)
2731

@@ -97,3 +101,78 @@ func referSameObject(a, b v1.OwnerReference) bool {
97101

98102
return aGV == bGV && a.Kind == b.Kind && a.Name == b.Name
99103
}
104+
105+
// OperationResult is the action result of a CreateOrUpdate call
106+
type OperationResult string
107+
108+
const ( // They should complete the sentence "Deployment default/foo has been ..."
109+
// OperationResultNone means that the resource has not been changed
110+
OperationResultNone OperationResult = "unchanged"
111+
// OperationResultCreated means that a new resource is created
112+
OperationResultCreated OperationResult = "created"
113+
// OperationResultUpdated means that an existing resource is updated
114+
OperationResultUpdated OperationResult = "updated"
115+
)
116+
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.
121+
//
122+
// It returns the executed operation and an error.
123+
func CreateOrUpdate(ctx context.Context, c client.Client, obj runtime.Object, f MutateFn) (OperationResult, error) {
124+
// op is the operation we are going to attempt
125+
op := OperationResultNone
126+
127+
// get the existing object meta
128+
metaObj, ok := obj.(v1.Object)
129+
if !ok {
130+
return OperationResultNone, fmt.Errorf("%T does not implement metav1.Object interface", obj)
131+
}
132+
133+
// retrieve the existing object
134+
key := client.ObjectKey{
135+
Name: metaObj.GetName(),
136+
Namespace: metaObj.GetNamespace(),
137+
}
138+
err := c.Get(ctx, key, obj)
139+
140+
// reconcile the existing object
141+
existing := obj.DeepCopyObject()
142+
existingObjMeta := existing.(v1.Object)
143+
existingObjMeta.SetName(metaObj.GetName())
144+
existingObjMeta.SetNamespace(metaObj.GetNamespace())
145+
146+
if e := f(obj); e != nil {
147+
return OperationResultNone, e
148+
}
149+
150+
if metaObj.GetName() != existingObjMeta.GetName() {
151+
return OperationResultNone, fmt.Errorf("ReconcileFn cannot mutate objects name")
152+
}
153+
154+
if metaObj.GetNamespace() != existingObjMeta.GetNamespace() {
155+
return OperationResultNone, fmt.Errorf("ReconcileFn cannot mutate objects namespace")
156+
}
157+
158+
if errors.IsNotFound(err) {
159+
err = c.Create(ctx, obj)
160+
op = OperationResultCreated
161+
} else if err == nil {
162+
if reflect.DeepEqual(existing, obj) {
163+
return OperationResultNone, nil
164+
}
165+
err = c.Update(ctx, obj)
166+
op = OperationResultUpdated
167+
} else {
168+
return OperationResultNone, err
169+
}
170+
171+
if err != nil {
172+
op = OperationResultNone
173+
}
174+
return op, err
175+
}
176+
177+
// MutateFn is a function which mutates the existing object into it's desired state.
178+
type MutateFn func(existing runtime.Object) error

pkg/controller/controllerutil/controllerutil_suite_test.go

+24
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,33 @@ import (
2121

2222
. "github.com/onsi/ginkgo"
2323
. "github.com/onsi/gomega"
24+
25+
"k8s.io/client-go/rest"
26+
"sigs.k8s.io/controller-runtime/pkg/client"
27+
"sigs.k8s.io/controller-runtime/pkg/envtest"
2428
)
2529

2630
func TestControllerutil(t *testing.T) {
2731
RegisterFailHandler(Fail)
2832
RunSpecs(t, "Controllerutil Suite")
2933
}
34+
35+
var t *envtest.Environment
36+
var cfg *rest.Config
37+
var c client.Client
38+
39+
var _ = BeforeSuite(func() {
40+
var err error
41+
42+
t = &envtest.Environment{}
43+
44+
cfg, err = t.Start()
45+
Expect(err).NotTo(HaveOccurred())
46+
47+
c, err = client.New(cfg, client.Options{})
48+
Expect(err).NotTo(HaveOccurred())
49+
})
50+
51+
var _ = AfterSuite(func() {
52+
t.Stop()
53+
})

pkg/controller/controllerutil/controllerutil_test.go

+158
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,18 @@ limitations under the License.
1717
package controllerutil_test
1818

1919
import (
20+
"context"
21+
"fmt"
22+
"math/rand"
23+
2024
. "github.com/onsi/ginkgo"
2125
. "github.com/onsi/gomega"
2226
appsv1 "k8s.io/api/apps/v1"
27+
corev1 "k8s.io/api/core/v1"
2328
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
2429
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2530
"k8s.io/apimachinery/pkg/runtime"
31+
"k8s.io/apimachinery/pkg/types"
2632
"k8s.io/client-go/kubernetes/scheme"
2733
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
2834
)
@@ -108,10 +114,162 @@ var _ = Describe("Controllerutil", func() {
108114
}))
109115
})
110116
})
117+
118+
Describe("CreateOrUpdate", func() {
119+
var deploy *appsv1.Deployment
120+
var deplSpec appsv1.DeploymentSpec
121+
var deplKey types.NamespacedName
122+
123+
BeforeEach(func() {
124+
deploy = &appsv1.Deployment{
125+
ObjectMeta: metav1.ObjectMeta{
126+
Name: fmt.Sprintf("deploy-%d", rand.Int31()),
127+
Namespace: "default",
128+
},
129+
}
130+
131+
deplSpec = appsv1.DeploymentSpec{
132+
Selector: &metav1.LabelSelector{
133+
MatchLabels: map[string]string{"foo": "bar"},
134+
},
135+
Template: corev1.PodTemplateSpec{
136+
ObjectMeta: metav1.ObjectMeta{
137+
Labels: map[string]string{
138+
"foo": "bar",
139+
},
140+
},
141+
Spec: corev1.PodSpec{
142+
Containers: []corev1.Container{
143+
corev1.Container{
144+
Name: "busybox",
145+
Image: "busybox",
146+
},
147+
},
148+
},
149+
},
150+
}
151+
152+
deplKey = types.NamespacedName{
153+
Name: deploy.Name,
154+
Namespace: deploy.Namespace,
155+
}
156+
})
157+
158+
It("creates a new object if one doesn't exists", func() {
159+
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentSpecr(deplSpec))
160+
161+
By("returning OperationResultCreatedd")
162+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
163+
164+
By("returning no error")
165+
Expect(err).NotTo(HaveOccurred())
166+
167+
By("actually having the deployment created")
168+
fetched := &appsv1.Deployment{}
169+
Expect(c.Get(context.TODO(), deplKey, fetched)).To(Succeed())
170+
})
171+
172+
It("updates existing object", func() {
173+
var scale int32 = 2
174+
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentSpecr(deplSpec))
175+
Expect(err).NotTo(HaveOccurred())
176+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
177+
178+
op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentScaler(scale))
179+
By("returning OperationResultUpdatedd")
180+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultUpdated))
181+
182+
By("returning no error")
183+
Expect(err).NotTo(HaveOccurred())
184+
185+
By("actually having the deployment scaled")
186+
fetched := &appsv1.Deployment{}
187+
Expect(c.Get(context.TODO(), deplKey, fetched)).To(Succeed())
188+
Expect(*fetched.Spec.Replicas).To(Equal(scale))
189+
})
190+
191+
It("updates only changed objects", func() {
192+
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentSpecr(deplSpec))
193+
194+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
195+
Expect(err).NotTo(HaveOccurred())
196+
197+
op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentIdentity)
198+
199+
By("returning OperationResultNone")
200+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone))
201+
202+
By("returning no error")
203+
Expect(err).NotTo(HaveOccurred())
204+
})
205+
206+
It("errors when reconcile renames an object", func() {
207+
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentSpecr(deplSpec))
208+
209+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
210+
Expect(err).NotTo(HaveOccurred())
211+
212+
op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentRenamer)
213+
214+
By("returning OperationResultNone")
215+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone))
216+
217+
By("returning error")
218+
Expect(err).To(HaveOccurred())
219+
})
220+
221+
It("errors when object namespace changes", func() {
222+
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentSpecr(deplSpec))
223+
224+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
225+
Expect(err).NotTo(HaveOccurred())
226+
227+
op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentNamespaceChanger)
228+
229+
By("returning OperationResultNone")
230+
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone))
231+
232+
By("returning error")
233+
Expect(err).To(HaveOccurred())
234+
})
235+
})
111236
})
112237

113238
var _ metav1.Object = &errMetaObj{}
114239

115240
type errMetaObj struct {
116241
metav1.ObjectMeta
117242
}
243+
244+
func deploymentSpecr(spec appsv1.DeploymentSpec) controllerutil.MutateFn {
245+
return func(obj runtime.Object) error {
246+
deploy := obj.(*appsv1.Deployment)
247+
deploy.Spec = spec
248+
return nil
249+
}
250+
}
251+
252+
var deploymentIdentity controllerutil.MutateFn = func(obj runtime.Object) error {
253+
return nil
254+
}
255+
256+
var deploymentRenamer controllerutil.MutateFn = func(obj runtime.Object) error {
257+
deploy := obj.(*appsv1.Deployment)
258+
deploy.Name = fmt.Sprintf("%s-1", deploy.Name)
259+
return nil
260+
}
261+
262+
var deploymentNamespaceChanger controllerutil.MutateFn = func(obj runtime.Object) error {
263+
deploy := obj.(*appsv1.Deployment)
264+
deploy.Namespace = fmt.Sprintf("%s-1", deploy.Namespace)
265+
return nil
266+
}
267+
268+
func deploymentScaler(replicas int32) controllerutil.MutateFn {
269+
fn := func(obj runtime.Object) error {
270+
deploy := obj.(*appsv1.Deployment)
271+
deploy.Spec.Replicas = &replicas
272+
return nil
273+
}
274+
return fn
275+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
Copyright 2018 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllerutil_test
18+
19+
import (
20+
"context"
21+
22+
appsv1 "k8s.io/api/apps/v1"
23+
corev1 "k8s.io/api/core/v1"
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
"k8s.io/apimachinery/pkg/runtime"
26+
27+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
28+
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
29+
)
30+
31+
var (
32+
log = logf.Log.WithName("controllerutil-examples")
33+
)
34+
35+
// This example creates or updates an existing deployment
36+
func ExampleCreateOrUpdate() {
37+
// c is client.Client
38+
39+
// Create or Update the deployment default/foo
40+
deployment := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"}}
41+
42+
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deployment, func(existing runtime.Object) error {
43+
deploy := existing.(*appsv1.Deployment)
44+
45+
// Deployment selector is immutable so we set this value only if
46+
// a new object is going to be created
47+
if deploy.ObjectMeta.CreationTimestamp.IsZero() {
48+
deploy.Spec.Selector = &metav1.LabelSelector{
49+
MatchLabels: map[string]string{"foo": "bar"},
50+
}
51+
}
52+
53+
// update the Deployment pod template
54+
deploy.Spec.Template = corev1.PodTemplateSpec{
55+
ObjectMeta: metav1.ObjectMeta{
56+
Labels: map[string]string{
57+
"foo": "bar",
58+
},
59+
},
60+
Spec: corev1.PodSpec{
61+
Containers: []corev1.Container{
62+
corev1.Container{
63+
Name: "busybox",
64+
Image: "busybox",
65+
},
66+
},
67+
},
68+
}
69+
70+
return nil
71+
})
72+
73+
if err != nil {
74+
log.Error(err, "Deployment reconcile failed")
75+
} else {
76+
log.Info("Deployment successfully reconciled", "operation", op)
77+
}
78+
}

0 commit comments

Comments
 (0)