istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/kube/krt/collection_test.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package krt_test
    16  
    17  import (
    18  	"strings"
    19  	"testing"
    20  
    21  	corev1 "k8s.io/api/core/v1"
    22  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    23  
    24  	istioclient "istio.io/client-go/pkg/apis/networking/v1alpha3"
    25  	"istio.io/istio/pkg/config"
    26  	"istio.io/istio/pkg/kube"
    27  	"istio.io/istio/pkg/kube/kclient"
    28  	"istio.io/istio/pkg/kube/kclient/clienttest"
    29  	"istio.io/istio/pkg/kube/krt"
    30  	"istio.io/istio/pkg/log"
    31  	"istio.io/istio/pkg/slices"
    32  	"istio.io/istio/pkg/test"
    33  	"istio.io/istio/pkg/test/util/assert"
    34  	"istio.io/istio/pkg/util/sets"
    35  )
    36  
    37  type SimplePod struct {
    38  	Named
    39  	Labeled
    40  	IP string
    41  }
    42  
    43  func SimplePodCollection(pods krt.Collection[*corev1.Pod]) krt.Collection[SimplePod] {
    44  	return krt.NewCollection(pods, func(ctx krt.HandlerContext, i *corev1.Pod) *SimplePod {
    45  		if i.Status.PodIP == "" {
    46  			return nil
    47  		}
    48  		return &SimplePod{
    49  			Named:   NewNamed(i),
    50  			Labeled: NewLabeled(i.Labels),
    51  			IP:      i.Status.PodIP,
    52  		}
    53  	})
    54  }
    55  
    56  type SizedPod struct {
    57  	Named
    58  	Size string
    59  }
    60  
    61  func SizedPodCollection(pods krt.Collection[*corev1.Pod]) krt.Collection[SizedPod] {
    62  	return krt.NewCollection(pods, func(ctx krt.HandlerContext, i *corev1.Pod) *SizedPod {
    63  		s, f := i.Labels["size"]
    64  		if !f {
    65  			return nil
    66  		}
    67  		return &SizedPod{
    68  			Named: NewNamed(i),
    69  			Size:  s,
    70  		}
    71  	})
    72  }
    73  
    74  func NewNamed(n config.Namer) Named {
    75  	return Named{
    76  		Namespace: n.GetNamespace(),
    77  		Name:      n.GetName(),
    78  	}
    79  }
    80  
    81  type Named struct {
    82  	Namespace string
    83  	Name      string
    84  }
    85  
    86  func (s Named) ResourceName() string {
    87  	return s.Namespace + "/" + s.Name
    88  }
    89  
    90  func NewLabeled(n map[string]string) Labeled {
    91  	return Labeled{n}
    92  }
    93  
    94  type Labeled struct {
    95  	Labels map[string]string
    96  }
    97  
    98  func (l Labeled) GetLabels() map[string]string {
    99  	return l.Labels
   100  }
   101  
   102  type SimpleService struct {
   103  	Named
   104  	Selector map[string]string
   105  }
   106  
   107  func SimpleServiceCollection(services krt.Collection[*corev1.Service]) krt.Collection[SimpleService] {
   108  	return krt.NewCollection(services, func(ctx krt.HandlerContext, i *corev1.Service) *SimpleService {
   109  		return &SimpleService{
   110  			Named:    NewNamed(i),
   111  			Selector: i.Spec.Selector,
   112  		}
   113  	})
   114  }
   115  
   116  func SimpleServiceCollectionFromEntries(entries krt.Collection[*istioclient.ServiceEntry]) krt.Collection[SimpleService] {
   117  	return krt.NewCollection(entries, func(ctx krt.HandlerContext, i *istioclient.ServiceEntry) *SimpleService {
   118  		l := i.Spec.WorkloadSelector.GetLabels()
   119  		if l == nil {
   120  			return nil
   121  		}
   122  		return &SimpleService{
   123  			Named:    NewNamed(i),
   124  			Selector: l,
   125  		}
   126  	})
   127  }
   128  
   129  type SimpleEndpoint struct {
   130  	Pod       string
   131  	Service   string
   132  	Namespace string
   133  	IP        string
   134  }
   135  
   136  func (s SimpleEndpoint) ResourceName() string {
   137  	return slices.Join("/", s.Namespace+"/"+s.Service+"/"+s.Pod)
   138  }
   139  
   140  func SimpleEndpointsCollection(pods krt.Collection[SimplePod], services krt.Collection[SimpleService]) krt.Collection[SimpleEndpoint] {
   141  	return krt.NewManyCollection[SimpleService, SimpleEndpoint](services, func(ctx krt.HandlerContext, svc SimpleService) []SimpleEndpoint {
   142  		pods := krt.Fetch(ctx, pods, krt.FilterLabel(svc.Selector))
   143  		return slices.Map(pods, func(pod SimplePod) SimpleEndpoint {
   144  			return SimpleEndpoint{
   145  				Pod:       pod.Name,
   146  				Service:   svc.Name,
   147  				Namespace: svc.Namespace,
   148  				IP:        pod.IP,
   149  			}
   150  		})
   151  	})
   152  }
   153  
   154  func init() {
   155  	log.FindScope("krt").SetOutputLevel(log.DebugLevel)
   156  }
   157  
   158  func TestCollectionSimple(t *testing.T) {
   159  	c := kube.NewFakeClient()
   160  	kpc := kclient.New[*corev1.Pod](c)
   161  	pc := clienttest.Wrap(t, kpc)
   162  	pods := krt.WrapClient[*corev1.Pod](kpc)
   163  	stop := test.NewStop(t)
   164  	c.RunAndWait(stop)
   165  	SimplePods := SimplePodCollection(pods)
   166  
   167  	assert.Equal(t, fetcherSorted(SimplePods)(), nil)
   168  	pod := &corev1.Pod{
   169  		ObjectMeta: metav1.ObjectMeta{
   170  			Name:      "name",
   171  			Namespace: "namespace",
   172  		},
   173  	}
   174  	pc.Create(pod)
   175  	assert.Equal(t, fetcherSorted(SimplePods)(), nil)
   176  
   177  	pod.Status = corev1.PodStatus{PodIP: "1.2.3.4"}
   178  	pc.UpdateStatus(pod)
   179  	assert.EventuallyEqual(t, fetcherSorted(SimplePods), []SimplePod{{NewNamed(pod), Labeled{}, "1.2.3.4"}})
   180  
   181  	pod.Status.PodIP = "1.2.3.5"
   182  	pc.UpdateStatus(pod)
   183  	assert.EventuallyEqual(t, fetcherSorted(SimplePods), []SimplePod{{NewNamed(pod), Labeled{}, "1.2.3.5"}})
   184  
   185  	// check we get updates if we add a handler later
   186  	tt := assert.NewTracker[string](t)
   187  	SimplePods.Register(TrackerHandler[SimplePod](tt))
   188  	tt.WaitUnordered("add/namespace/name")
   189  
   190  	pc.Delete(pod.Name, pod.Namespace)
   191  	assert.EventuallyEqual(t, fetcherSorted(SimplePods), nil)
   192  	tt.WaitUnordered("delete/namespace/name")
   193  }
   194  
   195  func TestCollectionInitialState(t *testing.T) {
   196  	c := kube.NewFakeClient(
   197  		&corev1.Pod{
   198  			ObjectMeta: metav1.ObjectMeta{
   199  				Name:      "pod",
   200  				Namespace: "namespace",
   201  				Labels:    map[string]string{"app": "foo"},
   202  			},
   203  			Status: corev1.PodStatus{PodIP: "1.2.3.4"},
   204  		},
   205  		&corev1.Service{
   206  			ObjectMeta: metav1.ObjectMeta{
   207  				Name:      "svc",
   208  				Namespace: "namespace",
   209  			},
   210  			Spec: corev1.ServiceSpec{Selector: map[string]string{"app": "foo"}},
   211  		},
   212  	)
   213  	pods := krt.NewInformer[*corev1.Pod](c)
   214  	services := krt.NewInformer[*corev1.Service](c)
   215  	stop := test.NewStop(t)
   216  	c.RunAndWait(stop)
   217  	SimplePods := SimplePodCollection(pods)
   218  	SimpleServices := SimpleServiceCollection(services)
   219  	SimpleEndpoints := SimpleEndpointsCollection(SimplePods, SimpleServices)
   220  	assert.Equal(t, SimpleEndpoints.Synced().WaitUntilSynced(stop), true)
   221  	// Assert Equal -- not EventuallyEqual -- to ensure our WaitForCacheSync is proper
   222  	assert.Equal(t, fetcherSorted(SimpleEndpoints)(), []SimpleEndpoint{{"pod", "svc", "namespace", "1.2.3.4"}})
   223  }
   224  
   225  func TestCollectionMerged(t *testing.T) {
   226  	c := kube.NewFakeClient()
   227  	pods := krt.NewInformer[*corev1.Pod](c)
   228  	services := krt.NewInformer[*corev1.Service](c)
   229  	stop := test.NewStop(t)
   230  	c.RunAndWait(stop)
   231  	pc := clienttest.Wrap(t, kclient.New[*corev1.Pod](c))
   232  	sc := clienttest.Wrap(t, kclient.New[*corev1.Service](c))
   233  	SimplePods := SimplePodCollection(pods)
   234  	SimpleServices := SimpleServiceCollection(services)
   235  	SimpleEndpoints := SimpleEndpointsCollection(SimplePods, SimpleServices)
   236  
   237  	assert.Equal(t, fetcherSorted(SimpleEndpoints)(), nil)
   238  	pod := &corev1.Pod{
   239  		ObjectMeta: metav1.ObjectMeta{
   240  			Name:      "pod",
   241  			Namespace: "namespace",
   242  			Labels:    map[string]string{"app": "foo"},
   243  		},
   244  	}
   245  	pc.Create(pod)
   246  	assert.Equal(t, fetcherSorted(SimpleEndpoints)(), nil)
   247  
   248  	svc := &corev1.Service{
   249  		ObjectMeta: metav1.ObjectMeta{
   250  			Name:      "svc",
   251  			Namespace: "namespace",
   252  		},
   253  		Spec: corev1.ServiceSpec{Selector: map[string]string{"app": "foo"}},
   254  	}
   255  	sc.Create(svc)
   256  	assert.Equal(t, fetcherSorted(SimpleEndpoints)(), nil)
   257  
   258  	pod.Status = corev1.PodStatus{PodIP: "1.2.3.4"}
   259  	pc.UpdateStatus(pod)
   260  	assert.EventuallyEqual(t, fetcherSorted(SimpleEndpoints), []SimpleEndpoint{{pod.Name, svc.Name, pod.Namespace, "1.2.3.4"}})
   261  
   262  	pod.Status.PodIP = "1.2.3.5"
   263  	pc.UpdateStatus(pod)
   264  	assert.EventuallyEqual(t, fetcherSorted(SimpleEndpoints), []SimpleEndpoint{{pod.Name, svc.Name, pod.Namespace, "1.2.3.5"}})
   265  
   266  	pc.Delete(pod.Name, pod.Namespace)
   267  	assert.EventuallyEqual(t, fetcherSorted(SimpleEndpoints), nil)
   268  
   269  	pod2 := &corev1.Pod{
   270  		ObjectMeta: metav1.ObjectMeta{
   271  			Name:      "name2",
   272  			Namespace: "namespace",
   273  			Labels:    map[string]string{"app": "foo"},
   274  		},
   275  		Status: corev1.PodStatus{PodIP: "2.3.4.5"},
   276  	}
   277  	pc.CreateOrUpdateStatus(pod)
   278  	pc.CreateOrUpdateStatus(pod2)
   279  	assert.EventuallyEqual(t, fetcherSorted(SimpleEndpoints), []SimpleEndpoint{
   280  		{pod2.Name, svc.Name, pod2.Namespace, pod2.Status.PodIP},
   281  		{pod.Name, svc.Name, pod.Namespace, pod.Status.PodIP},
   282  	})
   283  }
   284  
   285  type PodSizeCount struct {
   286  	Named
   287  	MatchingSizes int
   288  }
   289  
   290  func TestCollectionCycle(t *testing.T) {
   291  	c := kube.NewFakeClient()
   292  	pods := krt.NewInformer[*corev1.Pod](c)
   293  	c.RunAndWait(test.NewStop(t))
   294  	pc := clienttest.Wrap(t, kclient.New[*corev1.Pod](c))
   295  	SimplePods := SimplePodCollection(pods)
   296  	SizedPods := SizedPodCollection(pods)
   297  	Thingys := krt.NewCollection[SimplePod, PodSizeCount](SimplePods, func(ctx krt.HandlerContext, pd SimplePod) *PodSizeCount {
   298  		if _, f := pd.Labels["want-size"]; !f {
   299  			return nil
   300  		}
   301  		matches := krt.Fetch(ctx, SizedPods, krt.FilterGeneric(func(a any) bool {
   302  			return a.(SizedPod).Size == pd.Labels["want-size"]
   303  		}))
   304  		return &PodSizeCount{
   305  			Named:         pd.Named,
   306  			MatchingSizes: len(matches),
   307  		}
   308  	})
   309  	tt := assert.NewTracker[string](t)
   310  	Thingys.RegisterBatch(BatchedTrackerHandler[PodSizeCount](tt), true)
   311  
   312  	assert.Equal(t, fetcherSorted(Thingys)(), nil)
   313  	pod := &corev1.Pod{
   314  		ObjectMeta: metav1.ObjectMeta{
   315  			Name:      "name",
   316  			Namespace: "namespace",
   317  			Labels:    map[string]string{"want-size": "large"},
   318  		},
   319  		Status: corev1.PodStatus{PodIP: "1.2.3.4"},
   320  	}
   321  	pc.CreateOrUpdateStatus(pod)
   322  	tt.WaitOrdered("add/namespace/name")
   323  	assert.Equal(t, fetcherSorted(Thingys)(), []PodSizeCount{{
   324  		Named:         NewNamed(pod),
   325  		MatchingSizes: 0,
   326  	}})
   327  
   328  	largePod := &corev1.Pod{
   329  		ObjectMeta: metav1.ObjectMeta{
   330  			Name:      "name-large",
   331  			Namespace: "namespace",
   332  			Labels:    map[string]string{"size": "large"},
   333  		},
   334  		Status: corev1.PodStatus{PodIP: "1.2.3.5"},
   335  	}
   336  	pc.CreateOrUpdateStatus(largePod)
   337  	tt.WaitOrdered("update/namespace/name")
   338  	assert.Equal(t, fetcherSorted(Thingys)(), []PodSizeCount{{
   339  		Named:         NewNamed(pod),
   340  		MatchingSizes: 1,
   341  	}})
   342  
   343  	smallPod := &corev1.Pod{
   344  		ObjectMeta: metav1.ObjectMeta{
   345  			Name:      "name-small",
   346  			Namespace: "namespace",
   347  			Labels:    map[string]string{"size": "small"},
   348  		},
   349  		Status: corev1.PodStatus{PodIP: "1.2.3.6"},
   350  	}
   351  	pc.CreateOrUpdateStatus(smallPod)
   352  	pc.CreateOrUpdateStatus(largePod)
   353  	assert.Equal(t, fetcherSorted(Thingys)(), []PodSizeCount{{
   354  		Named:         NewNamed(pod),
   355  		MatchingSizes: 1,
   356  	}})
   357  	tt.Empty()
   358  
   359  	largePod2 := &corev1.Pod{
   360  		ObjectMeta: metav1.ObjectMeta{
   361  			Name:      "name-large2",
   362  			Namespace: "namespace",
   363  			Labels:    map[string]string{"size": "large"},
   364  		},
   365  		Status: corev1.PodStatus{PodIP: "1.2.3.7"},
   366  	}
   367  	pc.CreateOrUpdateStatus(largePod2)
   368  	tt.WaitOrdered("update/namespace/name")
   369  	assert.Equal(t, fetcherSorted(Thingys)(), []PodSizeCount{{
   370  		Named:         NewNamed(pod),
   371  		MatchingSizes: 2,
   372  	}})
   373  
   374  	dual := &corev1.Pod{
   375  		ObjectMeta: metav1.ObjectMeta{
   376  			Name:      "name-dual",
   377  			Namespace: "namespace",
   378  			Labels:    map[string]string{"size": "large", "want-size": "small"},
   379  		},
   380  		Status: corev1.PodStatus{PodIP: "1.2.3.8"},
   381  	}
   382  	pc.CreateOrUpdateStatus(dual)
   383  	tt.WaitUnordered("update/namespace/name", "add/namespace/name-dual")
   384  	assert.Equal(t, fetcherSorted(Thingys)(), []PodSizeCount{
   385  		{
   386  			Named:         NewNamed(pod),
   387  			MatchingSizes: 3,
   388  		},
   389  		{
   390  			Named:         NewNamed(dual),
   391  			MatchingSizes: 1,
   392  		},
   393  	})
   394  
   395  	largePod2.Labels["size"] = "small"
   396  	pc.CreateOrUpdateStatus(largePod2)
   397  	tt.WaitCompare(CompareUnordered("update/namespace/name-dual", "update/namespace/name"))
   398  	assert.Equal(t, fetcherSorted(Thingys)(), []PodSizeCount{
   399  		{
   400  			Named:         NewNamed(pod),
   401  			MatchingSizes: 2,
   402  		},
   403  		{
   404  			Named:         NewNamed(dual),
   405  			MatchingSizes: 2,
   406  		},
   407  	})
   408  
   409  	pc.Delete(dual.Name, dual.Namespace)
   410  	tt.WaitUnordered("update/namespace/name", "delete/namespace/name-dual")
   411  	assert.Equal(t, fetcherSorted(Thingys)(), []PodSizeCount{{
   412  		Named:         NewNamed(pod),
   413  		MatchingSizes: 1,
   414  	}})
   415  
   416  	pc.Delete(largePod.Name, largePod.Namespace)
   417  	tt.WaitOrdered("update/namespace/name")
   418  	assert.Equal(t, fetcherSorted(Thingys)(), []PodSizeCount{{
   419  		Named:         NewNamed(pod),
   420  		MatchingSizes: 0,
   421  	}})
   422  
   423  	pc.Delete(pod.Name, pod.Namespace)
   424  	tt.WaitOrdered("delete/namespace/name")
   425  	assert.Equal(t, fetcherSorted(Thingys)(), []PodSizeCount{})
   426  }
   427  
   428  func CompareUnordered(wants ...string) func(s string) bool {
   429  	want := sets.New(wants...)
   430  	return func(s string) bool {
   431  		got := sets.New(strings.Split(s, ",")...)
   432  		return want.Equals(got)
   433  	}
   434  }
   435  
   436  func fetcherSorted[T krt.ResourceNamer](c krt.Collection[T]) func() []T {
   437  	return func() []T {
   438  		return slices.SortBy(c.List(), func(t T) string {
   439  			return t.ResourceName()
   440  		})
   441  	}
   442  }
   443  
   444  func TestCollectionMultipleFetch(t *testing.T) {
   445  	type Result struct {
   446  		Named
   447  		Configs []string
   448  	}
   449  	c := kube.NewFakeClient()
   450  	kpc := kclient.New[*corev1.Pod](c)
   451  	kcc := kclient.New[*corev1.ConfigMap](c)
   452  	pc := clienttest.Wrap(t, kpc)
   453  	cc := clienttest.Wrap(t, kcc)
   454  	pods := krt.WrapClient[*corev1.Pod](kpc)
   455  	configMaps := krt.WrapClient[*corev1.ConfigMap](kcc)
   456  	c.RunAndWait(test.NewStop(t))
   457  
   458  	lblFoo := map[string]string{"app": "foo"}
   459  	lblBar := map[string]string{"app": "bar"}
   460  	// Setup a simple collection that fetches the same dependency twice
   461  	Results := krt.NewCollection(pods, func(ctx krt.HandlerContext, i *corev1.Pod) *Result {
   462  		foos := krt.Fetch(ctx, configMaps, krt.FilterLabel(lblFoo))
   463  		bars := krt.Fetch(ctx, configMaps, krt.FilterLabel(lblBar))
   464  		names := slices.Map(foos, func(f *corev1.ConfigMap) string { return f.Name })
   465  		names = append(names, slices.Map(bars, func(f *corev1.ConfigMap) string { return f.Name })...)
   466  		names = slices.Sort(names)
   467  		return &Result{
   468  			Named:   NewNamed(i),
   469  			Configs: slices.Sort(names),
   470  		}
   471  	})
   472  
   473  	assert.Equal(t, fetcherSorted(Results)(), nil)
   474  	pod := &corev1.Pod{
   475  		ObjectMeta: metav1.ObjectMeta{
   476  			Name:      "name",
   477  			Namespace: "namespace",
   478  		},
   479  	}
   480  	pc.Create(pod)
   481  	assert.EventuallyEqual(t, fetcherSorted(Results), []Result{{NewNamed(pod), nil}})
   482  
   483  	cc.Create(&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "foo1", Labels: lblFoo}})
   484  	assert.EventuallyEqual(t, fetcherSorted(Results), []Result{{NewNamed(pod), []string{"foo1"}}})
   485  
   486  	cc.Create(&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "switch", Labels: lblFoo}})
   487  	assert.EventuallyEqual(t, fetcherSorted(Results), []Result{{NewNamed(pod), []string{"foo1", "switch"}}})
   488  
   489  	cc.Create(&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "bar1", Labels: lblBar}})
   490  	assert.EventuallyEqual(t, fetcherSorted(Results), []Result{{NewNamed(pod), []string{"bar1", "foo1", "switch"}}})
   491  
   492  	cc.Update(&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "switch", Labels: lblBar}})
   493  	assert.EventuallyEqual(t, fetcherSorted(Results), []Result{{NewNamed(pod), []string{"bar1", "foo1", "switch"}}})
   494  
   495  	cc.Update(&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "switch", Labels: nil}})
   496  	assert.EventuallyEqual(t, fetcherSorted(Results), []Result{{NewNamed(pod), []string{"bar1", "foo1"}}})
   497  
   498  	cc.Delete("bar1", "")
   499  	assert.EventuallyEqual(t, fetcherSorted(Results), []Result{{NewNamed(pod), []string{"foo1"}}})
   500  
   501  	cc.Delete("foo1", "")
   502  	assert.EventuallyEqual(t, fetcherSorted(Results), []Result{{NewNamed(pod), nil}})
   503  }