knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/configmap/informer/informed_watcher_test.go (about)

     1  /*
     2  Copyright 2018 The Knative 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 informer
    18  
    19  import (
    20  	"context"
    21  	"sync"
    22  	"testing"
    23  
    24  	corev1 "k8s.io/api/core/v1"
    25  	"k8s.io/apimachinery/pkg/api/equality"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/labels"
    28  	"k8s.io/apimachinery/pkg/runtime"
    29  	"k8s.io/apimachinery/pkg/selection"
    30  	fakekubeclientset "k8s.io/client-go/kubernetes/fake"
    31  	"k8s.io/client-go/tools/cache"
    32  )
    33  
    34  type counter struct {
    35  	name string
    36  	mu   sync.RWMutex
    37  	cfg  []*corev1.ConfigMap
    38  	wg   *sync.WaitGroup
    39  }
    40  
    41  func (c *counter) callback(cm *corev1.ConfigMap) {
    42  	c.mu.Lock()
    43  	defer c.mu.Unlock()
    44  
    45  	c.cfg = append(c.cfg, cm)
    46  	if c.wg != nil {
    47  		c.wg.Done()
    48  	}
    49  }
    50  
    51  func (c *counter) count() int {
    52  	c.mu.RLock()
    53  	defer c.mu.RUnlock()
    54  	return len(c.cfg)
    55  }
    56  
    57  func TestInformedWatcher(t *testing.T) {
    58  	fooCM := &corev1.ConfigMap{
    59  		ObjectMeta: metav1.ObjectMeta{
    60  			Namespace: "default",
    61  			Name:      "foo",
    62  		},
    63  		Data: map[string]string{"key": "val"},
    64  	}
    65  	barCM := &corev1.ConfigMap{
    66  		ObjectMeta: metav1.ObjectMeta{
    67  			Namespace: "default",
    68  			Name:      "bar",
    69  		},
    70  		Data: map[string]string{"key2": "val3"},
    71  	}
    72  	kc := fakekubeclientset.NewSimpleClientset(fooCM, barCM)
    73  	cmw := NewInformedWatcher(kc, "default")
    74  
    75  	foo1 := &counter{name: "foo1"}
    76  	foo2 := &counter{name: "foo2"}
    77  	bar := &counter{name: "bar"}
    78  	cmw.Watch("foo", foo1.callback)
    79  	cmw.Watch("foo", foo2.callback)
    80  	cmw.Watch("bar", bar.callback)
    81  
    82  	stopCh := make(chan struct{})
    83  	defer close(stopCh)
    84  	if err := cmw.Start(stopCh); err != nil {
    85  		t.Fatal("cm.Start() =", err)
    86  	}
    87  
    88  	// When Start returns the callbacks should have been called with the
    89  	// version of the objects that is available.
    90  	for _, obj := range []*counter{foo1, foo2, bar} {
    91  		if got, want := obj.count(), 1; got != want {
    92  			t.Errorf("%v.count = %d, want %d", obj.name, got, want)
    93  		}
    94  	}
    95  
    96  	// After a "foo" event, the "foo" watchers should have 2,
    97  	// and the "bar" watchers should still have 1
    98  	cmw.updateConfigMapEvent(nil, fooCM)
    99  	for _, obj := range []*counter{foo1, foo2} {
   100  		if got, want := obj.count(), 2; got != want {
   101  			t.Errorf("%v.count = %d, want %d", obj.name, got, want)
   102  		}
   103  	}
   104  
   105  	for _, obj := range []*counter{bar} {
   106  		if got, want := obj.count(), 1; got != want {
   107  			t.Errorf("%v.count = %d, want %d", obj.name, got, want)
   108  		}
   109  	}
   110  
   111  	// After a "foo" and "bar" event, the "foo" watchers should have 3,
   112  	// and the "bar" watchers should still have 2
   113  	cmw.updateConfigMapEvent(nil, fooCM)
   114  	cmw.updateConfigMapEvent(nil, barCM)
   115  	for _, obj := range []*counter{foo1, foo2} {
   116  		if got, want := obj.count(), 3; got != want {
   117  			t.Errorf("%v.count = %d, want %d", obj.name, got, want)
   118  		}
   119  	}
   120  	for _, obj := range []*counter{bar} {
   121  		if got, want := obj.count(), 2; got != want {
   122  			t.Errorf("%v.count = %d, want %d", obj.name, got, want)
   123  		}
   124  	}
   125  
   126  	// After a "bar" event, all watchers should have 3
   127  	nbarCM := barCM.DeepCopy()
   128  	nbarCM.Data["wow"] = "now!"
   129  	cmw.updateConfigMapEvent(barCM, nbarCM)
   130  	for _, obj := range []*counter{foo1, foo2, bar} {
   131  		if got, want := obj.count(), 3; got != want {
   132  			t.Errorf("%v.count = %d, want %d", obj.name, got, want)
   133  		}
   134  	}
   135  
   136  	// After an idempotent event no changes should be recorded.
   137  	cmw.updateConfigMapEvent(barCM, barCM)
   138  	cmw.updateConfigMapEvent(fooCM, fooCM)
   139  	for _, obj := range []*counter{foo1, foo2, bar} {
   140  		if got, want := obj.count(), 3; got != want {
   141  			t.Errorf("%v.count = %d, want %d", obj.name, got, want)
   142  		}
   143  	}
   144  
   145  	// After an unwatched ConfigMap update, no change.
   146  
   147  	cmw.updateConfigMapEvent(nil, &corev1.ConfigMap{
   148  		ObjectMeta: metav1.ObjectMeta{
   149  			Namespace: "default",
   150  			Name:      "not-watched",
   151  		},
   152  	})
   153  	for _, obj := range []*counter{foo1, foo2, bar} {
   154  		if got, want := obj.count(), 3; got != want {
   155  			t.Errorf("%v.count = %d, want %d", obj.name, got, want)
   156  		}
   157  	}
   158  
   159  	// After a change in an unrelated namespace, no change.
   160  	cmw.updateConfigMapEvent(nil, &corev1.ConfigMap{
   161  		ObjectMeta: metav1.ObjectMeta{
   162  			Namespace: "not-default",
   163  			Name:      "foo",
   164  		},
   165  	})
   166  	for _, obj := range []*counter{foo1, foo2, bar} {
   167  		if got, want := obj.count(), 3; got != want {
   168  			t.Errorf("%v.count = %d, want %d", obj.name, got, want)
   169  		}
   170  	}
   171  }
   172  
   173  func TestFilterConfigByLabelExists(t *testing.T) {
   174  	testCases := map[string]struct {
   175  		input        string
   176  		expectOutStr string
   177  		expectErr    bool
   178  	}{
   179  		"Valid input": {
   180  			input:        "test/label",
   181  			expectOutStr: "test/label",
   182  		},
   183  		"Invalid input": {
   184  			input:     "invalid,",
   185  			expectErr: true,
   186  		},
   187  		"Empty input": {
   188  			input:     "",
   189  			expectErr: true,
   190  		},
   191  	}
   192  
   193  	for name, tc := range testCases {
   194  		t.Run(name, func(t *testing.T) {
   195  			req, err := FilterConfigByLabelExists(tc.input)
   196  
   197  			if gotError := err != nil; gotError != tc.expectErr {
   198  				t.Fatalf("[error] expected/got: %t/%t (msg: %v)", tc.expectErr, gotError, err)
   199  			}
   200  			if gotReq, wantReq := req != nil, tc.expectOutStr != ""; gotReq != wantReq {
   201  				t.Fatalf("[output] expected/got: %t/%t (req: %q)", wantReq, gotReq, req)
   202  			}
   203  
   204  			if req != nil && req.String() != tc.expectOutStr {
   205  				t.Errorf("Expected %q, got %q", tc.expectOutStr, req.String())
   206  			}
   207  		})
   208  	}
   209  }
   210  
   211  func TestWatchMissingFailsOnStart(t *testing.T) {
   212  	const (
   213  		labelKey = "test/label"
   214  		labelVal = "test"
   215  	)
   216  
   217  	cmWithLabel := &corev1.ConfigMap{
   218  		ObjectMeta: metav1.ObjectMeta{
   219  			Namespace: "default",
   220  			Name:      "with-label",
   221  			Labels:    map[string]string{labelKey: labelVal},
   222  		},
   223  	}
   224  	cmWithoutLabel := &corev1.ConfigMap{
   225  		ObjectMeta: metav1.ObjectMeta{
   226  			Namespace: "default",
   227  			Name:      "without-label",
   228  			Labels:    map[string]string{},
   229  		},
   230  	}
   231  
   232  	testCases := map[string]struct {
   233  		initialObj []runtime.Object
   234  		watchNames []string
   235  		expectErr  string
   236  		labelReq   string
   237  	}{
   238  		"ConfigMap does not exist": {
   239  			initialObj: nil,
   240  			watchNames: []string{"foo"},
   241  			expectErr:  `configmap "foo" not found`,
   242  		},
   243  		"ConfigMap is missing required label": {
   244  			initialObj: []runtime.Object{cmWithLabel, cmWithoutLabel},
   245  			watchNames: []string{"with-label", "without-label"},
   246  			expectErr:  `configmap "without-label" not found`,
   247  			labelReq:   labelKey,
   248  		},
   249  	}
   250  
   251  	for name, tc := range testCases {
   252  		t.Run(name, func(t *testing.T) {
   253  			var cmw *InformedWatcher
   254  
   255  			kc := fakekubeclientset.NewSimpleClientset(tc.initialObj...)
   256  
   257  			if tc.labelReq != "" {
   258  				req, _ := labels.NewRequirement(labelKey, selection.Equals, []string{labelVal})
   259  				cmw = NewInformedWatcher(kc, "default", *req)
   260  			} else {
   261  				cmw = NewInformedWatcher(kc, "default")
   262  			}
   263  
   264  			for _, cmName := range tc.watchNames {
   265  				cmw.Watch(cmName)
   266  			}
   267  
   268  			stopCh := make(chan struct{})
   269  			defer close(stopCh)
   270  			err := cmw.Start(stopCh)
   271  
   272  			switch {
   273  			case err == nil:
   274  				t.Fatal("Failed to start InformedWatcher =", err)
   275  			case err.Error() != tc.expectErr:
   276  				t.Fatal("Unexpected error =", err)
   277  			}
   278  		})
   279  	}
   280  }
   281  
   282  func TestWatchMissingOKWithDefaultOnStart(t *testing.T) {
   283  	fooCM := &corev1.ConfigMap{
   284  		ObjectMeta: metav1.ObjectMeta{
   285  			Namespace: "default",
   286  			Name:      "foo",
   287  		},
   288  	}
   289  
   290  	kc := fakekubeclientset.NewSimpleClientset()
   291  	cmw := NewInformedWatcher(kc, "default")
   292  
   293  	foo1 := &counter{name: "foo1"}
   294  	cmw.WatchWithDefault(*fooCM, foo1.callback)
   295  
   296  	stopCh := make(chan struct{})
   297  	defer close(stopCh)
   298  
   299  	// This shouldn't error because we don't have a ConfigMap named "foo", but we do have a default.
   300  	err := cmw.Start(stopCh)
   301  	if err != nil {
   302  		t.Fatal("cm.Start() failed =", err)
   303  	}
   304  
   305  	if foo1.count() != 1 {
   306  		t.Errorf("foo1.count = %d, want 1", foo1.count())
   307  	}
   308  }
   309  
   310  func TestErrorOnMultipleStarts(t *testing.T) {
   311  	fooCM := &corev1.ConfigMap{
   312  		ObjectMeta: metav1.ObjectMeta{
   313  			Namespace: "default",
   314  			Name:      "foo",
   315  		},
   316  	}
   317  	kc := fakekubeclientset.NewSimpleClientset(fooCM)
   318  	cmw := NewInformedWatcher(kc, "default")
   319  
   320  	foo1 := &counter{name: "foo1"}
   321  	cmw.Watch("foo", foo1.callback)
   322  
   323  	stopCh := make(chan struct{})
   324  	defer close(stopCh)
   325  
   326  	// This should succeed because the watched resource exists.
   327  	if err := cmw.Start(stopCh); err != nil {
   328  		t.Fatal("cm.Start() =", err)
   329  	}
   330  
   331  	// This should error because we already called Start()
   332  	if err := cmw.Start(stopCh); err == nil {
   333  		t.Fatal("cm.Start() succeeded, wanted error")
   334  	}
   335  }
   336  
   337  func TestDefaultObserved(t *testing.T) {
   338  	defaultFooCM := &corev1.ConfigMap{
   339  		ObjectMeta: metav1.ObjectMeta{
   340  			Namespace: "default",
   341  			Name:      "foo",
   342  		},
   343  		Data: map[string]string{
   344  			"default": "from code",
   345  		},
   346  	}
   347  	fooCM := &corev1.ConfigMap{
   348  		ObjectMeta: metav1.ObjectMeta{
   349  			Namespace: "default",
   350  			Name:      "foo",
   351  		},
   352  		Data: map[string]string{
   353  			"from": "k8s",
   354  		},
   355  	}
   356  
   357  	kc := fakekubeclientset.NewSimpleClientset(fooCM)
   358  	cmw := NewInformedWatcher(kc, "default")
   359  
   360  	foo1 := &counter{name: "foo1"}
   361  	cmw.WatchWithDefault(*defaultFooCM, foo1.callback)
   362  
   363  	stopCh := make(chan struct{})
   364  	defer close(stopCh)
   365  	err := cmw.Start(stopCh)
   366  	if err != nil {
   367  		t.Fatal("cm.Start() =", err)
   368  	}
   369  	// We expect:
   370  	// 1. The default to be seen once during startup.
   371  	// 2. The real K8s version during the initial pass.
   372  	expected := []*corev1.ConfigMap{defaultFooCM, fooCM}
   373  	if got, want := foo1.count(), len(expected); got != want {
   374  		t.Fatalf("foo1.count = %d, want %d", got, want)
   375  	}
   376  	for i, cfg := range expected {
   377  		if got, want := foo1.cfg[i].Data, cfg.Data; !equality.Semantic.DeepEqual(want, got) {
   378  			t.Errorf("%d config seen should have been '%v', actually '%v'", i, want, got)
   379  		}
   380  	}
   381  }
   382  
   383  func TestDefaultConfigMapDeleted(t *testing.T) {
   384  	defaultFooCM := &corev1.ConfigMap{
   385  		ObjectMeta: metav1.ObjectMeta{
   386  			Namespace: "default",
   387  			Name:      "foo",
   388  		},
   389  		Data: map[string]string{
   390  			"default": "from code",
   391  		},
   392  	}
   393  	fooCM := &corev1.ConfigMap{
   394  		ObjectMeta: metav1.ObjectMeta{
   395  			Namespace: "default",
   396  			Name:      "foo",
   397  		},
   398  		Data: map[string]string{
   399  			"from": "k8s",
   400  		},
   401  	}
   402  
   403  	kc := fakekubeclientset.NewSimpleClientset(fooCM)
   404  	cmw := NewInformedWatcher(kc, "default")
   405  
   406  	foo1 := &counter{
   407  		name: "foo1",
   408  		wg:   &sync.WaitGroup{},
   409  	}
   410  	foo1.wg.Add(3) // We expect 3 invocations.
   411  	cmw.WatchWithDefault(*defaultFooCM, foo1.callback)
   412  
   413  	stopCh := make(chan struct{})
   414  	defer close(stopCh)
   415  
   416  	if err := cmw.Start(stopCh); err != nil {
   417  		t.Fatal("cm.Start() =", err)
   418  	}
   419  
   420  	// Delete the real ConfigMap in K8s, which should cause the default to be processed again.
   421  	// Because this happens asynchronously via a watcher, use a sync.WaitGroup to wait until it has
   422  	// occurred.
   423  	if err := kc.CoreV1().ConfigMaps(fooCM.Namespace).Delete(
   424  		context.Background(), fooCM.Name, metav1.DeleteOptions{}); err != nil {
   425  		t.Fatal("Error deleting fooCM:", err)
   426  	}
   427  	foo1.wg.Wait()
   428  
   429  	// We expect:
   430  	// 1. The default to be seen once during startup.
   431  	// 2. The real K8s version during the initial pass.
   432  	// 3. The default again, when the real K8s version is deleted.
   433  	expected := []*corev1.ConfigMap{defaultFooCM, fooCM, defaultFooCM}
   434  	if got, want := foo1.count(), len(expected); got != want {
   435  		t.Fatalf("foo1.count = %v, want %d", got, want)
   436  	}
   437  	for i, cfg := range expected {
   438  		if got, want := foo1.cfg[i].Data, cfg.Data; !equality.Semantic.DeepEqual(want, got) {
   439  			t.Errorf("%d config seen should have been '%v', actually '%v'", i, want, got)
   440  		}
   441  	}
   442  }
   443  
   444  func TestWatchWithDefaultAfterStart(t *testing.T) {
   445  	defaultFooCM := &corev1.ConfigMap{
   446  		ObjectMeta: metav1.ObjectMeta{
   447  			Namespace: "default",
   448  			Name:      "foo",
   449  		},
   450  		Data: map[string]string{
   451  			"default": "from code",
   452  		},
   453  	}
   454  	fooCM := &corev1.ConfigMap{
   455  		ObjectMeta: metav1.ObjectMeta{
   456  			Namespace: "default",
   457  			Name:      "foo",
   458  		},
   459  		Data: map[string]string{
   460  			"from": "k8s",
   461  		},
   462  	}
   463  
   464  	kc := fakekubeclientset.NewSimpleClientset(fooCM)
   465  	cmw := NewInformedWatcher(kc, "default")
   466  
   467  	stopCh := make(chan struct{})
   468  	defer close(stopCh)
   469  	// Start before adding the WatchWithDefault.
   470  	if err := cmw.Start(stopCh); err != nil {
   471  		t.Fatal("cm.Start() =", err)
   472  	}
   473  
   474  	foo1 := &counter{name: "foo1"}
   475  
   476  	// Add the WatchWithDefault. This should panic because the InformedWatcher has already started.
   477  	func() {
   478  		defer func() {
   479  			recover()
   480  		}()
   481  		cmw.WatchWithDefault(*defaultFooCM, foo1.callback)
   482  		t.Fatal("WatchWithDefault should have panicked")
   483  	}()
   484  
   485  	// We expect nothing.
   486  	var expected []*corev1.ConfigMap
   487  	if got, want := foo1.count(), len(expected); got != want {
   488  		t.Fatalf("foo1.count = %v, want %d", got, want)
   489  	}
   490  }
   491  
   492  func TestDeleteConfigMapEventWithDeletedFinalStateUnknown(t *testing.T) {
   493  	defaultFooCM := &corev1.ConfigMap{
   494  		ObjectMeta: metav1.ObjectMeta{
   495  			Namespace: "default",
   496  			Name:      "foo",
   497  		},
   498  		Data: map[string]string{
   499  			"default": "from code",
   500  		},
   501  	}
   502  	fooCM := &corev1.ConfigMap{
   503  		ObjectMeta: metav1.ObjectMeta{
   504  			Namespace: "default",
   505  			Name:      "foo",
   506  		},
   507  		Data: map[string]string{
   508  			"from": "k8s",
   509  		},
   510  	}
   511  
   512  	kc := fakekubeclientset.NewSimpleClientset(fooCM)
   513  	cmw := NewInformedWatcher(kc, "default")
   514  
   515  	foo1 := &counter{name: "foo1"}
   516  	cmw.WatchWithDefault(*defaultFooCM, foo1.callback)
   517  
   518  	stopCh := make(chan struct{})
   519  	defer close(stopCh)
   520  	if err := cmw.Start(stopCh); err != nil {
   521  		t.Fatal("cm.Start() =", err)
   522  	}
   523  
   524  	// Test that deleteConfigMapEvent does NOT panic when given cache.DeletedFinalStateUnknown with a ConfigMap
   525  	// This simulates the scenario where the informer receives a DeletedFinalStateUnknown tombstone.
   526  	tombstone := cache.DeletedFinalStateUnknown{
   527  		Key: "default/foo",
   528  		Obj: fooCM,
   529  	}
   530  
   531  	// This should not panic and should trigger the default ConfigMap
   532  	cmw.deleteConfigMapEvent(tombstone)
   533  
   534  	// We expect:
   535  	// 1. The default to be seen once during startup.
   536  	// 2. The real K8s version during the initial pass.
   537  	// 3. The default again, when the real K8s version is deleted via tombstone.
   538  	expected := []*corev1.ConfigMap{defaultFooCM, fooCM, defaultFooCM}
   539  	if got, want := foo1.count(), len(expected); got != want {
   540  		t.Fatalf("foo1.count = %v, want %d", got, want)
   541  	}
   542  	for i, cfg := range expected {
   543  		if got, want := foo1.cfg[i].Data, cfg.Data; !equality.Semantic.DeepEqual(want, got) {
   544  			t.Errorf("%d config seen should have been '%v', actually '%v'", i, want, got)
   545  		}
   546  	}
   547  }
   548  
   549  func TestDeleteConfigMapEventWithDeletedFinalStateUnknownInvalidObject(t *testing.T) {
   550  	defaultFooCM := &corev1.ConfigMap{
   551  		ObjectMeta: metav1.ObjectMeta{
   552  			Namespace: "default",
   553  			Name:      "foo",
   554  		},
   555  		Data: map[string]string{
   556  			"default": "from code",
   557  		},
   558  	}
   559  
   560  	kc := fakekubeclientset.NewSimpleClientset()
   561  	cmw := NewInformedWatcher(kc, "default")
   562  
   563  	foo1 := &counter{name: "foo1"}
   564  	cmw.WatchWithDefault(*defaultFooCM, foo1.callback)
   565  
   566  	stopCh := make(chan struct{})
   567  	defer close(stopCh)
   568  	if err := cmw.Start(stopCh); err != nil {
   569  		t.Fatal("cm.Start() =", err)
   570  	}
   571  
   572  	initialCount := foo1.count()
   573  
   574  	// Test that deleteConfigMapEvent does NOT panic when given cache.DeletedFinalStateUnknown with an invalid object
   575  	// This simulates the scenario where the tombstone contains a non-ConfigMap object.
   576  	tombstone := cache.DeletedFinalStateUnknown{
   577  		Key: "default/foo",
   578  		Obj: "not-a-configmap", // Invalid object type
   579  	}
   580  
   581  	// This should not panic and should gracefully return without changing the count
   582  	cmw.deleteConfigMapEvent(tombstone)
   583  
   584  	// The count should remain unchanged since the object is invalid
   585  	if got, want := foo1.count(), initialCount; got != want {
   586  		t.Fatalf("foo1.count = %v, want %d (should not change)", got, want)
   587  	}
   588  }
   589  
   590  func TestDeleteConfigMapEventWithInvalidObject(t *testing.T) {
   591  	defaultFooCM := &corev1.ConfigMap{
   592  		ObjectMeta: metav1.ObjectMeta{
   593  			Namespace: "default",
   594  			Name:      "foo",
   595  		},
   596  		Data: map[string]string{
   597  			"default": "from code",
   598  		},
   599  	}
   600  
   601  	kc := fakekubeclientset.NewSimpleClientset()
   602  	cmw := NewInformedWatcher(kc, "default")
   603  
   604  	foo1 := &counter{name: "foo1"}
   605  	cmw.WatchWithDefault(*defaultFooCM, foo1.callback)
   606  
   607  	stopCh := make(chan struct{})
   608  	defer close(stopCh)
   609  	if err := cmw.Start(stopCh); err != nil {
   610  		t.Fatal("cm.Start() =", err)
   611  	}
   612  
   613  	initialCount := foo1.count()
   614  
   615  	// Test that deleteConfigMapEvent does NOT panic when given a completely invalid object
   616  	// This simulates the scenario where the informer receives an unexpected object type.
   617  	invalidObj := "not-a-configmap"
   618  
   619  	// This should not panic and should gracefully return without changing the count
   620  	cmw.deleteConfigMapEvent(invalidObj)
   621  
   622  	// The count should remain unchanged since the object is invalid
   623  	if got, want := foo1.count(), initialCount; got != want {
   624  		t.Fatalf("foo1.count = %v, want %d (should not change)", got, want)
   625  	}
   626  }