k8s.io/client-go@v0.31.1/rest/request_watchlist_test.go (about)

     1  /*
     2  Copyright 2024 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 rest
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"regexp"
    23  	"testing"
    24  
    25  	"github.com/google/go-cmp/cmp"
    26  
    27  	v1 "k8s.io/api/core/v1"
    28  	apiequality "k8s.io/apimachinery/pkg/api/equality"
    29  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/runtime"
    32  	"k8s.io/apimachinery/pkg/runtime/schema"
    33  	"k8s.io/apimachinery/pkg/watch"
    34  	clientfeatures "k8s.io/client-go/features"
    35  	clientfeaturestesting "k8s.io/client-go/features/testing"
    36  )
    37  
    38  func TestWatchListResult(t *testing.T) {
    39  	scenarios := []struct {
    40  		name   string
    41  		target WatchListResult
    42  		result runtime.Object
    43  
    44  		expectedResult *v1.PodList
    45  		expectedErr    error
    46  	}{
    47  		{
    48  			name:        "not a pointer",
    49  			result:      fakeObj{},
    50  			expectedErr: fmt.Errorf("rest.fakeObj is not a list: expected pointer, but got rest.fakeObj type"),
    51  		},
    52  		{
    53  			name:        "nil input won't panic",
    54  			result:      nil,
    55  			expectedErr: fmt.Errorf("<nil> is not a list: expected pointer, but got invalid kind"),
    56  		},
    57  		{
    58  			name:        "not a list",
    59  			result:      &v1.Pod{},
    60  			expectedErr: fmt.Errorf("*v1.Pod is not a list: no Items field in this object"),
    61  		},
    62  		{
    63  			name:        "an err is always returned",
    64  			result:      nil,
    65  			target:      WatchListResult{err: fmt.Errorf("dummy err")},
    66  			expectedErr: fmt.Errorf("dummy err"),
    67  		},
    68  		{
    69  			name:   "empty list",
    70  			result: &v1.PodList{},
    71  			expectedResult: &v1.PodList{
    72  				TypeMeta: metav1.TypeMeta{Kind: "PodList"},
    73  				Items:    []v1.Pod{},
    74  			},
    75  		},
    76  		{
    77  			name:   "gv is applied",
    78  			result: &v1.PodList{},
    79  			target: WatchListResult{gv: schema.GroupVersion{Group: "g", Version: "v"}},
    80  			expectedResult: &v1.PodList{
    81  				TypeMeta: metav1.TypeMeta{Kind: "PodList", APIVersion: "g/v"},
    82  				Items:    []v1.Pod{},
    83  			},
    84  		},
    85  		{
    86  			name:   "gv is applied, empty group",
    87  			result: &v1.PodList{},
    88  			target: WatchListResult{gv: schema.GroupVersion{Version: "v"}},
    89  			expectedResult: &v1.PodList{
    90  				TypeMeta: metav1.TypeMeta{Kind: "PodList", APIVersion: "v"},
    91  				Items:    []v1.Pod{},
    92  			},
    93  		},
    94  		{
    95  			name:   "rv is applied",
    96  			result: &v1.PodList{},
    97  			target: WatchListResult{initialEventsEndBookmarkRV: "100"},
    98  			expectedResult: &v1.PodList{
    99  				TypeMeta: metav1.TypeMeta{Kind: "PodList"},
   100  				ListMeta: metav1.ListMeta{ResourceVersion: "100"},
   101  				Items:    []v1.Pod{},
   102  			},
   103  		},
   104  		{
   105  			name:   "items are applied",
   106  			result: &v1.PodList{},
   107  			target: WatchListResult{items: []runtime.Object{makePod(1), makePod(2)}},
   108  			expectedResult: &v1.PodList{
   109  				TypeMeta: metav1.TypeMeta{Kind: "PodList"},
   110  				Items:    []v1.Pod{*makePod(1), *makePod(2)},
   111  			},
   112  		},
   113  		{
   114  			name:        "type mismatch",
   115  			result:      &v1.PodList{},
   116  			target:      WatchListResult{items: []runtime.Object{makeNamespace("1")}},
   117  			expectedErr: fmt.Errorf("received object type = v1.Namespace at index = 0, doesn't match the list item type = v1.Pod"),
   118  		},
   119  	}
   120  	for _, scenario := range scenarios {
   121  		t.Run(scenario.name, func(t *testing.T) {
   122  			err := scenario.target.Into(scenario.result)
   123  			if scenario.expectedErr != nil && err == nil {
   124  				t.Fatalf("expected an error = %v, got nil", scenario.expectedErr)
   125  			}
   126  			if scenario.expectedErr == nil && err != nil {
   127  				t.Fatalf("didn't expect an error, got =  %v", err)
   128  			}
   129  			if err != nil {
   130  				if scenario.expectedErr.Error() != err.Error() {
   131  					t.Fatalf("unexpected err = %v, expected = %v", err, scenario.expectedErr)
   132  				}
   133  				return
   134  			}
   135  			if !apiequality.Semantic.DeepEqual(scenario.expectedResult, scenario.result) {
   136  				t.Errorf("diff: %v", cmp.Diff(scenario.expectedResult, scenario.result))
   137  			}
   138  		})
   139  	}
   140  }
   141  
   142  func TestWatchListSuccess(t *testing.T) {
   143  	scenarios := []struct {
   144  		name           string
   145  		gv             schema.GroupVersion
   146  		watchEvents    []watch.Event
   147  		expectedResult *v1.PodList
   148  	}{
   149  		{
   150  			name: "happy path",
   151  			// Note that the APIVersion for the core API group is "v1" (not "core/v1").
   152  			// We fake "core/v1" here to test if the Group part is properly
   153  			// recognized and set on the resulting object.
   154  			gv: schema.GroupVersion{Group: "core", Version: "v1"},
   155  			watchEvents: []watch.Event{
   156  				{Type: watch.Added, Object: makePod(1)},
   157  				{Type: watch.Added, Object: makePod(2)},
   158  				{Type: watch.Bookmark, Object: makeBookmarkEvent(5)},
   159  			},
   160  			expectedResult: &v1.PodList{
   161  				TypeMeta: metav1.TypeMeta{
   162  					APIVersion: "core/v1",
   163  					Kind:       "PodList",
   164  				},
   165  				ListMeta: metav1.ListMeta{ResourceVersion: "5"},
   166  				Items:    []v1.Pod{*makePod(1), *makePod(2)},
   167  			},
   168  		},
   169  		{
   170  			name: "APIVersion with only version provided is properly set",
   171  			gv:   schema.GroupVersion{Version: "v1"},
   172  			watchEvents: []watch.Event{
   173  				{Type: watch.Added, Object: makePod(1)},
   174  				{Type: watch.Bookmark, Object: makeBookmarkEvent(5)},
   175  			},
   176  			expectedResult: &v1.PodList{
   177  				TypeMeta: metav1.TypeMeta{
   178  					APIVersion: "v1",
   179  					Kind:       "PodList",
   180  				},
   181  				ListMeta: metav1.ListMeta{ResourceVersion: "5"},
   182  				Items:    []v1.Pod{*makePod(1)},
   183  			},
   184  		},
   185  		{
   186  			name: "only the bookmark",
   187  			gv:   schema.GroupVersion{Version: "v1"},
   188  			watchEvents: []watch.Event{
   189  				{Type: watch.Bookmark, Object: makeBookmarkEvent(5)},
   190  			},
   191  			expectedResult: &v1.PodList{
   192  				TypeMeta: metav1.TypeMeta{
   193  					APIVersion: "v1",
   194  					Kind:       "PodList",
   195  				},
   196  				ListMeta: metav1.ListMeta{ResourceVersion: "5"},
   197  				Items:    []v1.Pod{},
   198  			},
   199  		},
   200  	}
   201  	for _, scenario := range scenarios {
   202  		t.Run(scenario.name, func(t *testing.T) {
   203  			ctx := context.Background()
   204  			fakeWatcher := watch.NewFake()
   205  			target := &Request{
   206  				c: &RESTClient{
   207  					content: ClientContentConfig{
   208  						GroupVersion: scenario.gv,
   209  					},
   210  				},
   211  			}
   212  
   213  			go func(watchEvents []watch.Event) {
   214  				for _, watchEvent := range watchEvents {
   215  					fakeWatcher.Action(watchEvent.Type, watchEvent.Object)
   216  				}
   217  			}(scenario.watchEvents)
   218  
   219  			res := target.handleWatchList(ctx, fakeWatcher)
   220  			if res.err != nil {
   221  				t.Fatal(res.err)
   222  			}
   223  
   224  			result := &v1.PodList{}
   225  			if err := res.Into(result); err != nil {
   226  				t.Fatal(err)
   227  			}
   228  			if !apiequality.Semantic.DeepEqual(scenario.expectedResult, result) {
   229  				t.Errorf("diff: %v", cmp.Diff(scenario.expectedResult, result))
   230  			}
   231  			if !fakeWatcher.IsStopped() {
   232  				t.Fatalf("the watcher wasn't stopped")
   233  			}
   234  		})
   235  	}
   236  }
   237  
   238  func TestWatchListFailure(t *testing.T) {
   239  	scenarios := []struct {
   240  		name        string
   241  		ctx         context.Context
   242  		watcher     *watch.FakeWatcher
   243  		watchEvents []watch.Event
   244  
   245  		expectedError error
   246  	}{
   247  		{
   248  			name: "request stop",
   249  			ctx: func() context.Context {
   250  				ctx, ctxCancel := context.WithCancel(context.TODO())
   251  				ctxCancel()
   252  				return ctx
   253  			}(),
   254  			watcher:       watch.NewFake(),
   255  			expectedError: fmt.Errorf("context canceled"),
   256  		},
   257  		{
   258  			name: "stop watcher",
   259  			ctx:  context.TODO(),
   260  			watcher: func() *watch.FakeWatcher {
   261  				w := watch.NewFake()
   262  				w.Stop()
   263  				return w
   264  			}(),
   265  			expectedError: fmt.Errorf("unexpected watch close"),
   266  		},
   267  		{
   268  			name:          "stop on watch.Error",
   269  			ctx:           context.TODO(),
   270  			watcher:       watch.NewFake(),
   271  			watchEvents:   []watch.Event{{Type: watch.Error, Object: &apierrors.NewInternalError(fmt.Errorf("dummy errror")).ErrStatus}},
   272  			expectedError: fmt.Errorf("Internal error occurred: dummy errror"),
   273  		},
   274  		{
   275  			name:          "incorrect watch type (Deleted)",
   276  			ctx:           context.TODO(),
   277  			watcher:       watch.NewFake(),
   278  			watchEvents:   []watch.Event{{Type: watch.Deleted, Object: makePod(1)}},
   279  			expectedError: fmt.Errorf("unexpected watch event .*, expected to only receive watch.Added and watch.Bookmark events"),
   280  		},
   281  		{
   282  			name:          "incorrect watch type (Modified)",
   283  			ctx:           context.TODO(),
   284  			watcher:       watch.NewFake(),
   285  			watchEvents:   []watch.Event{{Type: watch.Modified, Object: makePod(1)}},
   286  			expectedError: fmt.Errorf("unexpected watch event .*, expected to only receive watch.Added and watch.Bookmark events"),
   287  		},
   288  		{
   289  			name:          "unordered input returns an error",
   290  			ctx:           context.TODO(),
   291  			watcher:       watch.NewFake(),
   292  			watchEvents:   []watch.Event{{Type: watch.Added, Object: makePod(3)}, {Type: watch.Added, Object: makePod(1)}},
   293  			expectedError: fmt.Errorf("cannot add the obj .* with the key = ns/pod-1, as it violates the ordering guarantees provided by the watchlist feature in beta phase, lastInsertedKey was = ns/pod-3"),
   294  		},
   295  	}
   296  
   297  	for _, scenario := range scenarios {
   298  		t.Run(scenario.name, func(t *testing.T) {
   299  			target := &Request{}
   300  			go func(w *watch.FakeWatcher, watchEvents []watch.Event) {
   301  				for _, event := range watchEvents {
   302  					w.Action(event.Type, event.Object)
   303  				}
   304  			}(scenario.watcher, scenario.watchEvents)
   305  
   306  			res := target.handleWatchList(scenario.ctx, scenario.watcher)
   307  			resErr := res.Into(nil)
   308  			if resErr == nil {
   309  				t.Fatal("expected to get an error, got nil")
   310  			}
   311  			matched, err := regexp.MatchString(scenario.expectedError.Error(), resErr.Error())
   312  			if err != nil {
   313  				t.Fatal(err)
   314  			}
   315  			if !matched {
   316  				t.Fatalf("unexpected err = %v, expected = %v", resErr, scenario.expectedError)
   317  			}
   318  			if !scenario.watcher.IsStopped() {
   319  				t.Fatalf("the watcher wasn't stopped")
   320  			}
   321  		})
   322  	}
   323  }
   324  
   325  func TestWatchListWhenFeatureGateDisabled(t *testing.T) {
   326  	clientfeaturestesting.SetFeatureDuringTest(t, clientfeatures.WatchListClient, false)
   327  	expectedError := fmt.Errorf("%q feature gate is not enabled", clientfeatures.WatchListClient)
   328  	target := &Request{}
   329  
   330  	res := target.WatchList(context.TODO())
   331  
   332  	resErr := res.Into(nil)
   333  	if resErr == nil {
   334  		t.Fatal("expected to get an error, got nil")
   335  	}
   336  	if resErr.Error() != expectedError.Error() {
   337  		t.Fatalf("unexpected error: %v, expected: %v", resErr, expectedError)
   338  	}
   339  }
   340  
   341  func makePod(rv uint64) *v1.Pod {
   342  	return &v1.Pod{
   343  		ObjectMeta: metav1.ObjectMeta{
   344  			Name:            fmt.Sprintf("pod-%d", rv),
   345  			Namespace:       "ns",
   346  			ResourceVersion: fmt.Sprintf("%d", rv),
   347  			Annotations:     map[string]string{},
   348  		},
   349  	}
   350  }
   351  
   352  func makeNamespace(name string) *v1.Namespace {
   353  	return &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: name}}
   354  }
   355  
   356  func makeBookmarkEvent(rv uint64) *v1.Pod {
   357  	return &v1.Pod{
   358  		ObjectMeta: metav1.ObjectMeta{
   359  			ResourceVersion: fmt.Sprintf("%d", rv),
   360  			Annotations:     map[string]string{metav1.InitialEventsAnnotationKey: "true"},
   361  		},
   362  	}
   363  }
   364  
   365  type fakeObj struct {
   366  }
   367  
   368  func (f fakeObj) GetObjectKind() schema.ObjectKind {
   369  	return schema.EmptyObjectKind
   370  }
   371  
   372  func (f fakeObj) DeepCopyObject() runtime.Object {
   373  	return fakeObj{}
   374  }