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 }