Skip to content

Commit 3b338eb

Browse files
authored
Merge pull request #96 from droot/client/update-status-subresource
client: update status subresource support
2 parents 741f067 + ad4fd79 commit 3b338eb

File tree

6 files changed

+193
-16
lines changed

6 files changed

+193
-16
lines changed

pkg/client/client.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,36 @@ func (c *client) List(ctx context.Context, opts *ListOptions, obj runtime.Object
154154
Do().
155155
Into(obj)
156156
}
157+
158+
// Status implements client.StatusClient
159+
func (c *client) Status() StatusWriter {
160+
return &statusWriter{client: c}
161+
}
162+
163+
// statusWriter is client.StatusWriter that writes status subresource
164+
type statusWriter struct {
165+
client *client
166+
}
167+
168+
// ensure statusWriter implements client.StatusWriter
169+
var _ StatusWriter = &statusWriter{}
170+
171+
// Update implements client.StatusWriter
172+
func (sw *statusWriter) Update(_ context.Context, obj runtime.Object) error {
173+
o, err := sw.client.cache.getObjMeta(obj)
174+
if err != nil {
175+
return err
176+
}
177+
// TODO(droot): examine the returned error and check if it error needs to be
178+
// wrapped to improve the UX ?
179+
// It will be nice to receive an error saying the object doesn't implement
180+
// status subresource and check CRD definition
181+
return o.Put().
182+
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
183+
Resource(o.resource()).
184+
Name(o.GetName()).
185+
SubResource("status").
186+
Body(obj).
187+
Do().
188+
Into(obj)
189+
}

pkg/client/client_test.go

