Skip to content

Commit 3dbc006

Browse files
committed
client: status subresource support
1 parent 3aaf8cd commit 3dbc006

File tree

6 files changed

+180
-3
lines changed

6 files changed

+180
-3
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: 115 additions & 0 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
},
@@ -391,6 +393,119 @@ var _ = Describe("Client", func() {
391393
})
392394
})
393395

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+
It("should fail if the GVK cannot be mapped to a Resource", func() {
501+
502+
})
503+
504+
It("should fail if an API does not implement Status subresource", func() {
505+
506+
})
507+
})
508+
394509
Describe("Delete", func() {
395510
It("should delete an existing object from a go struct", func(done Done) {
396511
cl, err := client.New(cfg, client.Options{})

pkg/client/fake/client.go

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

126+
func (c *fakeClient) Status() client.StatusWriter {
127+
return &fakeStatusWriter{client: c}
128+
}
129+
126130
func getGVRFromObject(obj runtime.Object) (schema.GroupVersionResource, error) {
127131
gvk, err := apiutil.GVKForObject(obj, scheme.Scheme)
128132
if err != nil {
@@ -131,3 +135,13 @@ func getGVRFromObject(obj runtime.Object) (schema.GroupVersionResource, error) {
131135
gvr, _ := meta.UnsafeGuessKindToResource(gvk)
132136
return gvr, nil
133137
}
138+
139+
type fakeStatusWriter struct {
140+
client *fakeClient
141+
}
142+
143+
func (sw *fakeStatusWriter) Update(ctx context.Context, obj runtime.Object) error {
144+
// TODO(droot): This results in full update of the obj (spec + status). Need
145+
// a way to update status field only.
146+
return sw.client.Update(ctx, obj)
147+
}

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
@@ -132,7 +132,6 @@ func New(config *rest.Config, options Options) (Manager, error) {
132132
cache, err := options.newCache(config, cache.Options{Scheme: options.Scheme, Mapper: mapper, Resync: options.SyncPeriod})
133133
if err != nil {
134134
return nil, err
135-
136135
}
137136
// Create the recorder provider to inject event recorders for the components.
138137
recorderProvider, err := options.newRecorderProvider(config, options.Scheme)
@@ -146,7 +145,7 @@ func New(config *rest.Config, options Options) (Manager, error) {
146145
errChan: make(chan error),
147146
cache: cache,
148147
fieldIndexes: cache,
149-
client: client.DelegatingClient{Reader: cache, Writer: writeObj},
148+
client: client.DelegatingClient{Reader: cache, Writer: writeObj, StatusClient: writeObj},
150149
recorderProvider: recorderProvider,
151150
}, nil
152151
}

0 commit comments

Comments
 (0)