Skip to content

Commit 5110ac3

Browse files
committed
Get in line with client.Create and client.Update functions
1 parent 4855d31 commit 5110ac3

File tree

3 files changed

+198
-100
lines changed

3 files changed

+198
-100
lines changed

pkg/controller/controllerutil/controllerutil.go

Lines changed: 46 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -108,68 +108,60 @@ type OperationType string
108108
const ( // They should complete the sentence "Deployment default/foo has been ..."
109109
// OperationNoop means that the resource has not been changed
110110
OperationNoop = "unchanged"
111-
// OperationCreated means that a new resource has been created
112-
OperationCreated = "created"
113-
// OperationUpdated means that an existing resource has been updated
114-
OperationUpdated = "updated"
111+
// OperationCreate means that a new resource is created
112+
OperationCreate = "created"
113+
// OperationUpdate means that an existing resource is updated
114+
OperationUpdate = "updated"
115115
)
116116

117-
// CreateOrUpdate creates or updates a kubernetes resource. It takes in a key and
118-
// a placeholder for the existing object and returns the operation executed
119-
func CreateOrUpdate(ctx context.Context, c client.Client, key client.ObjectKey, existing runtime.Object, t TransformFn) (OperationType, error) {
120-
err := c.Get(ctx, key, existing)
121-
var obj runtime.Object
117+
// CreateOrUpdate a resource with the same type, name and namespace as obj
118+
// and reconcile it's state using the passed in ReconcileFn.
119+
// It returns the executed operation and an error.
120+
func CreateOrUpdate(ctx context.Context, c client.Client, obj runtime.Object, r ReconcileFn) (OperationType, error) {
121+
// op is the operation we are going to attempt
122+
var op OperationType = OperationNoop
122123

123-
if errors.IsNotFound(err) {
124-
// Create a new zero value object so that the in parameter of
125-
// TransformFn is always a "clean" object, with only Name and Namespace
126-
// set
127-
zero := reflect.New(reflect.TypeOf(existing).Elem()).Interface()
128-
129-
// Set Namespace and Name from the lookup key
130-
zmeta, ok := zero.(v1.Object)
131-
if !ok {
132-
return OperationNoop, fmt.Errorf("is not a %T a metav1.Object, cannot call CreateOrUpdate", zero)
133-
}
134-
zmeta.SetNamespace(key.Namespace)
135-
zmeta.SetName(key.Name)
124+
// get the existing object
125+
mo, ok := obj.(v1.Object)
126+
if !ok {
127+
return OperationNoop, fmt.Errorf("%T does not implement metav1.Object interface", obj)
128+
}
129+
key := client.ObjectKey{
130+
Name: mo.GetName(),
131+
Namespace: mo.GetNamespace(),
132+
}
133+
existing := reflect.New(reflect.TypeOf(obj).Elem()).Interface().(runtime.Object)
134+
err := c.Get(ctx, key, existing)
136135

137-
// Apply the TransformFn
138-
obj, err = t(zero.(runtime.Object))
139-
if err != nil {
140-
return OperationNoop, err
141-
}
136+
// reconcile the existing object
137+
newobj := existing.DeepCopyObject()
138+
if e := r(newobj); e != nil {
139+
return OperationNoop, e
140+
}
141+
// as promised, CreateOrUpdate creates or updates object with the same name and namespace
142+
newmo := newobj.(v1.Object)
143+
newmo.SetName(mo.GetName())
144+
newmo.SetNamespace(mo.GetNamespace())
142145

143-
// Create the new object
144-
err = c.Create(ctx, obj)
145-
if err != nil {
146-
return OperationNoop, err
146+
if errors.IsNotFound(err) {
147+
err = c.Create(ctx, newobj)
148+
op = OperationCreate
149+
} else if err == nil {
150+
if reflect.DeepEqual(existing, newobj) {
151+
return OperationNoop, nil
147152
}
148-
149-
return OperationCreated, err
150-
} else if err != nil {
151-
return OperationNoop, err
153+
err = c.Update(ctx, newobj)
154+
op = OperationUpdate
152155
} else {
153-
obj, err = t(existing.DeepCopyObject())
154-
if err != nil {
155-
return OperationNoop, err
156-
}
157-
158-
if !reflect.DeepEqual(existing, obj) {
159-
err = c.Update(ctx, obj)
160-
if err != nil {
161-
return OperationNoop, err
162-
}
163-
164-
return OperationUpdated, err
165-
}
156+
return OperationNoop, err
157+
}
166158

167-
return OperationNoop, nil
159+
if err != nil {
160+
op = OperationNoop
168161
}
162+
return op, err
169163
}
170164

171-
// TransformFn is a function which take in a kubernetes object and returns the
172-
// desired state of that object.
173-
// It is safe to mutate the object inside this function, since it's always
174-
// called with an object's deep copy.
175-
type TransformFn func(in runtime.Object) (runtime.Object, error)
165+
// ReconcileFn is a function which mutates the existing object into it's desired state.
166+
// If there is no existing object, a zero value object is passed in.
167+
type ReconcileFn func(existing runtime.Object) error

pkg/controller/controllerutil/controllerutil_test.go

Lines changed: 74 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package controllerutil_test
22

33
import (
44
"context"
5+
"fmt"
6+
"math/rand"
57

68
. "github.com/onsi/ginkgo"
79
. "github.com/onsi/gomega"
@@ -98,64 +100,91 @@ var _ = Describe("Controllerutil", func() {
98100
})
99101

100102
Describe("CreateOrUpdate", func() {
103+
var deploy *appsv1.Deployment
104+
var deplSpec appsv1.DeploymentSpec
105+
var deplKey types.NamespacedName
106+
107+
BeforeEach(func() {
108+
deploy = &appsv1.Deployment{
109+
ObjectMeta: metav1.ObjectMeta{
110+
Name: fmt.Sprintf("deploy-%d", rand.Int31()),
111+
Namespace: "default",
112+
},
113+
}
101114

102-
It("creates a new object if one doesn't exists", func() {
103-
deplKey := types.NamespacedName{Name: "test-create", Namespace: "default"}
104-
depl := &appsv1.Deployment{}
115+
deplSpec = appsv1.DeploymentSpec{
116+
Selector: &metav1.LabelSelector{
117+
MatchLabels: map[string]string{"foo": "bar"},
118+
},
119+
Template: corev1.PodTemplateSpec{
120+
ObjectMeta: metav1.ObjectMeta{
121+
Labels: map[string]string{
122+
"foo": "bar",
123+
},
124+
},
125+
Spec: corev1.PodSpec{
126+
Containers: []corev1.Container{
127+
corev1.Container{
128+
Name: "busybox",
129+
Image: "busybox",
130+
},
131+
},
132+
},
133+
},
134+
}
135+
136+
deplKey = types.NamespacedName{
137+
Name: deploy.Name,
138+
Namespace: deploy.Namespace,
139+
}
140+
})
105141

106-
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deplKey, depl, createDeployment)
142+
It("creates a new object if one doesn't exists", func() {
143+
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentSpecr(deplSpec))
107144

108145
By("returning OperationCreated")
109-
Expect(op).Should(BeEquivalentTo(controllerutil.OperationCreated))
146+
Expect(op).To(BeEquivalentTo(controllerutil.OperationCreate))
110147

111-
By("returning returning no error")
112-
Expect(err).ShouldNot(HaveOccurred())
148+
By("returning no error")
149+
Expect(err).NotTo(HaveOccurred())
113150

114151
By("actually having the deployment created")
115152
fetched := &appsv1.Deployment{}
116-
Expect(c.Get(context.TODO(), deplKey, fetched)).Should(Succeed())
153+
Expect(c.Get(context.TODO(), deplKey, fetched)).To(Succeed())
117154
})
118155

119-
It("update existing object", func() {
120-
deplKey := types.NamespacedName{Name: "test-update", Namespace: "default"}
121-
d, _ := createDeployment(&appsv1.Deployment{})
122-
depl := d.(*appsv1.Deployment)
123-
depl.Name = "test-update"
124-
depl.Namespace = "default"
125-
156+
It("updates existing object", func() {
126157
var scale int32 = 2
158+
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentSpecr(deplSpec))
159+
Expect(err).NotTo(HaveOccurred())
160+
Expect(op).To(BeEquivalentTo(controllerutil.OperationCreate))
127161

128-
Expect(c.Create(context.TODO(), depl)).Should(Succeed())
129-
130-
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deplKey, &appsv1.Deployment{}, deploymentScaler(scale))
131-
162+
op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentScaler(scale))
132163
By("returning OperationUpdated")
133-
Expect(op).Should(BeEquivalentTo(controllerutil.OperationUpdated))
164+
Expect(op).To(BeEquivalentTo(controllerutil.OperationUpdate))
134165

135-
By("returning returning no error")
136-
Expect(err).ShouldNot(HaveOccurred())
166+
By("returning no error")
167+
Expect(err).NotTo(HaveOccurred())
137168

138169
By("actually having the deployment scaled")
139170
fetched := &appsv1.Deployment{}
140-
Expect(c.Get(context.TODO(), deplKey, fetched)).Should(Succeed())
171+
Expect(c.Get(context.TODO(), deplKey, fetched)).To(Succeed())
141172
Expect(*fetched.Spec.Replicas).To(Equal(scale))
142173
})
143174

144175
It("updates only changed objects", func() {
145-
deplKey := types.NamespacedName{Name: "test-idempotency", Namespace: "default"}
146-
depl := &appsv1.Deployment{}
176+
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentSpecr(deplSpec))
147177

148-
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deplKey, depl, createDeployment)
149-
Expect(op).Should(BeEquivalentTo(controllerutil.OperationCreated))
150-
Expect(err).ShouldNot(HaveOccurred())
178+
Expect(op).To(BeEquivalentTo(controllerutil.OperationCreate))
179+
Expect(err).NotTo(HaveOccurred())
151180

152-
op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deplKey, depl, deploymentIdentity)
181+
op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentIdentity)
153182

154183
By("returning OperationNoop")
155-
Expect(op).Should(BeEquivalentTo(controllerutil.OperationNoop))
184+
Expect(op).To(BeEquivalentTo(controllerutil.OperationNoop))
156185

157-
By("returning returning no error")
158-
Expect(err).ShouldNot(HaveOccurred())
186+
By("returning no error")
187+
Expect(err).NotTo(HaveOccurred())
159188
})
160189
})
161190
})
@@ -166,24 +195,23 @@ type errMetaObj struct {
166195
metav1.ObjectMeta
167196
}
168197

