Skip to content
This repository was archived by the owner on Nov 19, 2020. It is now read-only.

Commit 76f5275

Browse files
committed
*: allow watches on individual resources
1 parent db7ff61 commit 76f5275

File tree

4 files changed

+196
-28
lines changed

4 files changed

+196
-28
lines changed

resource.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,11 +260,25 @@ func resourceWatchURL(endpoint, namespace string, r Resource, options ...Option)
260260
return "", fmt.Errorf("unregistered type %T", r)
261261
}
262262

263+
// Hack to let watch work on individual resources
264+
name := ""
265+
if meta := r.GetMetadata(); meta != nil && meta.Name != nil {
266+
name = *meta.Name
267+
if meta.Namespace != nil {
268+
// Ensure that namespaces aren't different.
269+
ns := *meta.Namespace
270+
if namespace != "" && ns != namespace {
271+
return "", fmt.Errorf("different namespace provided on resource than to watch call")
272+
}
273+
namespace = ns
274+
}
275+
}
276+
263277
if !t.namespaced && namespace != "" {
264278
return "", fmt.Errorf("type not namespaced")
265279
}
266280

267-
url := urlFor(endpoint, t.apiGroup, t.apiVersion, namespace, t.name, "", options...)
281+
url := urlFor(endpoint, t.apiGroup, t.apiVersion, namespace, t.name, name, options...)
268282
if strings.Contains(url, "?") {
269283
url = url + "&watch=true"
270284
} else {

resource_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,113 @@ func TestResourceURL(t *testing.T) {
161161
})
162162
}
163163
}
164+
165+
func TestResourceWatchURL(t *testing.T) {
166+
tests := []struct {
167+
name string
168+
endpoint string
169+
namespace string
170+
resource Resource
171+
options []Option
172+
want string
173+
wantErr bool
174+
}{
175+
{
176+
name: "watch_pods",
177+
namespace: "my-namespace",
178+
endpoint: "https://k8s.example.com/foo/",
179+
resource: &Pod{},
180+
want: "https://k8s.example.com/foo/api/v1/namespaces/my-namespace/pods?watch=true",
181+
},
182+
{
183+
name: "watch_all_pods",
184+
endpoint: "https://k8s.example.com/foo/",
185+
resource: &Pod{},
186+
want: "https://k8s.example.com/foo/api/v1/pods?watch=true",
187+
},
188+
{
189+
name: "watch_deployments",
190+
namespace: "my-namespace",
191+
endpoint: "https://k8s.example.com/foo/",
192+
resource: &Deployment{},
193+
want: "https://k8s.example.com/foo/apis/apps/v1beta2/namespaces/my-namespace/deployments?watch=true",
194+
},
195+
{
196+
name: "watch_with_options",
197+
namespace: "my-namespace",
198+
endpoint: "https://k8s.example.com/foo/",
199+
resource: &Deployment{},
200+
options: []Option{
201+
Timeout(time.Minute),
202+
},
203+
want: "https://k8s.example.com/foo/apis/apps/v1beta2/namespaces/my-namespace/deployments?timeoutSeconds=60&watch=true",
204+
},
205+
{
206+
name: "watch_non_namespaced",
207+
endpoint: "https://k8s.example.com/foo/",
208+
resource: &ClusterRole{},
209+
want: "https://k8s.example.com/foo/apis/rbac.authorization.k8s.io/v1/clusterroles?watch=true",
210+
},
211+
{
212+
name: "watch_non_namespaced_with_namespace",
213+
namespace: "my-namespace",
214+
endpoint: "https://k8s.example.com/foo/",
215+
resource: &ClusterRole{},
216+
wantErr: true, // can't provide a namespace for a non-namespaced resource
217+
},
218+
{
219+
name: "watch_deployment",
220+
endpoint: "https://k8s.example.com/foo/",
221+
resource: &Deployment{
222+
Metadata: &metav1.ObjectMeta{
223+
Namespace: String("my-namespace"),
224+
Name: String("my-deployment"),
225+
},
226+
},
227+
want: "https://k8s.example.com/foo/apis/apps/v1beta2/namespaces/my-namespace/deployments/my-deployment?watch=true",
228+
},
229+
{
230+
name: "watch_deployment_ns_in_call",
231+
endpoint: "https://k8s.example.com/foo/",
232+
namespace: "my-namespace",
233+
resource: &Deployment{
234+
Metadata: &metav1.ObjectMeta{
235+
Name: String("my-deployment"),
236+
},
237+
},
238+
want: "https://k8s.example.com/foo/apis/apps/v1beta2/namespaces/my-namespace/deployments/my-deployment?watch=true",
239+
},
240+
{
241+
name: "watch_deployment_mismatched_ns",
242+
endpoint: "https://k8s.example.com/foo/",
243+
namespace: "my-other-namespace",
244+
resource: &Deployment{
245+
Metadata: &metav1.ObjectMeta{
246+
Namespace: String("my-namespace"),
247+
Name: String("my-deployment"),
248+
},
249+
},
250+
wantErr: true,
251+
},
252+
}
253+
for _, test := range tests {
254+
t.Run(test.name, func(t *testing.T) {
255+
got, err := resourceWatchURL(
256+
test.endpoint,
257+
test.namespace,
258+
test.resource,
259+
test.options...,
260+
)
261+
if err != nil {
262+
if !test.wantErr {
263+
t.Fatalf("resourceWatchURL: %v", err)
264+
}
265+
return
266+
}
267+
if got != test.want {
268+
t.Errorf("want: %q", test.want)
269+
t.Errorf("got : %q", got)
270+
}
271+
})
272+
}
273+
}

