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 }