Lines changed: 128 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,15 @@ var _ = Describe("Client", func() {
5858
var pod *corev1.Pod
5959
var node *corev1.Node
6060
var count uint64 = 0
61+
var replicaCount int32 = 2
6162
var ns = "default"
6263

6364
BeforeEach(func(done Done) {
6465
atomic.AddUint64(&count, 1)
6566
dep = &appsv1.Deployment{
6667
ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("deployment-name-%v", count), Namespace: ns},
6768
Spec: appsv1.DeploymentSpec{
69+
Replicas: &replicaCount,
6870
Selector: &metav1.LabelSelector{
6971
MatchLabels: map[string]string{"foo": "bar"},
7072
},
@@ -143,7 +145,7 @@ var _ = Describe("Client", func() {
143145
close(done)
144146
})
145147

146-
It("should use the provided Mapper if provided", func() {
148+
PIt("should use the provided Mapper if provided", func() {
147149

148150
})
149151

@@ -267,7 +269,7 @@ var _ = Describe("Client", func() {
267269
Expect(err.Error()).To(ContainSubstring("no kind is registered for the type"))
268270
})
269271

270-
It("should fail if the GVK cannot be mapped to a Resource", func() {
272+
PIt("should fail if the GVK cannot be mapped to a Resource", func() {
271273
// TODO(seans3): implement these
272274
// Example: ListOptions
273275
})
@@ -358,11 +360,11 @@ var _ = Describe("Client", func() {
358360
close(done)
359361
})
360362

361-
It("should fail if the object does not pass server-side validation", func() {
363+
PIt("should fail if the object does not pass server-side validation", func() {
362364

363365
})
364366

365-
It("should fail if the object doesn't have meta", func() {
367+
PIt("should fail if the object doesn't have meta", func() {
366368

367369
})
368370

@@ -386,7 +388,120 @@ var _ = Describe("Client", func() {
386388
close(done)
387389
})
388390

389-
It("should fail if the GVK cannot be mapped to a Resource", func() {
391+
PIt("should fail if the GVK cannot be mapped to a Resource", func() {
392+
393+
})
394+
})
395+
396+
Describe("StatusClient", func() {
397+
It("should update status of an existing object", func(done Done) {
398+
cl, err := client.New(cfg, client.Options{})
399+
Expect(err).NotTo(HaveOccurred())
400+
Expect(cl).NotTo(BeNil())
401+
402+
By("initially creating a Deployment")
403+
dep, err := clientset.AppsV1().Deployments(ns).Create(dep)
404+
Expect(err).NotTo(HaveOccurred())
405+
406+
By("updating the status of Deployment")
407+
dep.Status.Replicas = 1
408+
err = cl.Status().Update(context.TODO(), dep)
409+
Expect(err).NotTo(HaveOccurred())
410+
411+
By("validating updated Deployment has new status")
412+
actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
413+
Expect(err).NotTo(HaveOccurred())
414+
Expect(actual).NotTo(BeNil())
415+
Expect(actual.Status.Replicas).To(BeEquivalentTo(1))
416+
417+
close(done)
418+
})
419+
420+
It("should not update spec of an existing object", func(done Done) {
421+
cl, err := client.New(cfg, client.Options{})
422+
Expect(err).NotTo(HaveOccurred())
423+
Expect(cl).NotTo(BeNil())
424+
425+
By("initially creating a Deployment")
426+
dep, err := clientset.AppsV1().Deployments(ns).Create(dep)
427+
Expect(err).NotTo(HaveOccurred())
428+
429+
By("updating the spec and status of Deployment")
430+
var rc int32 = 1
431+
dep.Status.Replicas = 1
432+
dep.Spec.Replicas = &rc
433+
err = cl.Status().Update(context.TODO(), dep)
434+
Expect(err).NotTo(HaveOccurred())
435+
436+
By("validating updated Deployment has new status and unchanged spec")
437+
actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
438+
Expect(err).NotTo(HaveOccurred())
439+
Expect(actual).NotTo(BeNil())
440+
Expect(actual.Status.Replicas).To(BeEquivalentTo(1))
441+
Expect(*actual.Spec.Replicas).To(BeEquivalentTo(replicaCount))
442+
443+
close(done)
444+
})
445+
446+
It("should update an existing object non-namespace object", func(done Done) {
447+
cl, err := client.New(cfg, client.Options{})
448+
Expect(err).NotTo(HaveOccurred())
449+
Expect(cl).NotTo(BeNil())
450+
451+
node, err := clientset.CoreV1().Nodes().Create(node)
452+
Expect(err).NotTo(HaveOccurred())
453+
454+
By("updating status of the object")
455+
node.Status.Phase = corev1.NodeRunning
456+
err = cl.Status().Update(context.TODO(), node)
457+
Expect(err).NotTo(HaveOccurred())
458+
459+
By("validate updated Node had new annotation")
460+
actual, err := clientset.CoreV1().Nodes().Get(node.Name, metav1.GetOptions{})
461+
Expect(err).NotTo(HaveOccurred())
462+
Expect(actual).NotTo(BeNil())
463+
Expect(actual.Status.Phase).To(Equal(corev1.NodeRunning))
464+
465+
close(done)
466+
})
467+
468+
It("should fail if the object does not exists", func(done Done) {
469+
cl, err := client.New(cfg, client.Options{})
470+
Expect(err).NotTo(HaveOccurred())
471+
Expect(cl).NotTo(BeNil())
472+
473+
By("updating status of a non-existent object")
474+
err = cl.Status().Update(context.TODO(), dep)
475+
Expect(err).To(HaveOccurred())
476+
477+
close(done)
478+
})
479+
480+
It("should fail if the object cannot be mapped to a GVK", func(done Done) {
481+
By("creating client with empty Scheme")
482+
emptyScheme := runtime.NewScheme()
483+
cl, err := client.New(cfg, client.Options{Scheme: emptyScheme})
484+
Expect(err).NotTo(HaveOccurred())
485+
Expect(cl).NotTo(BeNil())
486+
487+
By("initially creating a Deployment")
488+
dep, err := clientset.AppsV1().Deployments(ns).Create(dep)
489+
Expect(err).NotTo(HaveOccurred())
490+
491+
By("updating status of the Deployment")
492+
dep.Status.Replicas = 1
493+
err = cl.Status().Update(context.TODO(), dep)
494+
Expect(err).To(HaveOccurred())
495+
Expect(err.Error()).To(ContainSubstring("no kind is registered for the type"))
496+
497+
close(done)
498+
})
499+
500+
PIt("should fail if the GVK cannot be mapped to a Resource", func() {
501+
502+
})
503+
504+
PIt("should fail if an API does not implement Status subresource", func() {
390505

391506
})
392507
})
@@ -471,7 +586,7 @@ var _ = Describe("Client", func() {
471586
close(done)
472587
})
473588

474-
It("should fail if the object doesn't have meta", func() {
589+
PIt("should fail if the object doesn't have meta", func() {
475590

476591
})
477592

@@ -494,7 +609,7 @@ var _ = Describe("Client", func() {
494609
close(done)
495610
})
496611

497-
It("should fail if the GVK cannot be mapped to a Resource", func() {
612+
PIt("should fail if the GVK cannot be mapped to a Resource", func() {
498613

499614
})
500615
})
@@ -609,7 +724,7 @@ var _ = Describe("Client", func() {
609724
close(done)
610725
})
611726

612-
It("should fail if the object doesn't have meta", func() {
727+
PIt("should fail if the object doesn't have meta", func() {
613728

614729
})
615730

@@ -632,7 +747,7 @@ var _ = Describe("Client", func() {
632747
Expect(err.Error()).To(ContainSubstring("no kind is registered for the type"))
633748
})
634749

635-
It("should fail if the GVK cannot be mapped to a Resource", func() {
750+
PIt("should fail if the GVK cannot be mapped to a Resource", func() {
636751

637752
})
638753
})
@@ -846,19 +961,19 @@ var _ = Describe("Client", func() {
846961
close(done)
847962
}, serverSideTimeoutSeconds)
848963

849-
It("should fail if it cannot get a client", func() {
964+
PIt("should fail if it cannot get a client", func() {
850965

851966
})
852967

853-
It("should fail if the object doesn't have meta", func() {
968+
PIt("should fail if the object doesn't have meta", func() {
854969

855970
})
856971

857-
It("should fail if the object cannot be mapped to a GVK", func() {
972+
PIt("should fail if the object cannot be mapped to a GVK", func() {
858973

859974
})
860975

861-
It("should fail if the GVK cannot be mapped to a Resource", func() {
976+
PIt("should fail if the GVK cannot be mapped to a Resource", func() {
862977

863978
})
864979
})

pkg/client/fake/client.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ func (c *fakeClient) Update(ctx context.Context, obj runtime.Object) error {
129129
return c.tracker.Update(gvr, obj, accessor.GetNamespace())
130130
}
131131

132+
func (c *fakeClient) Status() client.StatusWriter {
133+
return &fakeStatusWriter{client: c}
134+
}
135+
132136
func getGVRFromObject(obj runtime.Object) (schema.GroupVersionResource, error) {
133137
gvk, err := apiutil.GVKForObject(obj, scheme.Scheme)
134138
if err != nil {
@@ -137,3 +141,13 @@ func getGVRFromObject(obj runtime.Object) (schema.GroupVersionResource, error) {
137141
gvr, _ := meta.UnsafeGuessKindToResource(gvk)
138142
return gvr, nil
139143
}
144+
145+
type fakeStatusWriter struct {
146+
client *fakeClient
147+
}
148+
149+
func (sw *fakeStatusWriter) Update(ctx context.Context, obj runtime.Object) error {
150+
// TODO(droot): This results in full update of the obj (spec + status). Need
151+
// a way to update status field only.
152+
return sw.client.Update(ctx, obj)
153+
}

pkg/client/interfaces.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,25 @@ type Writer interface {
5757
Update(ctx context.Context, obj runtime.Object) error
5858
}
5959

60+
// StatusClient knows how to create a client which can update status subresource
61+
// for kubernetes objects.
62+
type StatusClient interface {
63+
Status() StatusWriter
64+
}
65+
66+
// StatusWriter knows how to update status subresource of a Kubernetes object.
67+
type StatusWriter interface {
68+
// Update updates the fields corresponding to the status subresource for the
69+
// given obj. obj must be a struct pointer so that obj can be updated
70+
// with the content returned by the Server.
71+
Update(ctx context.Context, obj runtime.Object) error
72+
}
73+
6074
// Client knows how to perform CRUD operations on Kubernetes objects.
6175
type Client interface {
6276
Reader
6377
Writer
78+
StatusClient
6479
}
6580

6681
// IndexerFunc knows how to take an object and turn it into a series

pkg/client/split.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ limitations under the License.
1717
package client
1818

1919
// DelegatingClient forms an interface Client by composing separate
20-
// read and write interfaces. This way, you can have an Client that
20+
// reader, writer and statusclient interfaces. This way, you can have an Client that
2121
// reads from a cache and writes to the API server.
2222
type DelegatingClient struct {
2323
Reader
2424
Writer
25+
StatusClient
2526
}

pkg/manager/manager.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,6 @@ func New(config *rest.Config, options Options) (Manager, error) {
130130
cache, err := options.newCache(config, cache.Options{Scheme: options.Scheme, Mapper: mapper, Resync: options.SyncPeriod})
131131
if err != nil {
132132
return nil, err
133-
134133
}
135134
// Create the recorder provider to inject event recorders for the components.
136135
recorderProvider, err := options.newRecorderProvider(config, options.Scheme)
@@ -144,7 +143,7 @@ func New(config *rest.Config, options Options) (Manager, error) {
144143
errChan: make(chan error),
145144
cache: cache,
146145
fieldIndexes: cache,
147-
client: client.DelegatingClient{Reader: cache, Writer: writeObj},
146+
client: client.DelegatingClient{Reader: cache, Writer: writeObj, StatusClient: writeObj},
148147
recorderProvider: recorderProvider,
149148
}, nil
150149
}

0 commit comments

Comments
 (0)