...

Source file src/sigs.k8s.io/controller-runtime/pkg/cache/defaulting_test.go

Documentation: sigs.k8s.io/controller-runtime/pkg/cache

     1  /*
     2  Copyright 2023 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 cache
    18  
    19  import (
    20  	"reflect"
    21  	"testing"
    22  	"time"
    23  
    24  	"github.com/google/go-cmp/cmp"
    25  	"github.com/google/go-cmp/cmp/cmpopts"
    26  	fuzz "github.com/google/gofuzz"
    27  	corev1 "k8s.io/api/core/v1"
    28  	"k8s.io/apimachinery/pkg/api/meta"
    29  	"k8s.io/apimachinery/pkg/fields"
    30  	"k8s.io/apimachinery/pkg/labels"
    31  	"k8s.io/apimachinery/pkg/runtime/schema"
    32  	"k8s.io/client-go/rest"
    33  	"k8s.io/client-go/tools/cache"
    34  	"k8s.io/utils/ptr"
    35  	"sigs.k8s.io/controller-runtime/pkg/client"
    36  )
    37  
    38  func TestDefaultOpts(t *testing.T) {
    39  	t.Parallel()
    40  
    41  	pod := &corev1.Pod{}
    42  
    43  	compare := func(a, b any) string {
    44  		return cmp.Diff(a, b,
    45  			cmpopts.IgnoreUnexported(Options{}),
    46  			cmpopts.IgnoreFields(Options{}, "HTTPClient", "Scheme", "Mapper", "SyncPeriod"),
    47  			cmp.Comparer(func(a, b fields.Selector) bool {
    48  				if (a != nil) != (b != nil) {
    49  					return false
    50  				}
    51  				if a == nil {
    52  					return true
    53  				}
    54  				return a.String() == b.String()
    55  			}),
    56  		)
    57  	}
    58  	testCases := []struct {
    59  		name string
    60  		in   Options
    61  
    62  		verification func(Options) string
    63  	}{
    64  		{
    65  			name: "ByObject.Namespaces gets defaulted from ByObject",
    66  			in: Options{
    67  				ByObject: map[client.Object]ByObject{pod: {
    68  					Namespaces: map[string]Config{
    69  						"default": {},
    70  					},
    71  					Label: labels.SelectorFromSet(map[string]string{"from": "by-object"}),
    72  				}},
    73  				DefaultNamespaces: map[string]Config{
    74  					"default": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "default-namespaces"})},
    75  				},
    76  				DefaultLabelSelector: labels.SelectorFromSet(map[string]string{"from": "default-label-selector"}),
    77  			},
    78  
    79  			verification: func(o Options) string {
    80  				expected := map[string]Config{
    81  					"default": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "by-object"})},
    82  				}
    83  				return cmp.Diff(expected, o.ByObject[pod].Namespaces)
    84  			},
    85  		},
    86  		{
    87  			name: "ByObject.Namespaces gets defaulted from DefaultNamespaces",
    88  			in: Options{
    89  				ByObject: map[client.Object]ByObject{pod: {
    90  					Namespaces: map[string]Config{
    91  						"default": {},
    92  					},
    93  				}},
    94  				DefaultNamespaces: map[string]Config{
    95  					"default": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "default-namespaces"})},
    96  				},
    97  				DefaultLabelSelector: labels.SelectorFromSet(map[string]string{"from": "default-label-selector"}),
    98  			},
    99  
   100  			verification: func(o Options) string {
   101  				expected := map[string]Config{
   102  					"default": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "default-namespaces"})},
   103  				}
   104  				return cmp.Diff(expected, o.ByObject[pod].Namespaces)
   105  			},
   106  		},
   107  		{
   108  			name: "ByObject.Namespaces gets defaulted from DefaultLabelSelector",
   109  			in: Options{
   110  				ByObject: map[client.Object]ByObject{pod: {
   111  					Namespaces: map[string]Config{
   112  						"default": {},
   113  					},
   114  				}},
   115  				DefaultLabelSelector: labels.SelectorFromSet(map[string]string{"from": "default-label-selector"}),
   116  			},
   117  
   118  			verification: func(o Options) string {
   119  				expected := map[string]Config{
   120  					"default": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "default-label-selector"})},
   121  				}
   122  				return cmp.Diff(expected, o.ByObject[pod].Namespaces)
   123  			},
   124  		},
   125  		{
   126  			name: "ByObject.Namespaces gets defaulted from DefaultNamespaces",
   127  			in: Options{
   128  				ByObject: map[client.Object]ByObject{pod: {}},
   129  				DefaultNamespaces: map[string]Config{
   130  					"default": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "default-namespaces"})},
   131  				},
   132  			},
   133  
   134  			verification: func(o Options) string {
   135  				expected := map[string]Config{
   136  					"default": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "default-namespaces"})},
   137  				}
   138  				return cmp.Diff(expected, o.ByObject[pod].Namespaces)
   139  			},
   140  		},
   141  		{
   142  			name: "ByObject.Namespaces doesn't get defaulted when its empty",
   143  			in: Options{
   144  				ByObject: map[client.Object]ByObject{pod: {Namespaces: map[string]Config{}}},
   145  				DefaultNamespaces: map[string]Config{
   146  					"default": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "default-namespaces"})},
   147  				},
   148  			},
   149  
   150  			verification: func(o Options) string {
   151  				expected := map[string]Config{}
   152  				return cmp.Diff(expected, o.ByObject[pod].Namespaces)
   153  			},
   154  		},
   155  		{
   156  			name: "ByObject.Labels gets defaulted from DefautLabelSelector",
   157  			in: Options{
   158  				ByObject:             map[client.Object]ByObject{pod: {}},
   159  				DefaultLabelSelector: labels.SelectorFromSet(map[string]string{"from": "default-label-selector"}),
   160  			},
   161  
   162  			verification: func(o Options) string {
   163  				expected := labels.SelectorFromSet(map[string]string{"from": "default-label-selector"})
   164  				return cmp.Diff(expected, o.ByObject[pod].Label)
   165  			},
   166  		},
   167  		{
   168  			name: "ByObject.Labels doesn't get defaulted when set",
   169  			in: Options{
   170  				ByObject:             map[client.Object]ByObject{pod: {Label: labels.SelectorFromSet(map[string]string{"from": "by-object"})}},
   171  				DefaultLabelSelector: labels.SelectorFromSet(map[string]string{"from": "default-label-selector"}),
   172  			},
   173  
   174  			verification: func(o Options) string {
   175  				expected := labels.SelectorFromSet(map[string]string{"from": "by-object"})
   176  				return cmp.Diff(expected, o.ByObject[pod].Label)
   177  			},
   178  		},
   179  		{
   180  			name: "ByObject.Fields gets defaulted from DefaultFieldSelector",
   181  			in: Options{
   182  				ByObject:             map[client.Object]ByObject{pod: {}},
   183  				DefaultFieldSelector: fields.SelectorFromSet(map[string]string{"from": "default-field-selector"}),
   184  			},
   185  
   186  			verification: func(o Options) string {
   187  				expected := fields.SelectorFromSet(map[string]string{"from": "default-field-selector"})
   188  				return cmp.Diff(expected, o.ByObject[pod].Field, cmp.Exporter(func(reflect.Type) bool { return true }))
   189  			},
   190  		},
   191  		{
   192  			name: "ByObject.Fields doesn't get defaulted when set",
   193  			in: Options{
   194  				ByObject:             map[client.Object]ByObject{pod: {Field: fields.SelectorFromSet(map[string]string{"from": "by-object"})}},
   195  				DefaultFieldSelector: fields.SelectorFromSet(map[string]string{"from": "default-field-selector"}),
   196  			},
   197  
   198  			verification: func(o Options) string {
   199  				expected := fields.SelectorFromSet(map[string]string{"from": "by-object"})
   200  				return cmp.Diff(expected, o.ByObject[pod].Field, cmp.Exporter(func(reflect.Type) bool { return true }))
   201  			},
   202  		},
   203  		{
   204  			name: "ByObject.UnsafeDisableDeepCopy gets defaulted from DefaultUnsafeDisableDeepCopy",
   205  			in: Options{
   206  				ByObject:                     map[client.Object]ByObject{pod: {}},
   207  				DefaultUnsafeDisableDeepCopy: ptr.To(true),
   208  			},
   209  
   210  			verification: func(o Options) string {
   211  				expected := ptr.To(true)
   212  				return cmp.Diff(expected, o.ByObject[pod].UnsafeDisableDeepCopy)
   213  			},
   214  		},
   215  		{
   216  			name: "ByObject.UnsafeDisableDeepCopy doesn't get defaulted when set",
   217  			in: Options{
   218  				ByObject:                     map[client.Object]ByObject{pod: {UnsafeDisableDeepCopy: ptr.To(false)}},
   219  				DefaultUnsafeDisableDeepCopy: ptr.To(true),
   220  			},
   221  
   222  			verification: func(o Options) string {
   223  				expected := ptr.To(false)
   224  				return cmp.Diff(expected, o.ByObject[pod].UnsafeDisableDeepCopy)
   225  			},
   226  		},
   227  		{
   228  			name: "DefaultNamespace label selector gets defaulted from DefaultLabelSelector",
   229  			in: Options{
   230  				DefaultNamespaces:    map[string]Config{"default": {}},
   231  				DefaultLabelSelector: labels.SelectorFromSet(map[string]string{"from": "default-label-selector"}),
   232  			},
   233  
   234  			verification: func(o Options) string {
   235  				expected := map[string]Config{
   236  					"default": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "default-label-selector"})},
   237  				}
   238  				return cmp.Diff(expected, o.DefaultNamespaces)
   239  			},
   240  		},
   241  		{
   242  			name: "ByObject.Namespaces get selector from DefaultNamespaces before DefaultSelector",
   243  			in: Options{
   244  				ByObject: map[client.Object]ByObject{
   245  					pod: {Namespaces: map[string]Config{"default": {}}},
   246  				},
   247  				DefaultNamespaces:    map[string]Config{"default": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "namespace"})}},
   248  				DefaultLabelSelector: labels.SelectorFromSet(map[string]string{"from": "default"}),
   249  			},
   250  
   251  			verification: func(o Options) string {
   252  				expected := Options{
   253  					ByObject: map[client.Object]ByObject{
   254  						pod: {Namespaces: map[string]Config{"default": {
   255  							LabelSelector: labels.SelectorFromSet(map[string]string{"from": "namespace"}),
   256  						}}},
   257  					},
   258  					DefaultNamespaces:    map[string]Config{"default": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "namespace"})}},
   259  					DefaultLabelSelector: labels.SelectorFromSet(map[string]string{"from": "default"}),
   260  				}
   261  
   262  				return compare(expected, o)
   263  			},
   264  		},
   265  		{
   266  			name: "Two namespaces in DefaultNamespaces with custom selection logic",
   267  			in: Options{DefaultNamespaces: map[string]Config{
   268  				"kube-public": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "kube-public"})},
   269  				"kube-system": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "kube-system"})},
   270  				"":            {},
   271  			}},
   272  
   273  			verification: func(o Options) string {
   274  				expected := Options{
   275  					DefaultNamespaces: map[string]Config{
   276  						"kube-public": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "kube-public"})},
   277  						"kube-system": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "kube-system"})},
   278  						"":            {FieldSelector: fields.ParseSelectorOrDie("metadata.namespace!=kube-public,metadata.namespace!=kube-system")},
   279  					},
   280  				}
   281  
   282  				return compare(expected, o)
   283  			},
   284  		},
   285  		{
   286  			name: "Two namespaces in DefaultNamespaces with custom selection logic and namespace default has its own field selector",
   287  			in: Options{DefaultNamespaces: map[string]Config{
   288  				"kube-public": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "kube-public"})},
   289  				"kube-system": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "kube-system"})},
   290  				"":            {FieldSelector: fields.ParseSelectorOrDie("spec.nodeName=foo")},
   291  			}},
   292  
   293  			verification: func(o Options) string {
   294  				expected := Options{
   295  					DefaultNamespaces: map[string]Config{
   296  						"kube-public": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "kube-public"})},
   297  						"kube-system": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "kube-system"})},
   298  						"": {FieldSelector: fields.ParseSelectorOrDie(
   299  							"metadata.namespace!=kube-public,metadata.namespace!=kube-system,spec.nodeName=foo",
   300  						)},
   301  					},
   302  				}
   303  
   304  				return compare(expected, o)
   305  			},
   306  		},
   307  		{
   308  			name: "Two namespaces in ByObject.Namespaces with custom selection logic",
   309  			in: Options{ByObject: map[client.Object]ByObject{pod: {
   310  				Namespaces: map[string]Config{
   311  					"kube-public": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "kube-public"})},
   312  					"kube-system": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "kube-system"})},
   313  					"":            {},
   314  				},
   315  			}}},
   316  
   317  			verification: func(o Options) string {
   318  				expected := Options{ByObject: map[client.Object]ByObject{pod: {
   319  					Namespaces: map[string]Config{
   320  						"kube-public": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "kube-public"})},
   321  						"kube-system": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "kube-system"})},
   322  						"": {FieldSelector: fields.ParseSelectorOrDie(
   323  							"metadata.namespace!=kube-public,metadata.namespace!=kube-system",
   324  						)},
   325  					},
   326  				}}}
   327  
   328  				return compare(expected, o)
   329  			},
   330  		},
   331  		{
   332  			name: "Two namespaces in ByObject.Namespaces with custom selection logic and namespace default has its own field selector",
   333  			in: Options{ByObject: map[client.Object]ByObject{pod: {
   334  				Namespaces: map[string]Config{
   335  					"kube-public": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "kube-public"})},
   336  					"kube-system": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "kube-system"})},
   337  					"":            {FieldSelector: fields.ParseSelectorOrDie("spec.nodeName=foo")},
   338  				},
   339  			}}},
   340  
   341  			verification: func(o Options) string {
   342  				expected := Options{ByObject: map[client.Object]ByObject{pod: {
   343  					Namespaces: map[string]Config{
   344  						"kube-public": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "kube-public"})},
   345  						"kube-system": {LabelSelector: labels.SelectorFromSet(map[string]string{"from": "kube-system"})},
   346  						"": {FieldSelector: fields.ParseSelectorOrDie(
   347  							"metadata.namespace!=kube-public,metadata.namespace!=kube-system,spec.nodeName=foo",
   348  						)},
   349  					},
   350  				}}}
   351  
   352  				return compare(expected, o)
   353  			},
   354  		},
   355  		{
   356  			name: "DefaultNamespace label selector doesn't get defaulted when set",
   357  			in: Options{
   358  				DefaultNamespaces:    map[string]Config{"default": {LabelSelector: labels.Everything()}},
   359  				DefaultLabelSelector: labels.SelectorFromSet(map[string]string{"from": "default-label-selector"}),
   360  			},
   361  
   362  			verification: func(o Options) string {
   363  				expected := map[string]Config{
   364  					"default": {LabelSelector: labels.Everything()},
   365  				}
   366  				return cmp.Diff(expected, o.DefaultNamespaces)
   367  			},
   368  		},
   369  		{
   370  			name: "Defaulted namespaces in ByObject contain ByObject's selector",
   371  			in: Options{
   372  				ByObject: map[client.Object]ByObject{
   373  					pod: {Label: labels.SelectorFromSet(map[string]string{"from": "pod"})},
   374  				},
   375  				DefaultNamespaces: map[string]Config{"default": {}},
   376  			},
   377  			verification: func(o Options) string {
   378  				expected := Options{
   379  					ByObject: map[client.Object]ByObject{
   380  						pod: {
   381  							Label: labels.SelectorFromSet(map[string]string{"from": "pod"}),
   382  							Namespaces: map[string]Config{"default": {
   383  								LabelSelector: labels.SelectorFromSet(map[string]string{"from": "pod"}),
   384  							}},
   385  						},
   386  					},
   387  
   388  					DefaultNamespaces: map[string]Config{"default": {}},
   389  				}
   390  				return compare(expected, o)
   391  			},
   392  		},
   393  	}
   394  
   395  	for _, tc := range testCases {
   396  		t.Run(tc.name, func(t *testing.T) {
   397  			tc.in.Mapper = &fakeRESTMapper{}
   398  
   399  			defaulted, err := defaultOpts(&rest.Config{}, tc.in)
   400  			if err != nil {
   401  				t.Fatal(err)
   402  			}
   403  
   404  			if diff := tc.verification(defaulted); diff != "" {
   405  				t.Errorf("expected config differs from actual: %s", diff)
   406  			}
   407  		})
   408  	}
   409  }
   410  
   411  type fakeRESTMapper struct {
   412  	meta.RESTMapper
   413  }
   414  
   415  func (f *fakeRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) {
   416  	return &meta.RESTMapping{Scope: meta.RESTScopeNamespace}, nil
   417  }
   418  
   419  func TestDefaultConfigConsidersAllFields(t *testing.T) {
   420  	t.Parallel()
   421  	seed := time.Now().UnixNano()
   422  	t.Logf("Seed is %d", seed)
   423  	f := fuzz.NewWithSeed(seed).Funcs(
   424  		func(ls *labels.Selector, _ fuzz.Continue) {
   425  			*ls = labels.SelectorFromSet(map[string]string{"foo": "bar"})
   426  		},
   427  		func(fs *fields.Selector, _ fuzz.Continue) {
   428  			*fs = fields.SelectorFromSet(map[string]string{"foo": "bar"})
   429  		},
   430  		func(tf *cache.TransformFunc, _ fuzz.Continue) {
   431  			// never default this, as functions can not be compared so we fail down the line
   432  		},
   433  	)
   434  
   435  	for i := 0; i < 100; i++ {
   436  		fuzzed := Config{}
   437  		f.Fuzz(&fuzzed)
   438  
   439  		defaulted := defaultConfig(Config{}, fuzzed)
   440  
   441  		if diff := cmp.Diff(fuzzed, defaulted, cmp.Exporter(func(reflect.Type) bool { return true })); diff != "" {
   442  			t.Errorf("Defaulted config doesn't match fuzzed one: %s", diff)
   443  		}
   444  	}
   445  }
   446  

View as plain text