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  }