github.com/netdata/go.d.plugin@v0.58.1/agent/discovery/sd/kubernetes/service_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/client-go/kubernetes" 19 "k8s.io/client-go/tools/cache" 20 ) 21 22 func TestServiceTargetGroup_Provider(t *testing.T) { 23 var s serviceTargetGroup 24 assert.NotEmpty(t, s.Provider()) 25 } 26 27 func TestServiceTargetGroup_Source(t *testing.T) { 28 tests := map[string]struct { 29 createSim func() discoverySim 30 wantSources []string 31 }{ 32 "ClusterIP svc with multiple ports": { 33 createSim: func() discoverySim { 34 httpd, nginx := newHTTPDClusterIPService(), newNGINXClusterIPService() 35 disc, _ := prepareAllNsSvcDiscoverer(httpd, nginx) 36 37 return discoverySim{ 38 td: disc, 39 wantTargetGroups: []model.TargetGroup{ 40 prepareSvcTargetGroup(httpd), 41 prepareSvcTargetGroup(nginx), 42 }, 43 } 44 }, 45 wantSources: []string{ 46 "sd:k8s:service(default/httpd-cluster-ip-service)", 47 "sd:k8s:service(default/nginx-cluster-ip-service)", 48 }, 49 }, 50 } 51 52 for name, test := range tests { 53 t.Run(name, func(t *testing.T) { 54 sim := test.createSim() 55 56 var sources []string 57 for _, tgg := range sim.run(t) { 58 sources = append(sources, tgg.Source()) 59 } 60 61 assert.Equal(t, test.wantSources, sources) 62 }) 63 } 64 } 65 66 func TestServiceTargetGroup_Targets(t *testing.T) { 67 tests := map[string]struct { 68 createSim func() discoverySim 69 wantTargets int 70 }{ 71 "ClusterIP svc with multiple ports": { 72 createSim: func() discoverySim { 73 httpd, nginx := newHTTPDClusterIPService(), newNGINXClusterIPService() 74 disc, _ := prepareAllNsSvcDiscoverer(httpd, nginx) 75 76 return discoverySim{ 77 td: disc, 78 wantTargetGroups: []model.TargetGroup{ 79 prepareSvcTargetGroup(httpd), 80 prepareSvcTargetGroup(nginx), 81 }, 82 } 83 }, 84 wantTargets: 4, 85 }, 86 } 87 88 for name, test := range tests { 89 t.Run(name, func(t *testing.T) { 90 sim := test.createSim() 91 92 var targets int 93 for _, tgg := range sim.run(t) { 94 targets += len(tgg.Targets()) 95 } 96 97 assert.Equal(t, test.wantTargets, targets) 98 }) 99 } 100 } 101 102 func TestServiceTarget_Hash(t *testing.T) { 103 tests := map[string]struct { 104 createSim func() discoverySim 105 wantHashes []uint64 106 }{ 107 "ClusterIP svc with multiple ports": { 108 createSim: func() discoverySim { 109 httpd, nginx := newHTTPDClusterIPService(), newNGINXClusterIPService() 110 disc, _ := prepareAllNsSvcDiscoverer(httpd, nginx) 111 112 return discoverySim{ 113 td: disc, 114 wantTargetGroups: []model.TargetGroup{ 115 prepareSvcTargetGroup(httpd), 116 prepareSvcTargetGroup(nginx), 117 }, 118 } 119 }, 120 wantHashes: []uint64{ 121 17611803477081780974, 122 6019985892433421258, 123 4151907287549842238, 124 5757608926096186119, 125 }, 126 }, 127 } 128 129 for name, test := range tests { 130 t.Run(name, func(t *testing.T) { 131 sim := test.createSim() 132 133 var hashes []uint64 134 for _, tgg := range sim.run(t) { 135 for _, tgt := range tgg.Targets() { 136 hashes = append(hashes, tgt.Hash()) 137 } 138 } 139 140 assert.Equal(t, test.wantHashes, hashes) 141 }) 142 } 143 } 144 145 func TestServiceTarget_TUID(t *testing.T) { 146 tests := map[string]struct { 147 createSim func() discoverySim 148 wantTUID []string 149 }{ 150 "ClusterIP svc with multiple ports": { 151 createSim: func() discoverySim { 152 httpd, nginx := newHTTPDClusterIPService(), newNGINXClusterIPService() 153 disc, _ := prepareAllNsSvcDiscoverer(httpd, nginx) 154 155 return discoverySim{ 156 td: disc, 157 wantTargetGroups: []model.TargetGroup{ 158 prepareSvcTargetGroup(httpd), 159 prepareSvcTargetGroup(nginx), 160 }, 161 } 162 }, 163 wantTUID: []string{ 164 "default_httpd-cluster-ip-service_tcp_80", 165 "default_httpd-cluster-ip-service_tcp_443", 166 "default_nginx-cluster-ip-service_tcp_80", 167 "default_nginx-cluster-ip-service_tcp_443", 168 }, 169 }, 170 } 171 172 for name, test := range tests { 173 t.Run(name, func(t *testing.T) { 174 sim := test.createSim() 175 176 var tuid []string 177 for _, tgg := range sim.run(t) { 178 for _, tgt := range tgg.Targets() { 179 tuid = append(tuid, tgt.TUID()) 180 } 181 } 182 183 assert.Equal(t, test.wantTUID, tuid) 184 }) 185 } 186 } 187 188 func TestNewServiceDiscoverer(t *testing.T) { 189 tests := map[string]struct { 190 informer cache.SharedInformer 191 wantPanic bool 192 }{ 193 "valid informer": { 194 wantPanic: false, 195 informer: cache.NewSharedInformer(nil, &corev1.Service{}, resyncPeriod), 196 }, 197 "nil informer": { 198 wantPanic: true, 199 informer: nil, 200 }, 201 } 202 203 for name, test := range tests { 204 t.Run(name, func(t *testing.T) { 205 f := func() { newServiceDiscoverer(test.informer) } 206 207 if test.wantPanic { 208 assert.Panics(t, f) 209 } else { 210 assert.NotPanics(t, f) 211 } 212 }) 213 } 214 } 215 216 func TestServiceDiscoverer_String(t *testing.T) { 217 var s serviceDiscoverer 218 assert.NotEmpty(t, s.String()) 219 } 220 221 func TestServiceDiscoverer_Discover(t *testing.T) { 222 tests := map[string]func() discoverySim{ 223 "ADD: ClusterIP svc exist before run": func() discoverySim { 224 httpd, nginx := newHTTPDClusterIPService(), newNGINXClusterIPService() 225 disc, _ := prepareAllNsSvcDiscoverer(httpd, nginx) 226 227 return discoverySim{ 228 td: disc, 229 wantTargetGroups: []model.TargetGroup{ 230 prepareSvcTargetGroup(httpd), 231 prepareSvcTargetGroup(nginx), 232 }, 233 } 234 }, 235 "ADD: ClusterIP svc exist before run and add after sync": func() discoverySim { 236 httpd, nginx := newHTTPDClusterIPService(), newNGINXClusterIPService() 237 disc, client := prepareAllNsSvcDiscoverer(httpd) 238 svcClient := client.CoreV1().Services("default") 239 240 return discoverySim{ 241 td: disc, 242 runAfterSync: func(ctx context.Context) { 243 _, _ = svcClient.Create(ctx, nginx, metav1.CreateOptions{}) 244 }, 245 wantTargetGroups: []model.TargetGroup{ 246 prepareSvcTargetGroup(httpd), 247 prepareSvcTargetGroup(nginx), 248 }, 249 } 250 }, 251 "DELETE: ClusterIP svc remove after sync": func() discoverySim { 252 httpd, nginx := newHTTPDClusterIPService(), newNGINXClusterIPService() 253 disc, client := prepareAllNsSvcDiscoverer(httpd, nginx) 254 svcClient := client.CoreV1().Services("default") 255 256 return discoverySim{ 257 td: disc, 258 runAfterSync: func(ctx context.Context) { 259 time.Sleep(time.Millisecond * 50) 260 _ = svcClient.Delete(ctx, httpd.Name, metav1.DeleteOptions{}) 261 _ = svcClient.Delete(ctx, nginx.Name, metav1.DeleteOptions{}) 262 }, 263 wantTargetGroups: []model.TargetGroup{ 264 prepareSvcTargetGroup(httpd), 265 prepareSvcTargetGroup(nginx), 266 prepareEmptySvcTargetGroup(httpd), 267 prepareEmptySvcTargetGroup(nginx), 268 }, 269 } 270 }, 271 "ADD,DELETE: ClusterIP svc remove and add after sync": func() discoverySim { 272 httpd, nginx := newHTTPDClusterIPService(), newNGINXClusterIPService() 273 disc, client := prepareAllNsSvcDiscoverer(httpd) 274 svcClient := client.CoreV1().Services("default") 275 276 return discoverySim{ 277 td: disc, 278 runAfterSync: func(ctx context.Context) { 279 time.Sleep(time.Millisecond * 50) 280 _ = svcClient.Delete(ctx, httpd.Name, metav1.DeleteOptions{}) 281 _, _ = svcClient.Create(ctx, nginx, metav1.CreateOptions{}) 282 }, 283 wantTargetGroups: []model.TargetGroup{ 284 prepareSvcTargetGroup(httpd), 285 prepareEmptySvcTargetGroup(httpd), 286 prepareSvcTargetGroup(nginx), 287 }, 288 } 289 }, 290 "ADD: Headless svc exist before run": func() discoverySim { 291 httpd, nginx := newHTTPDHeadlessService(), newNGINXHeadlessService() 292 disc, _ := prepareAllNsSvcDiscoverer(httpd, nginx) 293 294 return discoverySim{ 295 td: disc, 296 wantTargetGroups: []model.TargetGroup{ 297 prepareEmptySvcTargetGroup(httpd), 298 prepareEmptySvcTargetGroup(nginx), 299 }, 300 } 301 }, 302 "UPDATE: Headless => ClusterIP svc after sync": func() discoverySim { 303 httpd, nginx := newHTTPDHeadlessService(), newNGINXHeadlessService() 304 httpdUpd, nginxUpd := *httpd, *nginx 305 httpdUpd.Spec.ClusterIP = "10.100.0.1" 306 nginxUpd.Spec.ClusterIP = "10.100.0.2" 307 disc, client := prepareAllNsSvcDiscoverer(httpd, nginx) 308 svcClient := client.CoreV1().Services("default") 309 310 return discoverySim{ 311 td: disc, 312 runAfterSync: func(ctx context.Context) { 313 time.Sleep(time.Millisecond * 50) 314 _, _ = svcClient.Update(ctx, &httpdUpd, metav1.UpdateOptions{}) 315 _, _ = svcClient.Update(ctx, &nginxUpd, metav1.UpdateOptions{}) 316 }, 317 wantTargetGroups: []model.TargetGroup{ 318 prepareEmptySvcTargetGroup(httpd), 319 prepareEmptySvcTargetGroup(nginx), 320 prepareSvcTargetGroup(&httpdUpd), 321 prepareSvcTargetGroup(&nginxUpd), 322 }, 323 } 324 }, 325 "ADD: ClusterIP svc with zero exposed ports": func() discoverySim { 326 httpd, nginx := newHTTPDClusterIPService(), newNGINXClusterIPService() 327 httpd.Spec.Ports = httpd.Spec.Ports[:0] 328 nginx.Spec.Ports = httpd.Spec.Ports[:0] 329 disc, _ := prepareAllNsSvcDiscoverer(httpd, nginx) 330 331 return discoverySim{ 332 td: disc, 333 wantTargetGroups: []model.TargetGroup{ 334 prepareEmptySvcTargetGroup(httpd), 335 prepareEmptySvcTargetGroup(nginx), 336 }, 337 } 338 }, 339 } 340 341 for name, createSim := range tests { 342 t.Run(name, func(t *testing.T) { 343 sim := createSim() 344 sim.run(t) 345 }) 346 } 347 } 348 349 func prepareAllNsSvcDiscoverer(objects ...runtime.Object) (*KubeDiscoverer, kubernetes.Interface) { 350 return prepareDiscoverer("svc", []string{corev1.NamespaceAll}, objects...) 351 } 352 353 func prepareSvcDiscoverer(namespaces []string, objects ...runtime.Object) (*KubeDiscoverer, kubernetes.Interface) { 354 return prepareDiscoverer("svc", namespaces, objects...) 355 } 356 357 func newHTTPDClusterIPService() *corev1.Service { 358 return &corev1.Service{ 359 ObjectMeta: metav1.ObjectMeta{ 360 Name: "httpd-cluster-ip-service", 361 Namespace: "default", 362 Annotations: map[string]string{"phase": "prod"}, 363 Labels: map[string]string{"app": "httpd", "tier": "frontend"}, 364 }, 365 Spec: corev1.ServiceSpec{ 366 Ports: []corev1.ServicePort{ 367 {Name: "http", Protocol: corev1.ProtocolTCP, Port: 80}, 368 {Name: "https", Protocol: corev1.ProtocolTCP, Port: 443}, 369 }, 370 Type: corev1.ServiceTypeClusterIP, 371 ClusterIP: "10.100.0.1", 372 Selector: map[string]string{"app": "httpd", "tier": "frontend"}, 373 }, 374 } 375 } 376 377 func newNGINXClusterIPService() *corev1.Service { 378 return &corev1.Service{ 379 ObjectMeta: metav1.ObjectMeta{ 380 Name: "nginx-cluster-ip-service", 381 Namespace: "default", 382 Annotations: map[string]string{"phase": "prod"}, 383 Labels: map[string]string{"app": "nginx", "tier": "frontend"}, 384 }, 385 Spec: corev1.ServiceSpec{ 386 Ports: []corev1.ServicePort{ 387 {Name: "http", Protocol: corev1.ProtocolTCP, Port: 80}, 388 {Name: "https", Protocol: corev1.ProtocolTCP, Port: 443}, 389 }, 390 Type: corev1.ServiceTypeClusterIP, 391 ClusterIP: "10.100.0.2", 392 Selector: map[string]string{"app": "nginx", "tier": "frontend"}, 393 }, 394 } 395 } 396 397 func newHTTPDHeadlessService() *corev1.Service { 398 svc := newHTTPDClusterIPService() 399 svc.Name = "httpd-headless-service" 400 svc.Spec.ClusterIP = "" 401 return svc 402 } 403 404 func newNGINXHeadlessService() *corev1.Service { 405 svc := newNGINXClusterIPService() 406 svc.Name = "nginx-headless-service" 407 svc.Spec.ClusterIP = "" 408 return svc 409 } 410 411 func prepareEmptySvcTargetGroup(svc *corev1.Service) *serviceTargetGroup { 412 return &serviceTargetGroup{source: serviceSource(svc)} 413 } 414 415 func prepareSvcTargetGroup(svc *corev1.Service) *serviceTargetGroup { 416 tgg := prepareEmptySvcTargetGroup(svc) 417 418 for _, port := range svc.Spec.Ports { 419 portNum := strconv.FormatInt(int64(port.Port), 10) 420 tgt := &ServiceTarget{ 421 tuid: serviceTUID(svc, port), 422 Address: net.JoinHostPort(svc.Name+"."+svc.Namespace+".svc", portNum), 423 Namespace: svc.Namespace, 424 Name: svc.Name, 425 Annotations: mapAny(svc.Annotations), 426 Labels: mapAny(svc.Labels), 427 Port: portNum, 428 PortName: port.Name, 429 PortProtocol: string(port.Protocol), 430 ClusterIP: svc.Spec.ClusterIP, 431 ExternalName: svc.Spec.ExternalName, 432 Type: string(svc.Spec.Type), 433 } 434 tgt.hash = mustCalcHash(tgt) 435 tgt.Tags().Merge(discoveryTags) 436 tgg.targets = append(tgg.targets, tgt) 437 } 438 439 return tgg 440 }