Skip to content

Commit 25c5bfc

Browse files
committed
fix: reject unsupported watch-list options
1 parent 4f807a7 commit 25c5bfc

4 files changed

Lines changed: 131 additions & 0 deletions

File tree

internal/aggregated/storage/template.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ func (s *TemplateStorage) Watch(ctx context.Context, opts *metainternalversion.L
197197
return nil, err
198198
}
199199

200+
if err := validateUnsupportedWatchListOptions(opts); err != nil {
201+
return nil, apierrors.NewBadRequest(fmt.Sprintf("invalid watch options: %v", err))
202+
}
203+
200204
filter, err := filterForListOptions(requestNamespace, opts)
201205
if err != nil {
202206
return nil, apierrors.NewBadRequest(fmt.Sprintf("invalid watch options: %v", err))

internal/aggregated/storage/watch.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,26 @@ func validateFieldSelector(sel fields.Selector) error {
3838
return nil
3939
}
4040

41+
// validateUnsupportedWatchListOptions rejects watch-list options that this storage
42+
// does not implement yet.
43+
func validateUnsupportedWatchListOptions(opts *metainternalversion.ListOptions) error {
44+
if opts == nil {
45+
return nil
46+
}
47+
48+
if opts.SendInitialEvents != nil {
49+
return fmt.Errorf("sendInitialEvents is not supported for this watch endpoint")
50+
}
51+
if opts.ResourceVersionMatch != "" {
52+
return fmt.Errorf(
53+
"resourceVersionMatch %q is not supported for this watch endpoint",
54+
opts.ResourceVersionMatch,
55+
)
56+
}
57+
58+
return nil
59+
}
60+
4161
// filterForListOptions builds a watch.FilterFunc that applies namespace, label, and field selector filtering.
4262
// Returns nil if no filtering is needed.
4363
func filterForListOptions(requestNamespace string, opts *metainternalversion.ListOptions) (watch.FilterFunc, error) {

internal/aggregated/storage/watch_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"time"
88

99
"github.com/google/uuid"
10+
apierrors "k8s.io/apimachinery/pkg/api/errors"
1011
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
1112
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1213
"k8s.io/apimachinery/pkg/fields"
@@ -288,6 +289,87 @@ func TestWatchStopsOnContextCancel(t *testing.T) {
288289
assertWatchClosed(t, watcher, watchEventTimeout)
289290
}
290291

292+
func TestWorkspaceWatchRejectsUnsupportedWatchListOptions(t *testing.T) {
293+
t.Parallel()
294+
295+
server, _ := newMockCoderServer(t)
296+
defer server.Close()
297+
298+
workspaceStorage := NewWorkspaceStorage(newTestClientProvider(t, server.URL))
299+
defer workspaceStorage.Destroy()
300+
301+
ctx := namespacedContext("control-plane")
302+
303+
t.Run("sendInitialEvents", func(t *testing.T) {
304+
sendInitialEvents := true
305+
watcher, err := workspaceStorage.Watch(ctx, &metainternalversion.ListOptions{SendInitialEvents: &sendInitialEvents})
306+
assertBadRequestWatchOptionsError(t, watcher, err, "sendInitialEvents")
307+
})
308+
309+
t.Run("resourceVersionMatch", func(t *testing.T) {
310+
watcher, err := workspaceStorage.Watch(ctx, &metainternalversion.ListOptions{
311+
ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
312+
})
313+
assertBadRequestWatchOptionsError(t, watcher, err, "resourceVersionMatch")
314+
})
315+
}
316+
317+
func TestTemplateWatchRejectsUnsupportedWatchListOptions(t *testing.T) {
318+
t.Parallel()
319+
320+
server, _ := newMockCoderServer(t)
321+
defer server.Close()
322+
323+
templateStorage := NewTemplateStorage(newTestClientProvider(t, server.URL))
324+
defer templateStorage.Destroy()
325+
326+
ctx := namespacedContext("control-plane")
327+
328+
t.Run("sendInitialEvents", func(t *testing.T) {
329+
sendInitialEvents := true
330+
watcher, err := templateStorage.Watch(ctx, &metainternalversion.ListOptions{SendInitialEvents: &sendInitialEvents})
331+
assertBadRequestWatchOptionsError(t, watcher, err, "sendInitialEvents")
332+
})
333+
334+
t.Run("resourceVersionMatch", func(t *testing.T) {
335+
watcher, err := templateStorage.Watch(ctx, &metainternalversion.ListOptions{
336+
ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
337+
})
338+
assertBadRequestWatchOptionsError(t, watcher, err, "resourceVersionMatch")
339+
})
340+
}
341+
342+
func TestValidateUnsupportedWatchListOptions(t *testing.T) {
343+
t.Parallel()
344+
345+
if err := validateUnsupportedWatchListOptions(nil); err != nil {
346+
t.Fatalf("expected nil list options to be accepted, got %v", err)
347+
}
348+
349+
if err := validateUnsupportedWatchListOptions(&metainternalversion.ListOptions{}); err != nil {
350+
t.Fatalf("expected empty list options to be accepted, got %v", err)
351+
}
352+
353+
sendInitialEvents := true
354+
err := validateUnsupportedWatchListOptions(&metainternalversion.ListOptions{SendInitialEvents: &sendInitialEvents})
355+
if err == nil {
356+
t.Fatal("expected sendInitialEvents to be rejected")
357+
}
358+
if !strings.Contains(err.Error(), "sendInitialEvents") {
359+
t.Fatalf("expected sendInitialEvents error, got %v", err)
360+
}
361+
362+
err = validateUnsupportedWatchListOptions(&metainternalversion.ListOptions{
363+
ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
364+
})
365+
if err == nil {
366+
t.Fatal("expected resourceVersionMatch to be rejected")
367+
}
368+
if !strings.Contains(err.Error(), "resourceVersionMatch") {
369+
t.Fatalf("expected resourceVersionMatch error, got %v", err)
370+
}
371+
}
372+
291373
func TestValidateFieldSelector(t *testing.T) {
292374
t.Parallel()
293375

@@ -459,6 +541,27 @@ func assertWatchClosed(t *testing.T, watcher watch.Interface, timeout time.Durat
459541
}
460542
}
461543

544+
func assertBadRequestWatchOptionsError(t *testing.T, watcher watch.Interface, err error, wantErrSubstring string) {
545+
t.Helper()
546+
547+
if err == nil {
548+
if watcher != nil {
549+
watcher.Stop()
550+
}
551+
t.Fatalf("expected watch options error containing %q", wantErrSubstring)
552+
}
553+
if watcher != nil {
554+
watcher.Stop()
555+
t.Fatalf("expected watcher to be nil when watch options are rejected, got %T", watcher)
556+
}
557+
if !apierrors.IsBadRequest(err) {
558+
t.Fatalf("expected bad request error, got %v", err)
559+
}
560+
if !strings.Contains(err.Error(), wantErrSubstring) {
561+
t.Fatalf("expected error containing %q, got %v", wantErrSubstring, err)
562+
}
563+
}
564+
462565
func templateFromWatchEvent(t *testing.T, evt watch.Event) *aggregationv1alpha1.CoderTemplate {
463566
t.Helper()
464567

internal/aggregated/storage/workspace.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ func (s *WorkspaceStorage) Watch(ctx context.Context, opts *metainternalversion.
186186
return nil, err
187187
}
188188

189+
if err := validateUnsupportedWatchListOptions(opts); err != nil {
190+
return nil, apierrors.NewBadRequest(fmt.Sprintf("invalid watch options: %v", err))
191+
}
192+
189193
filter, err := filterForListOptions(requestNamespace, opts)
190194
if err != nil {
191195
return nil, apierrors.NewBadRequest(fmt.Sprintf("invalid watch options: %v", err))

0 commit comments

Comments
 (0)