master
go 649 lines 16.8 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package k8ssd
4
5 import (
6 "context"
7 "net"
8 "strconv"
9 "testing"
10 "time"
11
12 "github.com/netdata/netdata/go/plugins/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 "discoverer=k8s,kind=pod,namespace=default,pod_name=httpd-dd95c4d68-5bkwl,test=test",
48 "discoverer=k8s,kind=pod,namespace=default,pod_name=nginx-7cfd77469b-q6kxj,test=test",
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 12448367577781070857,
123 14383147416909666398,
124 15844848658936368667,
125 10371342506191352910,
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]struct {
227 createSim func() discoverySim
228 }{
229 "ADD: pods exist before run": {
230 createSim: func() discoverySim {
231 httpd, nginx := newHTTPDPod(), newNGINXPod()
232 td, _ := prepareAllNsPodDiscoverer(httpd, nginx)
233
234 return discoverySim{
235 td: td,
236 wantTargetGroups: []model.TargetGroup{
237 preparePodTargetGroup(httpd),
238 preparePodTargetGroup(nginx),
239 },
240 }
241 },
242 },
243 "ADD: pods exist before run and add after sync": {
244 createSim: func() discoverySim {
245 httpd, nginx := newHTTPDPod(), newNGINXPod()
246 disc, client := prepareAllNsPodDiscoverer(httpd)
247 podClient := client.CoreV1().Pods("default")
248
249 return discoverySim{
250 td: disc,
251 runAfterSync: func(ctx context.Context) {
252 _, _ = podClient.Create(ctx, nginx, metav1.CreateOptions{})
253 },
254 wantTargetGroups: []model.TargetGroup{
255 preparePodTargetGroup(httpd),
256 preparePodTargetGroup(nginx),
257 },
258 }
259 },
260 },
261 "DELETE: remove pods after sync": {
262 createSim: func() discoverySim {
263 httpd, nginx := newHTTPDPod(), newNGINXPod()
264 disc, client := prepareAllNsPodDiscoverer(httpd, nginx)
265 podClient := client.CoreV1().Pods("default")
266
267 return discoverySim{
268 td: disc,
269 runAfterSync: func(ctx context.Context) {
270 time.Sleep(time.Millisecond * 50)
271 _ = podClient.Delete(ctx, httpd.Name, metav1.DeleteOptions{})
272 _ = podClient.Delete(ctx, nginx.Name, metav1.DeleteOptions{})
273 },
274 wantTargetGroups: []model.TargetGroup{
275 preparePodTargetGroup(httpd),
276 preparePodTargetGroup(nginx),
277 prepareEmptyPodTargetGroup(httpd),
278 prepareEmptyPodTargetGroup(nginx),
279 },
280 }
281 },
282 },
283 "DELETE,ADD: remove and add pods after sync": {
284 createSim: func() discoverySim {
285 httpd, nginx := newHTTPDPod(), newNGINXPod()
286 disc, client := prepareAllNsPodDiscoverer(httpd)
287 podClient := client.CoreV1().Pods("default")
288
289 return discoverySim{
290 td: disc,
291 runAfterSync: func(ctx context.Context) {
292 time.Sleep(time.Millisecond * 50)
293 _ = podClient.Delete(ctx, httpd.Name, metav1.DeleteOptions{})
294 _, _ = podClient.Create(ctx, nginx, metav1.CreateOptions{})
295 },
296 wantTargetGroups: []model.TargetGroup{
297 preparePodTargetGroup(httpd),
298 prepareEmptyPodTargetGroup(httpd),
299 preparePodTargetGroup(nginx),
300 },
301 }
302 },
303 },
304 "ADD: pods with empty PodIP": {
305 createSim: func() discoverySim {
306 httpd, nginx := newHTTPDPod(), newNGINXPod()
307 httpd.Status.PodIP = ""
308 nginx.Status.PodIP = ""
309 disc, _ := prepareAllNsPodDiscoverer(httpd, nginx)
310
311 return discoverySim{
312 td: disc,
313 wantTargetGroups: []model.TargetGroup{
314 prepareEmptyPodTargetGroup(httpd),
315 prepareEmptyPodTargetGroup(nginx),
316 },
317 }
318 },
319 },
320 "UPDATE: set pods PodIP after sync": {
321 createSim: func() discoverySim {
322 httpd, nginx := newHTTPDPod(), newNGINXPod()
323 httpd.Status.PodIP = ""
324 nginx.Status.PodIP = ""
325 disc, client := prepareAllNsPodDiscoverer(httpd, nginx)
326 podClient := client.CoreV1().Pods("default")
327
328 return discoverySim{
329 td: disc,
330 runAfterSync: func(ctx context.Context) {
331 time.Sleep(time.Millisecond * 50)
332 _, _ = podClient.Update(ctx, newHTTPDPod(), metav1.UpdateOptions{})
333 _, _ = podClient.Update(ctx, newNGINXPod(), metav1.UpdateOptions{})
334 },
335 wantTargetGroups: []model.TargetGroup{
336 prepareEmptyPodTargetGroup(httpd),
337 prepareEmptyPodTargetGroup(nginx),
338 preparePodTargetGroup(newHTTPDPod()),
339 preparePodTargetGroup(newNGINXPod()),
340 },
341 }
342 },
343 },
344 "ADD: pods without containers": {
345 createSim: func() discoverySim {
346 httpd, nginx := newHTTPDPod(), newNGINXPod()
347 httpd.Spec.Containers = httpd.Spec.Containers[:0]
348 nginx.Spec.Containers = httpd.Spec.Containers[:0]
349 disc, _ := prepareAllNsPodDiscoverer(httpd, nginx)
350
351 return discoverySim{
352 td: disc,
353 wantTargetGroups: []model.TargetGroup{
354 prepareEmptyPodTargetGroup(httpd),
355 prepareEmptyPodTargetGroup(nginx),
356 },
357 }
358 },
359 },
360 "Env: from value": {
361 createSim: func() discoverySim {
362 httpd := newHTTPDPod()
363 mangle := func(c *corev1.Container) {
364 c.Env = []corev1.EnvVar{
365 {Name: "key1", Value: "value1"},
366 }
367 }
368 mangleContainers(httpd.Spec.Containers, mangle)
369 data := map[string]string{"key1": "value1"}
370
371 disc, _ := prepareAllNsPodDiscoverer(httpd)
372
373 return discoverySim{
374 td: disc,
375 wantTargetGroups: []model.TargetGroup{
376 preparePodTargetGroupWithEnv(httpd, data),
377 },
378 }
379 },
380 },
381 "Env: from Secret": {
382 createSim: func() discoverySim {
383 httpd := newHTTPDPod()
384 mangle := func(c *corev1.Container) {
385 c.Env = []corev1.EnvVar{
386 {
387 Name: "key1",
388 ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{
389 LocalObjectReference: corev1.LocalObjectReference{Name: "my-secret"},
390 Key: "key1",
391 }},
392 },
393 }
394 }
395 mangleContainers(httpd.Spec.Containers, mangle)
396 data := map[string]string{"key1": "value1"}
397 secret := prepareSecret("my-secret", data)
398
399 disc, _ := prepareAllNsPodDiscoverer(httpd, secret)
400
401 return discoverySim{
402 td: disc,
403 wantTargetGroups: []model.TargetGroup{
404 preparePodTargetGroupWithEnv(httpd, data),
405 },
406 }
407 },
408 },
409 "Env: from ConfigMap": {
410 createSim: func() discoverySim {
411 httpd := newHTTPDPod()
412 mangle := func(c *corev1.Container) {
413 c.Env = []corev1.EnvVar{
414 {
415 Name: "key1",
416 ValueFrom: &corev1.EnvVarSource{ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
417 LocalObjectReference: corev1.LocalObjectReference{Name: "my-cmap"},
418 Key: "key1",
419 }},
420 },
421 }
422 }
423 mangleContainers(httpd.Spec.Containers, mangle)
424 data := map[string]string{"key1": "value1"}
425 cmap := prepareConfigMap("my-cmap", data)
426
427 disc, _ := prepareAllNsPodDiscoverer(httpd, cmap)
428
429 return discoverySim{
430 td: disc,
431 wantTargetGroups: []model.TargetGroup{
432 preparePodTargetGroupWithEnv(httpd, data),
433 },
434 }
435 },
436 },
437 "EnvFrom: from ConfigMap": {
438 createSim: func() discoverySim {
439 httpd := newHTTPDPod()
440 mangle := func(c *corev1.Container) {
441 c.EnvFrom = []corev1.EnvFromSource{
442 {
443 ConfigMapRef: &corev1.ConfigMapEnvSource{
444 LocalObjectReference: corev1.LocalObjectReference{Name: "my-cmap"}},
445 },
446 }
447 }
448 mangleContainers(httpd.Spec.Containers, mangle)
449 data := map[string]string{"key1": "value1", "key2": "value2"}
450 cmap := prepareConfigMap("my-cmap", data)
451
452 disc, _ := prepareAllNsPodDiscoverer(httpd, cmap)
453
454 return discoverySim{
455 td: disc,
456 wantTargetGroups: []model.TargetGroup{
457 preparePodTargetGroupWithEnv(httpd, data),
458 },
459 }
460 },
461 },
462 "EnvFrom: from Secret": {
463 createSim: func() discoverySim {
464 httpd := newHTTPDPod()
465 mangle := func(c *corev1.Container) {
466 c.EnvFrom = []corev1.EnvFromSource{
467 {
468 SecretRef: &corev1.SecretEnvSource{
469 LocalObjectReference: corev1.LocalObjectReference{Name: "my-secret"}},
470 },
471 }
472 }
473 mangleContainers(httpd.Spec.Containers, mangle)
474 data := map[string]string{"key1": "value1", "key2": "value2"}
475 secret := prepareSecret("my-secret", data)
476
477 disc, _ := prepareAllNsPodDiscoverer(httpd, secret)
478
479 return discoverySim{
480 td: disc,
481 wantTargetGroups: []model.TargetGroup{
482 preparePodTargetGroupWithEnv(httpd, data),
483 },
484 }
485 },
486 },
487 }
488
489 for name, test := range tests {
490 t.Run(name, func(t *testing.T) {
491 sim := test.createSim()
492 sim.run(t)
493 })
494 }
495 }
496
497 func prepareAllNsPodDiscoverer(objects ...runtime.Object) (*KubeDiscoverer, kubernetes.Interface) {
498 return prepareDiscoverer(rolePod, []string{corev1.NamespaceAll}, objects...)
499 }
500
501 func preparePodDiscoverer(namespaces []string, objects ...runtime.Object) (*KubeDiscoverer, kubernetes.Interface) {
502 return prepareDiscoverer(rolePod, namespaces, objects...)
503 }
504
505 func mangleContainers(containers []corev1.Container, mange func(container *corev1.Container)) {
506 for i := range containers {
507 mange(&containers[i])
508 }
509 }
510
511 var controllerTrue = true
512
513 func newHTTPDPod() *corev1.Pod {
514 return &corev1.Pod{
515 ObjectMeta: metav1.ObjectMeta{
516 Name: "httpd-dd95c4d68-5bkwl",
517 Namespace: "default",
518 UID: "1cebb6eb-0c1e-495b-8131-8fa3e6668dc8",
519 Annotations: map[string]string{"phase": "prod"},
520 Labels: map[string]string{"app": "httpd", "tier": "frontend"},
521 OwnerReferences: []metav1.OwnerReference{
522 {Name: "netdata-test", Kind: "DaemonSet", Controller: &controllerTrue},
523 },
524 },
525 Spec: corev1.PodSpec{
526 NodeName: "m01",
527 Containers: []corev1.Container{
528 {
529 Name: "httpd",
530 Image: "httpd",
531 Ports: []corev1.ContainerPort{
532 {Name: "http", Protocol: corev1.ProtocolTCP, ContainerPort: 80},
533 {Name: "https", Protocol: corev1.ProtocolTCP, ContainerPort: 443},
534 },
535 },
536 },
537 },
538 Status: corev1.PodStatus{
539 PodIP: "172.17.0.1",
540 },
541 }
542 }
543
544 func newNGINXPod() *corev1.Pod {
545 return &corev1.Pod{
546 ObjectMeta: metav1.ObjectMeta{
547 Name: "nginx-7cfd77469b-q6kxj",
548 Namespace: "default",
549 UID: "09e883f2-d740-4c5f-970d-02cf02876522",
550 Annotations: map[string]string{"phase": "prod"},
551 Labels: map[string]string{"app": "nginx", "tier": "frontend"},
552 OwnerReferences: []metav1.OwnerReference{
553 {Name: "netdata-test", Kind: "DaemonSet", Controller: &controllerTrue},
554 },
555 },
556 Spec: corev1.PodSpec{
557 NodeName: "m01",
558 Containers: []corev1.Container{
559 {
560 Name: "nginx",
561 Image: "nginx",
562 Ports: []corev1.ContainerPort{
563 {Name: "http", Protocol: corev1.ProtocolTCP, ContainerPort: 80},
564 {Name: "https", Protocol: corev1.ProtocolTCP, ContainerPort: 443},
565 },
566 },
567 },
568 },
569 Status: corev1.PodStatus{
570 PodIP: "172.17.0.2",
571 },
572 }
573 }
574
575 func prepareConfigMap(name string, data map[string]string) *corev1.ConfigMap {
576 return &corev1.ConfigMap{
577 ObjectMeta: metav1.ObjectMeta{
578 Name: name,
579 Namespace: "default",
580 UID: types.UID("a03b8dc6-dc40-46dc-b571-5030e69d8167" + name),
581 },
582 Data: data,
583 }
584 }
585
586 func prepareSecret(name string, data map[string]string) *corev1.Secret {
587 secretData := make(map[string][]byte, len(data))
588 for k, v := range data {
589 secretData[k] = []byte(v)
590 }
591 return &corev1.Secret{
592 ObjectMeta: metav1.ObjectMeta{
593 Name: name,
594 Namespace: "default",
595 UID: types.UID("a03b8dc6-dc40-46dc-b571-5030e69d8161" + name),
596 },
597 Data: secretData,
598 }
599 }
600
601 func prepareEmptyPodTargetGroup(pod *corev1.Pod) *podTargetGroup {
602 tgg := &podTargetGroup{source: podSource(pod)}
603 tgg.source += ",test=test"
604 return tgg
605 }
606
607 func preparePodTargetGroup(pod *corev1.Pod) *podTargetGroup {
608 tgg := prepareEmptyPodTargetGroup(pod)
609
610 for _, container := range pod.Spec.Containers {
611 for _, port := range container.Ports {
612 portNum := strconv.FormatUint(uint64(port.ContainerPort), 10)
613 tgt := &PodTarget{
614 tuid: podTUIDWithPort(pod, container, port),
615 Address: net.JoinHostPort(pod.Status.PodIP, portNum),
616 Namespace: pod.Namespace,
617 Name: pod.Name,
618 Annotations: model.MapAny(pod.Annotations),
619 Labels: model.MapAny(pod.Labels),
620 NodeName: pod.Spec.NodeName,
621 PodIP: pod.Status.PodIP,
622 ControllerName: "netdata-test",
623 ControllerKind: "DaemonSet",
624 ContName: container.Name,
625 Image: container.Image,
626 Env: nil,
627 Port: portNum,
628 PortName: port.Name,
629 PortProtocol: string(port.Protocol),
630 }
631 tgt.hash = mustCalcHash(tgt)
632
633 tgg.targets = append(tgg.targets, tgt)
634 }
635 }
636
637 return tgg
638 }
639
640 func preparePodTargetGroupWithEnv(pod *corev1.Pod, env map[string]string) *podTargetGroup {
641 tgg := preparePodTargetGroup(pod)
642
643 for _, tgt := range tgg.Targets() {
644 tgt.(*PodTarget).Env = model.MapAny(env)
645 tgt.(*PodTarget).hash = mustCalcHash(tgt)
646 }
647
648 return tgg
649 }