github.com/netdata/go.d.plugin@v0.58.1/agent/discovery/sd/kubernetes/pod_test.go (about) 1 // SPDX-License-Identifier: GPL-3.0-or-later 2 3 package kubernetes 4 5 import ( 6 "context" 7 "net" 8 "strconv" 9 "testing" 10 "time" 11 12 "github.com/netdata/go.d.plugin/agent/discovery/sd/model" 13 14 "github.com/stretchr/testify/assert" 15 corev1 "k8s.io/api/core/v1" 16 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 "k8s.io/apimachinery/pkg/runtime" 18 "k8s.io/apimachinery/pkg/types" 19 "k8s.io/client-go/kubernetes" 20 "k8s.io/client-go/tools/cache" 21 ) 22 23 func TestPodTargetGroup_Provider(t *testing.T) { 24 var p podTargetGroup 25 assert.NotEmpty(t, p.Provider()) 26 } 27 28 func TestPodTargetGroup_Source(t *testing.T) { 29 tests := map[string]struct { 30 createSim func() discoverySim 31 wantSources []string 32 }{ 33 "pods with multiple ports": { 34 createSim: func() discoverySim { 35 httpd, nginx := newHTTPDPod(), newNGINXPod() 36 disc, _ := prepareAllNsPodDiscoverer(httpd, nginx) 37 38 return discoverySim{ 39 td: disc, 40 wantTargetGroups: []model.TargetGroup{ 41 preparePodTargetGroup(httpd), 42 preparePodTargetGroup(nginx), 43 }, 44 } 45 }, 46 wantSources: []string{ 47 "sd:k8s:pod(default/httpd-dd95c4d68-5bkwl)", 48 "sd:k8s:pod(default/nginx-7cfd77469b-q6kxj)", 49 }, 50 }, 51 } 52 53 for name, test := range tests { 54 t.Run(name, func(t *testing.T) { 55 sim := test.createSim() 56 57 var sources []string 58 for _, tgg := range sim.run(t) { 59 sources = append(sources, tgg.Source()) 60 } 61 62 assert.Equal(t, test.wantSources, sources) 63 }) 64 } 65 } 66 67 func TestPodTargetGroup_Targets(t *testing.T) { 68 tests := map[string]struct { 69 createSim func() discoverySim 70 wantTargets int 71 }{ 72 "pods with multiple ports": { 73 createSim: func() discoverySim { 74 httpd, nginx := newHTTPDPod(), newNGINXPod() 75 discovery, _ := prepareAllNsPodDiscoverer(httpd, nginx) 76 77 return discoverySim{ 78 td: discovery, 79 wantTargetGroups: []model.TargetGroup{ 80 preparePodTargetGroup(httpd), 81 preparePodTargetGroup(nginx), 82 }, 83 } 84 }, 85 wantTargets: 4, 86 }, 87 } 88 89 for name, test := range tests { 90 t.Run(name, func(t *testing.T) { 91 sim := test.createSim() 92 93 var targets int 94 for _, tgg := range sim.run(t) { 95 targets += len(tgg.Targets()) 96 } 97 98 assert.Equal(t, test.wantTargets, targets) 99 }) 100 } 101 } 102 103 func TestPodTarget_Hash(t *testing.T) { 104 tests := map[string]struct { 105 createSim func() discoverySim 106 wantHashes []uint64 107 }{ 108 "pods with multiple ports": { 109 createSim: func() discoverySim { 110 httpd, nginx := newHTTPDPod(), newNGINXPod() 111 discovery, _ := prepareAllNsPodDiscoverer(httpd, nginx) 112 113 return discoverySim{ 114 td: discovery, 115 wantTargetGroups: []model.TargetGroup{ 116 preparePodTargetGroup(httpd), 117 preparePodTargetGroup(nginx), 118 }, 119 } 120 }, 121 wantHashes: []uint64{ 122 12703169414253998055, 123 13351713096133918928, 124 8241692333761256175, 125 11562466355572729519, 126 }, 127 }, 128 } 129 130 for name, test := range tests { 131 t.Run(name, func(t *testing.T) { 132 sim := test.createSim() 133 134 var hashes []uint64 135 for _, tgg := range sim.run(t) { 136 for _, tg := range tgg.Targets() { 137 hashes = append(hashes, tg.Hash()) 138 } 139 } 140 141 assert.Equal(t, test.wantHashes, hashes) 142 }) 143 } 144 } 145 146 func TestPodTarget_TUID(t *testing.T) { 147 tests := map[string]struct { 148 createSim func() discoverySim 149 wantTUID []string 150 }{ 151 "pods with multiple ports": { 152 createSim: func() discoverySim { 153 httpd, nginx := newHTTPDPod(), newNGINXPod() 154 discovery, _ := prepareAllNsPodDiscoverer(httpd, nginx) 155 156 return discoverySim{ 157 td: discovery, 158 wantTargetGroups: []model.TargetGroup{ 159 preparePodTargetGroup(httpd), 160 preparePodTargetGroup(nginx), 161 }, 162 } 163 }, 164 wantTUID: []string{ 165 "default_httpd-dd95c4d68-5bkwl_httpd_tcp_80", 166 "default_httpd-dd95c4d68-5bkwl_httpd_tcp_443", 167 "default_nginx-7cfd77469b-q6kxj_nginx_tcp_80", 168 "default_nginx-7cfd77469b-q6kxj_nginx_tcp_443", 169 }, 170 }, 171 } 172 173 for name, test := range tests { 174 t.Run(name, func(t *testing.T) { 175 sim := test.createSim() 176 177 var tuid []string 178 for _, tgg := range sim.run(t) { 179 for _, tg := range tgg.Targets() { 180 tuid = append(tuid, tg.TUID()) 181 } 182 } 183 184 assert.Equal(t, test.wantTUID, tuid) 185 }) 186 } 187 } 188 189 func TestNewPodDiscoverer(t *testing.T) { 190 tests := map[string]struct { 191 podInf cache.SharedInformer 192 cmapInf cache.SharedInformer 193 secretInf cache.SharedInformer 194 wantPanic bool 195 }{ 196 "valid informers": { 197 wantPanic: false, 198 podInf: cache.NewSharedInformer(nil, &corev1.Pod{}, resyncPeriod), 199 cmapInf: cache.NewSharedInformer(nil, &corev1.ConfigMap{}, resyncPeriod), 200 secretInf: cache.NewSharedInformer(nil, &corev1.Secret{}, resyncPeriod), 201 }, 202 "nil informers": { 203 wantPanic: true, 204 }, 205 } 206 207 for name, test := range tests { 208 t.Run(name, func(t *testing.T) { 209 f := func() { newPodDiscoverer(test.podInf, test.cmapInf, test.secretInf) } 210 211 if test.wantPanic { 212 assert.Panics(t, f) 213 } else { 214 assert.NotPanics(t, f) 215 } 216 }) 217 } 218 } 219 220 func TestPodDiscoverer_String(t *testing.T) { 221 var p podDiscoverer 222 assert.NotEmpty(t, p.String()) 223 } 224 225 func TestPodDiscoverer_Discover(t *testing.T) { 226 tests := map[string]func() discoverySim{ 227 "ADD: pods exist before run": func() discoverySim { 228 httpd, nginx := newHTTPDPod(), newNGINXPod() 229 td, _ := prepareAllNsPodDiscoverer(httpd, nginx) 230 231 return discoverySim{ 232 td: td, 233 wantTargetGroups: []model.TargetGroup{ 234 preparePodTargetGroup(httpd), 235 preparePodTargetGroup(nginx), 236 }, 237 } 238 }, 239 "ADD: pods exist before run and add after sync": func() discoverySim { 240 httpd, nginx := newHTTPDPod(), newNGINXPod() 241 disc, client := prepareAllNsPodDiscoverer(httpd) 242 podClient := client.CoreV1().Pods("default") 243 244 return discoverySim{ 245 td: disc, 246 runAfterSync: func(ctx context.Context) { 247 _, _ = podClient.Create(ctx, nginx, metav1.CreateOptions{}) 248 }, 249 wantTargetGroups: []model.TargetGroup{ 250 preparePodTargetGroup(httpd), 251 preparePodTargetGroup(nginx), 252 }, 253 } 254 }, 255 "DELETE: remove pods after sync": func() discoverySim { 256 httpd, nginx := newHTTPDPod(), newNGINXPod() 257 disc, client := prepareAllNsPodDiscoverer(httpd, nginx) 258 podClient := client.CoreV1().Pods("default") 259 260 return discoverySim{ 261 td: disc, 262 runAfterSync: func(ctx context.Context) { 263 time.Sleep(time.Millisecond * 50) 264 _ = podClient.Delete(ctx, httpd.Name, metav1.DeleteOptions{}) 265 _ = podClient.Delete(ctx, nginx.Name, metav1.DeleteOptions{}) 266 }, 267 wantTargetGroups: []model.TargetGroup{ 268 preparePodTargetGroup(httpd), 269 preparePodTargetGroup(nginx), 270 prepareEmptyPodTargetGroup(httpd), 271 prepareEmptyPodTargetGroup(nginx), 272 }, 273 } 274 }, 275 "DELETE,ADD: remove and add pods after sync": func() discoverySim { 276 httpd, nginx := newHTTPDPod(), newNGINXPod() 277 disc, client := prepareAllNsPodDiscoverer(httpd) 278 podClient := client.CoreV1().Pods("default") 279 280 return discoverySim{ 281 td: disc, 282 runAfterSync: func(ctx context.Context) { 283 time.Sleep(time.Millisecond * 50) 284 _ = podClient.Delete(ctx, httpd.Name, metav1.DeleteOptions{}) 285 _, _ = podClient.Create(ctx, nginx, metav1.CreateOptions{}) 286 }, 287 wantTargetGroups: []model.TargetGroup{ 288 preparePodTargetGroup(httpd), 289 prepareEmptyPodTargetGroup(httpd), 290 preparePodTargetGroup(nginx), 291 }, 292 } 293 }, 294 "ADD: pods with empty PodIP": func() discoverySim { 295 httpd, nginx := newHTTPDPod(), newNGINXPod() 296 httpd.Status.PodIP = "" 297 nginx.Status.PodIP = "" 298 disc, _ := prepareAllNsPodDiscoverer(httpd, nginx) 299 300 return discoverySim{ 301 td: disc, 302 wantTargetGroups: []model.TargetGroup{ 303 prepareEmptyPodTargetGroup(httpd), 304 prepareEmptyPodTargetGroup(nginx), 305 }, 306 } 307 }, 308 "UPDATE: set pods PodIP after sync": func() discoverySim { 309 httpd, nginx := newHTTPDPod(), newNGINXPod() 310 httpd.Status.PodIP = "" 311 nginx.Status.PodIP = "" 312 disc, client := prepareAllNsPodDiscoverer(httpd, nginx) 313 podClient := client.CoreV1().Pods("default") 314 315 return discoverySim{ 316 td: disc, 317 runAfterSync: func(ctx context.Context) { 318 time.Sleep(time.Millisecond * 50) 319 _, _ = podClient.Update(ctx, newHTTPDPod(), metav1.UpdateOptions{}) 320 _, _ = podClient.Update(ctx, newNGINXPod(), metav1.UpdateOptions{}) 321 }, 322 wantTargetGroups: []model.TargetGroup{ 323 prepareEmptyPodTargetGroup(httpd), 324 prepareEmptyPodTargetGroup(nginx), 325 preparePodTargetGroup(newHTTPDPod()), 326 preparePodTargetGroup(newNGINXPod()), 327 }, 328 } 329 }, 330 "ADD: pods without containers": func() discoverySim { 331 httpd, nginx := newHTTPDPod(), newNGINXPod() 332 httpd.Spec.Containers = httpd.Spec.Containers[:0] 333 nginx.Spec.Containers = httpd.Spec.Containers[:0] 334 disc, _ := prepareAllNsPodDiscoverer(httpd, nginx) 335 336 return discoverySim{ 337 td: disc, 338 wantTargetGroups: []model.TargetGroup{ 339 prepareEmptyPodTargetGroup(httpd), 340 prepareEmptyPodTargetGroup(nginx), 341 }, 342 } 343 }, 344 "Env: from value": func() discoverySim { 345 httpd := newHTTPDPod() 346 mangle := func(c *corev1.Container) { 347 c.Env = []corev1.EnvVar{ 348 {Name: "key1", Value: "value1"}, 349 } 350 } 351 mangleContainers(httpd.Spec.Containers, mangle) 352 data := map[string]string{"key1": "value1"} 353 354 disc, _ := prepareAllNsPodDiscoverer(httpd) 355 356 return discoverySim{ 357 td: disc, 358 wantTargetGroups: []model.TargetGroup{ 359 preparePodTargetGroupWithEnv(httpd, data), 360 }, 361 } 362 }, 363 "Env: from Secret": func() discoverySim { 364 httpd := newHTTPDPod() 365 mangle := func(c *corev1.Container) { 366 c.Env = []corev1.EnvVar{ 367 { 368 Name: "key1", 369 ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{ 370 LocalObjectReference: corev1.LocalObjectReference{Name: "my-secret"}, 371 Key: "key1", 372 }}, 373 }, 374 } 375 } 376 mangleContainers(httpd.Spec.Containers, mangle) 377 data := map[string]string{"key1": "value1"} 378 secret := prepareSecret("my-secret", data) 379 380 disc, _ := prepareAllNsPodDiscoverer(httpd, secret) 381 382 return discoverySim{ 383 td: disc, 384 wantTargetGroups: []model.TargetGroup{ 385 preparePodTargetGroupWithEnv(httpd, data), 386 }, 387 } 388 }, 389 "Env: from ConfigMap": func() discoverySim { 390 httpd := newHTTPDPod() 391 mangle := func(c *corev1.Container) { 392 c.Env = []corev1.EnvVar{ 393 { 394 Name: "key1", 395 ValueFrom: &corev1.EnvVarSource{ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ 396 LocalObjectReference: corev1.LocalObjectReference{Name: "my-cmap"}, 397 Key: "key1", 398 }}, 399 }, 400 } 401 } 402 mangleContainers(httpd.Spec.Containers, mangle) 403 data := map[string]string{"key1": "value1"} 404 cmap := prepareConfigMap("my-cmap", data) 405 406 disc, _ := prepareAllNsPodDiscoverer(httpd, cmap) 407 408 return discoverySim{ 409 td: disc, 410 wantTargetGroups: []model.TargetGroup{ 411 preparePodTargetGroupWithEnv(httpd, data), 412 }, 413 } 414 }, 415 "EnvFrom: from ConfigMap": func() discoverySim { 416 httpd := newHTTPDPod() 417 mangle := func(c *corev1.Container) { 418 c.EnvFrom = []corev1.EnvFromSource{ 419 { 420 ConfigMapRef: &corev1.ConfigMapEnvSource{ 421 LocalObjectReference: corev1.LocalObjectReference{Name: "my-cmap"}}, 422 }, 423 } 424 } 425 mangleContainers(httpd.Spec.Containers, mangle) 426 data := map[string]string{"key1": "value1", "key2": "value2"} 427 cmap := prepareConfigMap("my-cmap", data) 428 429 disc, _ := prepareAllNsPodDiscoverer(httpd, cmap) 430 431 return discoverySim{ 432 td: disc, 433 wantTargetGroups: []model.TargetGroup{ 434 preparePodTargetGroupWithEnv(httpd, data), 435 }, 436 } 437 }, 438 "EnvFrom: from Secret": func() discoverySim { 439 httpd := newHTTPDPod() 440 mangle := func(c *corev1.Container) { 441 c.EnvFrom = []corev1.EnvFromSource{ 442 { 443 SecretRef: &corev1.SecretEnvSource{ 444 LocalObjectReference: corev1.LocalObjectReference{Name: "my-secret"}}, 445 }, 446 } 447 } 448 mangleContainers(httpd.Spec.Containers, mangle) 449 data := map[string]string{"key1": "value1", "key2": "value2"} 450 secret := prepareSecret("my-secret", data) 451 452 disc, _ := prepareAllNsPodDiscoverer(httpd, secret) 453 454 return discoverySim{ 455 td: disc, 456 wantTargetGroups: []model.TargetGroup{ 457 preparePodTargetGroupWithEnv(httpd, data), 458 }, 459 } 460 }, 461 } 462 463 for name, createSim := range tests { 464 t.Run(name, func(t *testing.T) { 465 sim := createSim() 466 sim.run(t) 467 }) 468 } 469 } 470 471 func prepareAllNsPodDiscoverer(objects ...runtime.Object) (*KubeDiscoverer, kubernetes.Interface) { 472 return prepareDiscoverer("pod", []string{corev1.NamespaceAll}, objects...) 473 } 474 475 func preparePodDiscoverer(namespaces []string, objects ...runtime.Object) (*KubeDiscoverer, kubernetes.Interface) { 476 return prepareDiscoverer("pod", namespaces, objects...) 477 } 478 479 func mangleContainers(containers []corev1.Container, mange func(container *corev1.Container)) { 480 for i := range containers { 481 mange(&containers[i]) 482 } 483 } 484 485 var controllerTrue = true 486 487 func newHTTPDPod() *corev1.Pod { 488 return &corev1.Pod{ 489 ObjectMeta: metav1.ObjectMeta{ 490 Name: "httpd-dd95c4d68-5bkwl", 491 Namespace: "default", 492 UID: "1cebb6eb-0c1e-495b-8131-8fa3e6668dc8", 493 Annotations: map[string]string{"phase": "prod"}, 494 Labels: map[string]string{"app": "httpd", "tier": "frontend"}, 495 OwnerReferences: []metav1.OwnerReference{ 496 {Name: "netdata-test", Kind: "DaemonSet", Controller: &controllerTrue}, 497 }, 498 }, 499 Spec: corev1.PodSpec{ 500 NodeName: "m01", 501 Containers: []corev1.Container{ 502 { 503 Name: "httpd", 504 Image: "httpd", 505 Ports: []corev1.ContainerPort{ 506 {Name: "http", Protocol: corev1.ProtocolTCP, ContainerPort: 80}, 507 {Name: "https", Protocol: corev1.ProtocolTCP, ContainerPort: 443}, 508 }, 509 }, 510 }, 511 }, 512 Status: corev1.PodStatus{ 513 PodIP: "172.17.0.1", 514 }, 515 } 516 } 517 518 func newNGINXPod() *corev1.Pod { 519 return &corev1.Pod{ 520 ObjectMeta: metav1.ObjectMeta{ 521 Name: "nginx-7cfd77469b-q6kxj", 522 Namespace: "default", 523 UID: "09e883f2-d740-4c5f-970d-02cf02876522", 524 Annotations: map[string]string{"phase": "prod"}, 525 Labels: map[string]string{"app": "nginx", "tier": "frontend"}, 526 OwnerReferences: []metav1.OwnerReference{ 527 {Name: "netdata-test", Kind: "DaemonSet", Controller: &controllerTrue}, 528 }, 529 }, 530 Spec: corev1.PodSpec{ 531 NodeName: "m01", 532 Containers: []corev1.Container{ 533 { 534 Name: "nginx", 535 Image: "nginx", 536 Ports: []corev1.ContainerPort{ 537 {Name: "http", Protocol: corev1.ProtocolTCP, ContainerPort: 80}, 538 {Name: "https", Protocol: corev1.ProtocolTCP, ContainerPort: 443}, 539 }, 540 }, 541 }, 542 }, 543 Status: corev1.PodStatus{ 544 PodIP: "172.17.0.2", 545 }, 546 } 547 } 548 549 func prepareConfigMap(name string, data map[string]string) *corev1.ConfigMap { 550 return &corev1.ConfigMap{ 551 ObjectMeta: metav1.ObjectMeta{ 552 Name: name, 553 Namespace: "default", 554 UID: types.UID("a03b8dc6-dc40-46dc-b571-5030e69d8167" + name), 555 }, 556 Data: data, 557 } 558 } 559 560 func prepareSecret(name string, data map[string]string) *corev1.Secret { 561 secretData := make(map[string][]byte, len(data)) 562 for k, v := range data { 563 secretData[k] = []byte(v) 564 } 565 return &corev1.Secret{ 566 ObjectMeta: metav1.ObjectMeta{ 567 Name: name, 568 Namespace: "default", 569 UID: types.UID("a03b8dc6-dc40-46dc-b571-5030e69d8161" + name), 570 }, 571 Data: secretData, 572 } 573 } 574 575 func prepareEmptyPodTargetGroup(pod *corev1.Pod) *podTargetGroup { 576 return &podTargetGroup{source: podSource(pod)} 577 } 578 579 func preparePodTargetGroup(pod *corev1.Pod) *podTargetGroup { 580 tgg := prepareEmptyPodTargetGroup(pod) 581 582 for _, container := range pod.Spec.Containers { 583 for _, port := range container.Ports { 584 portNum := strconv.FormatUint(uint64(port.ContainerPort), 10) 585 tgt := &PodTarget{ 586 tuid: podTUIDWithPort(pod, container, port), 587 Address: net.JoinHostPort(pod.Status.PodIP, portNum), 588 Namespace: pod.Namespace, 589 Name: pod.Name, 590 Annotations: mapAny(pod.Annotations), 591 Labels: mapAny(pod.Labels), 592 NodeName: pod.Spec.NodeName, 593 PodIP: pod.Status.PodIP, 594 ControllerName: "netdata-test", 595 ControllerKind: "DaemonSet", 596 ContName: container.Name, 597 Image: container.Image, 598 Env: nil, 599 Port: portNum, 600 PortName: port.Name, 601 PortProtocol: string(port.Protocol), 602 } 603 tgt.hash = mustCalcHash(tgt) 604 tgt.Tags().Merge(discoveryTags) 605 606 tgg.targets = append(tgg.targets, tgt) 607 } 608 } 609 610 return tgg 611 } 612 613 func preparePodTargetGroupWithEnv(pod *corev1.Pod, env map[string]string) *podTargetGroup { 614 tgg := preparePodTargetGroup(pod) 615 616 for _, tgt := range tgg.Targets() { 617 tgt.(*PodTarget).Env = mapAny(env) 618 tgt.(*PodTarget).hash = mustCalcHash(tgt) 619 } 620 621 return tgg 622 }