watch.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,22 @@ func parseUnknown(b []byte) (*runtime.Unknown, error) {
160160
// fmt.Println(eventType, *cm.Metadata.Name)
161161
// }
162162
//
163+
// To watch an individual resource, provide a resource with pre-populated
164+
// metadata:
165+
//
166+
// // Watch "my-configmap" in "my-namespace"
167+
// configMap := corev1.ConfigMap{
168+
// Metadata: &metav1.ObjectMeta{
169+
// Namespace: String("my-namespace"),
170+
// Name: String("my-configmap"),
171+
// },
172+
// }
173+
// watcher, err := client.Watch(ctx, "", &configMap)
174+
// if err != nil {
175+
// // handle error
176+
// }
177+
// defer watcher.Close() // Always close the returned watcher.
178+
//
163179
func (c *Client) Watch(ctx context.Context, namespace string, r Resource, options ...Option) (*Watcher, error) {
164180
url, err := resourceWatchURL(c.Endpoint, namespace, r, options...)
165181
if err != nil {

watch_test.go

Lines changed: 55 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,52 +24,62 @@ func init() {
2424
k8s.Register("", "v1", "configmaps", true, &configMapJSON{})
2525
}
2626

27-
func testWatch(t *testing.T, client *k8s.Client, namespace string, newCM func() k8s.Resource, update func(cm k8s.Resource)) {
28-
w, err := client.Watch(context.TODO(), namespace, newCM())
27+
func wantEvent(t *testing.T, w *k8s.Watcher, eventType string, got, want k8s.Resource) {
28+
t.Helper()
29+
eT, err := w.Next(got)
2930
if err != nil {
30-
t.Errorf("watch configmaps: %v", err)
31+
t.Errorf("decode watch event: %v", err)
32+
return
3133
}
32-
defer w.Close()
34+
if eT != eventType {
35+
t.Errorf("expected event type %q got %q", eventType, eT)
36+
}
37+
want.GetMetadata().ResourceVersion = k8s.String("")
38+
got.GetMetadata().ResourceVersion = k8s.String("")
39+
if !reflect.DeepEqual(got, want) {
40+
t.Errorf("configmaps didn't match")
41+
t.Errorf("want: %#v", want)
42+
t.Errorf(" got: %#v", got)
43+
}
44+
}
3345

46+
func testWatch(t *testing.T, client *k8s.Client, namespace string, r k8s.Resource, newCM func() k8s.Resource, update func(cm k8s.Resource)) {
3447
cm := newCM()
35-
want := func(eventType string) {
36-
got := newCM()
37-
eT, err := w.Next(got)
38-
if err != nil {
39-
t.Errorf("decode watch event: %v", err)
48+
49+
if r.GetMetadata() != nil {
50+
// Individual watch must created beforehand
51+
if err := client.Create(context.TODO(), cm); err != nil {
52+
t.Errorf("create configmap: %v", err)
4053
return
4154
}
42-
if eT != eventType {
43-
t.Errorf("expected event type %q got %q", eventType, eT)
44-
}
45-
cm.GetMetadata().ResourceVersion = k8s.String("")
46-
got.GetMetadata().ResourceVersion = k8s.String("")
47-
if !reflect.DeepEqual(got, cm) {
48-
t.Errorf("configmaps didn't match")
49-
t.Errorf("want: %#v", cm)
50-
t.Errorf(" got: %#v", got)
51-
}
5255
}
56+
w, err := client.Watch(context.TODO(), namespace, r)
57+
if err != nil {
58+
t.Fatalf("watch configmaps: %v", err)
59+
}
60+
defer w.Close()
5361

54-
if err := client.Create(context.TODO(), cm); err != nil {
55-
t.Errorf("create configmap: %v", err)
56-
return
62+
if r.GetMetadata() == nil {
63+
if err := client.Create(context.TODO(), cm); err != nil {
64+
t.Errorf("create configmap: %v", err)
65+
return
66+
}
67+
wantEvent(t, w, k8s.EventAdded, newCM(), cm)
5768
}
58-
want(k8s.EventAdded)
5969

6070
update(cm)
6171

6272
if err := client.Update(context.TODO(), cm); err != nil {
6373
t.Errorf("update configmap: %v", err)
6474
return
6575
}
66-
want(k8s.EventModified)
76+
wantEvent(t, w, k8s.EventModified, newCM(), cm)
6777

6878
if err := client.Delete(context.TODO(), cm); err != nil {
6979
t.Errorf("Delete configmap: %v", err)
7080
return
7181
}
72-
want(k8s.EventDeleted)
82+
wantEvent(t, w, k8s.EventDeleted, newCM(), cm)
7383
}
7484

7585
func TestWatchConfigMapJSON(t *testing.T) {
@@ -86,7 +96,7 @@ func TestWatchConfigMapJSON(t *testing.T) {
8696
updateCM := func(cm k8s.Resource) {
8797
(cm.(*configMapJSON)).Data = map[string]string{"hello": "world"}
8898
}
89-
testWatch(t, client, namespace, newCM, updateCM)
99+
testWatch(t, client, namespace, &configMapJSON{}, newCM, updateCM)
90100
})
91101
}
92102

@@ -104,6 +114,24 @@ func TestWatchConfigMapProto(t *testing.T) {
104114
updateCM := func(cm k8s.Resource) {
105115
(cm.(*corev1.ConfigMap)).Data = map[string]string{"hello": "world"}
106116
}
107-
testWatch(t, client, namespace, newCM, updateCM)
117+
testWatch(t, client, namespace, &corev1.ConfigMap{}, newCM, updateCM)
118+
})
119+
}
120+
121+
func TestWatchIndividualConfigMap(t *testing.T) {
122+
withNamespace(t, func(client *k8s.Client, namespace string) {
123+
newCM := func() k8s.Resource {
124+
return &corev1.ConfigMap{
125+
Metadata: &metav1.ObjectMeta{
126+
Name: k8s.String("my-configmap"),
127+
Namespace: &namespace,
128+
},
129+
}
130+
}
131+
132+
updateCM := func(cm k8s.Resource) {
133+
(cm.(*corev1.ConfigMap)).Data = map[string]string{"hello": "world"}
134+
}
135+
testWatch(t, client, namespace, newCM(), newCM, updateCM)
108136
})
109137
}

0 commit comments

Comments
 (0)