github.com/netdata/go.d.plugin@v0.58.1/agent/discovery/sd/kubernetes/service.go (about)

     1  // SPDX-License-Identifier: GPL-3.0-or-later
     2  
     3  package kubernetes
     4  
     5  import (
     6  	"context"
     7  	"fmt"
     8  	"net"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/netdata/go.d.plugin/agent/discovery/sd/model"
    13  	"github.com/netdata/go.d.plugin/logger"
    14  
    15  	corev1 "k8s.io/api/core/v1"
    16  	"k8s.io/client-go/tools/cache"
    17  	"k8s.io/client-go/util/workqueue"
    18  )
    19  
    20  type serviceTargetGroup struct {
    21  	targets []model.Target
    22  	source  string
    23  }
    24  
    25  func (s serviceTargetGroup) Provider() string        { return "sd:k8s:service" }
    26  func (s serviceTargetGroup) Source() string          { return fmt.Sprintf("%s(%s)", s.Provider(), s.source) }
    27  func (s serviceTargetGroup) Targets() []model.Target { return s.targets }
    28  
    29  type ServiceTarget struct {
    30  	model.Base `hash:"ignore"`
    31  
    32  	hash uint64
    33  	tuid string
    34  
    35  	Address      string
    36  	Namespace    string
    37  	Name         string
    38  	Annotations  map[string]any
    39  	Labels       map[string]any
    40  	Port         string
    41  	PortName     string
    42  	PortProtocol string
    43  	ClusterIP    string
    44  	ExternalName string
    45  	Type         string
    46  }
    47  
    48  func (s ServiceTarget) Hash() uint64 { return s.hash }
    49  func (s ServiceTarget) TUID() string { return s.tuid }
    50  
    51  type serviceDiscoverer struct {
    52  	*logger.Logger
    53  	model.Base
    54  
    55  	informer cache.SharedInformer
    56  	queue    *workqueue.Type
    57  }
    58  
    59  func newServiceDiscoverer(inf cache.SharedInformer) *serviceDiscoverer {
    60  	if inf == nil {
    61  		panic("nil service informer")
    62  	}
    63  
    64  	queue := workqueue.NewWithConfig(workqueue.QueueConfig{Name: "service"})
    65  	_, _ = inf.AddEventHandler(cache.ResourceEventHandlerFuncs{
    66  		AddFunc:    func(obj any) { enqueue(queue, obj) },
    67  		UpdateFunc: func(_, obj any) { enqueue(queue, obj) },
    68  		DeleteFunc: func(obj any) { enqueue(queue, obj) },
    69  	})
    70  
    71  	return &serviceDiscoverer{
    72  		Logger:   log,
    73  		informer: inf,
    74  		queue:    queue,
    75  	}
    76  }
    77  
    78  func (s *serviceDiscoverer) String() string {
    79  	return "k8s service"
    80  }
    81  
    82  func (s *serviceDiscoverer) Discover(ctx context.Context, ch chan<- []model.TargetGroup) {
    83  	s.Info("instance is started")
    84  	defer s.Info("instance is stopped")
    85  	defer s.queue.ShutDown()
    86  
    87  	go s.informer.Run(ctx.Done())
    88  
    89  	if !cache.WaitForCacheSync(ctx.Done(), s.informer.HasSynced) {
    90  		s.Error("failed to sync caches")
    91  		return
    92  	}
    93  
    94  	go s.run(ctx, ch)
    95  
    96  	<-ctx.Done()
    97  }
    98  
    99  func (s *serviceDiscoverer) run(ctx context.Context, in chan<- []model.TargetGroup) {
   100  	for {
   101  		item, shutdown := s.queue.Get()
   102  		if shutdown {
   103  			return
   104  		}
   105  
   106  		s.handleQueueItem(ctx, in, item)
   107  	}
   108  }
   109  
   110  func (s *serviceDiscoverer) handleQueueItem(ctx context.Context, in chan<- []model.TargetGroup, item any) {
   111  	defer s.queue.Done(item)
   112  
   113  	key := item.(string)
   114  	namespace, name, err := cache.SplitMetaNamespaceKey(key)
   115  	if err != nil {
   116  		return
   117  	}
   118  
   119  	obj, exists, err := s.informer.GetStore().GetByKey(key)
   120  	if err != nil {
   121  		return
   122  	}
   123  
   124  	if !exists {
   125  		tgg := &serviceTargetGroup{source: serviceSourceFromNsName(namespace, name)}
   126  		send(ctx, in, tgg)
   127  		return
   128  	}
   129  
   130  	svc, err := toService(obj)
   131  	if err != nil {
   132  		return
   133  	}
   134  
   135  	tgg := s.buildTargetGroup(svc)
   136  
   137  	for _, tgt := range tgg.Targets() {
   138  		tgt.Tags().Merge(s.Tags())
   139  	}
   140  
   141  	send(ctx, in, tgg)
   142  }
   143  
   144  func (s *serviceDiscoverer) buildTargetGroup(svc *corev1.Service) model.TargetGroup {
   145  	// TODO: headless service?
   146  	if svc.Spec.ClusterIP == "" || len(svc.Spec.Ports) == 0 {
   147  		return &serviceTargetGroup{
   148  			source: serviceSource(svc),
   149  		}
   150  	}
   151  	return &serviceTargetGroup{
   152  		source:  serviceSource(svc),
   153  		targets: s.buildTargets(svc),
   154  	}
   155  }
   156  
   157  func (s *serviceDiscoverer) buildTargets(svc *corev1.Service) (targets []model.Target) {
   158  	for _, port := range svc.Spec.Ports {
   159  		portNum := strconv.FormatInt(int64(port.Port), 10)
   160  		tgt := &ServiceTarget{
   161  			tuid:         serviceTUID(svc, port),
   162  			Address:      net.JoinHostPort(svc.Name+"."+svc.Namespace+".svc", portNum),
   163  			Namespace:    svc.Namespace,
   164  			Name:         svc.Name,
   165  			Annotations:  mapAny(svc.Annotations),
   166  			Labels:       mapAny(svc.Labels),
   167  			Port:         portNum,
   168  			PortName:     port.Name,
   169  			PortProtocol: string(port.Protocol),
   170  			ClusterIP:    svc.Spec.ClusterIP,
   171  			ExternalName: svc.Spec.ExternalName,
   172  			Type:         string(svc.Spec.Type),
   173  		}
   174  		hash, err := calcHash(tgt)
   175  		if err != nil {
   176  			continue
   177  		}
   178  		tgt.hash = hash
   179  
   180  		targets = append(targets, tgt)
   181  	}
   182  
   183  	return targets
   184  }
   185  
   186  func serviceTUID(svc *corev1.Service, port corev1.ServicePort) string {
   187  	return fmt.Sprintf("%s_%s_%s_%s",
   188  		svc.Namespace,
   189  		svc.Name,
   190  		strings.ToLower(string(port.Protocol)),
   191  		strconv.FormatInt(int64(port.Port), 10),
   192  	)
   193  }
   194  
   195  func serviceSourceFromNsName(namespace, name string) string {
   196  	return namespace + "/" + name
   197  }
   198  
   199  func serviceSource(svc *corev1.Service) string {
   200  	return serviceSourceFromNsName(svc.Namespace, svc.Name)
   201  }
   202  
   203  func toService(obj any) (*corev1.Service, error) {
   204  	svc, ok := obj.(*corev1.Service)
   205  	if !ok {
   206  		return nil, fmt.Errorf("received unexpected object type: %T", obj)
   207  	}
   208  	return svc, nil
   209  }