169-
var createDeployment controllerutil.TransformFn = func(in runtime.Object) (runtime.Object, error) {
170-
out := in.(*appsv1.Deployment)
171-
out.Spec.Selector = &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}
172-
out.Spec.Template.ObjectMeta.Labels = map[string]string{"foo": "bar"}
173-
out.Spec.Template.Spec.Containers = []corev1.Container{corev1.Container{Name: "foo", Image: "busybox"}}
174-
return out, nil
198+
func deploymentSpecr(spec appsv1.DeploymentSpec) controllerutil.ReconcileFn {
199+
return func(obj runtime.Object) error {
200+
deploy := obj.(*appsv1.Deployment)
201+
deploy.Spec = spec
202+
return nil
203+
}
175204
}
176205

177-
var deploymentIdentity controllerutil.TransformFn = func(in runtime.Object) (runtime.Object, error) {
178-
return in, nil
206+
var deploymentIdentity controllerutil.ReconcileFn = func(obj runtime.Object) error {
207+
return nil
179208
}
180209

181-
func deploymentScaler(replicas int32) controllerutil.TransformFn {
182-
fn := func(in runtime.Object) (runtime.Object, error) {
183-
d, _ := createDeployment(in)
184-
out := d.(*appsv1.Deployment)
185-
out.Spec.Replicas = &replicas
186-
return out, nil
210+
func deploymentScaler(replicas int32) controllerutil.ReconcileFn {
211+
fn := func(obj runtime.Object) error {
212+
deploy := obj.(*appsv1.Deployment)
213+
deploy.Spec.Replicas = &replicas
214+
return nil
187215
}
188216
return fn
189217
}
Lines changed: 78 additions & 0 deletions
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)