github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/scrape/discovery/kubernetes/endpoints.go (about)

     1  // Copyright 2016 The Prometheus Authors
     2  // Copyright 2021 The Pyroscope Authors
     3  //
     4  // Licensed under the Apache License, Version 2.0 (the "License");
     5  // you may not use this file except in compliance with the License.
     6  // You may obtain a copy of the License at
     7  //
     8  // http://www.apache.org/licenses/LICENSE-2.0
     9  //
    10  // Unless required by applicable law or agreed to in writing, software
    11  // distributed under the License is distributed on an "AS IS" BASIS,
    12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  // See the License for the specific language governing permissions and
    14  // limitations under the License.
    15  
    16  package kubernetes
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"net"
    22  	"strconv"
    23  
    24  	"github.com/sirupsen/logrus"
    25  	apiv1 "k8s.io/api/core/v1"
    26  	"k8s.io/client-go/tools/cache"
    27  	"k8s.io/client-go/util/workqueue"
    28  
    29  	"github.com/pyroscope-io/pyroscope/pkg/scrape/discovery/targetgroup"
    30  	"github.com/pyroscope-io/pyroscope/pkg/scrape/model"
    31  )
    32  
    33  // revive:disable:cognitive-complexity preserve original implementation
    34  
    35  var (
    36  	epAddCount    = eventCount.WithLabelValues("endpoints", "add")
    37  	epUpdateCount = eventCount.WithLabelValues("endpoints", "update")
    38  	epDeleteCount = eventCount.WithLabelValues("endpoints", "delete")
    39  )
    40  
    41  // Endpoints discovers new endpoint targets.
    42  type Endpoints struct {
    43  	logger logrus.FieldLogger
    44  
    45  	endpointsInf cache.SharedInformer
    46  	serviceInf   cache.SharedInformer
    47  	podInf       cache.SharedInformer
    48  
    49  	podStore       cache.Store
    50  	endpointsStore cache.Store
    51  	serviceStore   cache.Store
    52  
    53  	queue *workqueue.Type
    54  }
    55  
    56  // NewEndpoints returns a new endpoints discovery.
    57  func NewEndpoints(l logrus.FieldLogger, svc, eps, pod cache.SharedInformer) *Endpoints {
    58  	e := &Endpoints{
    59  		logger:         l,
    60  		endpointsInf:   eps,
    61  		endpointsStore: eps.GetStore(),
    62  		serviceInf:     svc,
    63  		serviceStore:   svc.GetStore(),
    64  		podInf:         pod,
    65  		podStore:       pod.GetStore(),
    66  		queue:          workqueue.NewNamed("endpoints"),
    67  	}
    68  
    69  	e.endpointsInf.AddEventHandler(cache.ResourceEventHandlerFuncs{
    70  		AddFunc: func(o interface{}) {
    71  			epAddCount.Inc()
    72  			e.enqueue(o)
    73  		},
    74  		UpdateFunc: func(_, o interface{}) {
    75  			epUpdateCount.Inc()
    76  			e.enqueue(o)
    77  		},
    78  		DeleteFunc: func(o interface{}) {
    79  			epDeleteCount.Inc()
    80  			e.enqueue(o)
    81  		},
    82  	})
    83  
    84  	serviceUpdate := func(o interface{}) {
    85  		svc, err := convertToService(o)
    86  		if err != nil {
    87  			e.logger.WithError(err).Error("converting to Service object failed")
    88  			return
    89  		}
    90  
    91  		ep := &apiv1.Endpoints{}
    92  		ep.Namespace = svc.Namespace
    93  		ep.Name = svc.Name
    94  		obj, exists, err := e.endpointsStore.Get(ep)
    95  		if exists && err == nil {
    96  			e.enqueue(obj.(*apiv1.Endpoints))
    97  		}
    98  
    99  		if err != nil {
   100  			e.logger.WithError(err).Error("retrieving endpoints failed")
   101  		}
   102  	}
   103  	e.serviceInf.AddEventHandler(cache.ResourceEventHandlerFuncs{
   104  		// TODO(fabxc): potentially remove add and delete event handlers. Those should
   105  		// be triggered via the endpoint handlers already.
   106  		AddFunc: func(o interface{}) {
   107  			svcAddCount.Inc()
   108  			serviceUpdate(o)
   109  		},
   110  		UpdateFunc: func(_, o interface{}) {
   111  			svcUpdateCount.Inc()
   112  			serviceUpdate(o)
   113  		},
   114  		DeleteFunc: func(o interface{}) {
   115  			svcDeleteCount.Inc()
   116  			serviceUpdate(o)
   117  		},
   118  	})
   119  
   120  	return e
   121  }
   122  
   123  func (e *Endpoints) enqueue(obj interface{}) {
   124  	key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
   125  	if err != nil {
   126  		return
   127  	}
   128  
   129  	e.queue.Add(key)
   130  }
   131  
   132  // Run implements the Discoverer interface.
   133  func (e *Endpoints) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
   134  	defer e.queue.ShutDown()
   135  
   136  	if !cache.WaitForCacheSync(ctx.Done(), e.endpointsInf.HasSynced, e.serviceInf.HasSynced, e.podInf.HasSynced) {
   137  		if ctx.Err() != context.Canceled {
   138  			e.logger.Error("endpoints informer unable to sync cache")
   139  		}
   140  		return
   141  	}
   142  
   143  	go func() {
   144  		for {
   145  			if !e.process(ctx, ch) {
   146  				return
   147  			}
   148  		}
   149  	}()
   150  
   151  	// Block until the target provider is explicitly canceled.
   152  	<-ctx.Done()
   153  }
   154  
   155  func (e *Endpoints) process(ctx context.Context, ch chan<- []*targetgroup.Group) bool {
   156  	keyObj, quit := e.queue.Get()
   157  	if quit {
   158  		return false
   159  	}
   160  	defer e.queue.Done(keyObj)
   161  	key := keyObj.(string)
   162  	logger := e.logger.WithField("key", key)
   163  	namespace, name, err := cache.SplitMetaNamespaceKey(key)
   164  	if err != nil {
   165  		logger.WithError(err).Error("splitting key")
   166  		return true
   167  	}
   168  
   169  	o, exists, err := e.endpointsStore.GetByKey(key)
   170  	if err != nil {
   171  		logger.WithError(err).Error("getting object from store")
   172  		return true
   173  	}
   174  	if !exists {
   175  		send(ctx, ch, &targetgroup.Group{Source: endpointsSourceFromNamespaceAndName(namespace, name)})
   176  		return true
   177  	}
   178  	eps, err := convertToEndpoints(o)
   179  	if err != nil {
   180  		logger.WithError(err).Error("converting to Endpoints object")
   181  		return true
   182  	}
   183  	send(ctx, ch, e.buildEndpoints(eps))
   184  	return true
   185  }
   186  
   187  func convertToEndpoints(o interface{}) (*apiv1.Endpoints, error) {
   188  	endpoints, ok := o.(*apiv1.Endpoints)
   189  	if ok {
   190  		return endpoints, nil
   191  	}
   192  
   193  	return nil, fmt.Errorf("received unexpected object: %v", o)
   194  }
   195  
   196  func endpointsSource(ep *apiv1.Endpoints) string {
   197  	return endpointsSourceFromNamespaceAndName(ep.Namespace, ep.Name)
   198  }
   199  
   200  func endpointsSourceFromNamespaceAndName(namespace, name string) string {
   201  	return "endpoints/" + namespace + "/" + name
   202  }
   203  
   204  const (
   205  	endpointsLabelPrefix           = metaLabelPrefix + "endpoints_label_"
   206  	endpointsLabelPresentPrefix    = metaLabelPrefix + "endpoints_labelpresent_"
   207  	endpointsNameLabel             = metaLabelPrefix + "endpoints_name"
   208  	endpointNodeName               = metaLabelPrefix + "endpoint_node_name"
   209  	endpointHostname               = metaLabelPrefix + "endpoint_hostname"
   210  	endpointReadyLabel             = metaLabelPrefix + "endpoint_ready"
   211  	endpointPortNameLabel          = metaLabelPrefix + "endpoint_port_name"
   212  	endpointPortProtocolLabel      = metaLabelPrefix + "endpoint_port_protocol"
   213  	endpointAddressTargetKindLabel = metaLabelPrefix + "endpoint_address_target_kind"
   214  	endpointAddressTargetNameLabel = metaLabelPrefix + "endpoint_address_target_name"
   215  )
   216  
   217  func (e *Endpoints) buildEndpoints(eps *apiv1.Endpoints) *targetgroup.Group {
   218  	tg := &targetgroup.Group{
   219  		Source: endpointsSource(eps),
   220  	}
   221  	tg.Labels = model.LabelSet{
   222  		namespaceLabel:     lv(eps.Namespace),
   223  		endpointsNameLabel: lv(eps.Name),
   224  	}
   225  	e.addServiceLabels(eps.Namespace, eps.Name, tg)
   226  	// Add endpoints labels metadata.
   227  	for k, v := range eps.Labels {
   228  		ln := sanitizeLabelName(k)
   229  		tg.Labels[model.LabelName(endpointsLabelPrefix+ln)] = lv(v)
   230  		tg.Labels[model.LabelName(endpointsLabelPresentPrefix+ln)] = presentValue
   231  	}
   232  
   233  	type podEntry struct {
   234  		pod          *apiv1.Pod
   235  		servicePorts []apiv1.EndpointPort
   236  	}
   237  	seenPods := map[string]*podEntry{}
   238  
   239  	add := func(addr apiv1.EndpointAddress, port apiv1.EndpointPort, ready string) {
   240  		a := net.JoinHostPort(addr.IP, strconv.FormatUint(uint64(port.Port), 10))
   241  
   242  		target := model.LabelSet{
   243  			model.AddressLabel:        lv(a),
   244  			endpointPortNameLabel:     lv(port.Name),
   245  			endpointPortProtocolLabel: lv(string(port.Protocol)),
   246  			endpointReadyLabel:        lv(ready),
   247  		}
   248  
   249  		if addr.TargetRef != nil {
   250  			target[model.LabelName(endpointAddressTargetKindLabel)] = lv(addr.TargetRef.Kind)
   251  			target[model.LabelName(endpointAddressTargetNameLabel)] = lv(addr.TargetRef.Name)
   252  		}
   253  
   254  		if addr.NodeName != nil {
   255  			target[model.LabelName(endpointNodeName)] = lv(*addr.NodeName)
   256  		}
   257  		if addr.Hostname != "" {
   258  			target[model.LabelName(endpointHostname)] = lv(addr.Hostname)
   259  		}
   260  
   261  		pod := e.resolvePodRef(addr.TargetRef)
   262  		if pod == nil {
   263  			// This target is not a Pod, so don't continue with Pod specific logic.
   264  			tg.Targets = append(tg.Targets, target)
   265  			return
   266  		}
   267  		s := pod.Namespace + "/" + pod.Name
   268  
   269  		sp, ok := seenPods[s]
   270  		if !ok {
   271  			sp = &podEntry{pod: pod}
   272  			seenPods[s] = sp
   273  		}
   274  
   275  		// Attach standard pod labels.
   276  		target = target.Merge(podLabels(pod))
   277  
   278  		// Attach potential container port labels matching the endpoint port.
   279  		for _, c := range pod.Spec.Containers {
   280  			for _, cport := range c.Ports {
   281  				if port.Port == cport.ContainerPort {
   282  					ports := strconv.FormatUint(uint64(port.Port), 10)
   283  
   284  					target[podContainerNameLabel] = lv(c.Name)
   285  					target[podContainerPortNameLabel] = lv(cport.Name)
   286  					target[podContainerPortNumberLabel] = lv(ports)
   287  					target[podContainerPortProtocolLabel] = lv(string(port.Protocol))
   288  					break
   289  				}
   290  			}
   291  		}
   292  
   293  		// Add service port so we know that we have already generated a target
   294  		// for it.
   295  		sp.servicePorts = append(sp.servicePorts, port)
   296  		tg.Targets = append(tg.Targets, target)
   297  	}
   298  
   299  	for _, ss := range eps.Subsets {
   300  		for _, port := range ss.Ports {
   301  			for _, addr := range ss.Addresses {
   302  				add(addr, port, "true")
   303  			}
   304  			// Although this generates the same target again, as it was generated in
   305  			// the loop above, it causes the ready meta label to be overridden.
   306  			for _, addr := range ss.NotReadyAddresses {
   307  				add(addr, port, "false")
   308  			}
   309  		}
   310  	}
   311  
   312  	v := eps.Labels[apiv1.EndpointsOverCapacity]
   313  	if v == "truncated" {
   314  		e.logger.WithField("endpoint", eps.Name).
   315  			Warn("number of endpoints in one Endpoints object exceeds 1000 and has been truncated, please use \"role: endpointslice\" instead")
   316  	}
   317  	if v == "warning" {
   318  		e.logger.WithField("endpoint", eps.Name).
   319  			Warn("number of endpoints in one Endpoints object exceeds 1000, please use \"role: endpointslice\" instead")
   320  	}
   321  
   322  	// For all seen pods, check all container ports. If they were not covered
   323  	// by one of the service endpoints, generate targets for them.
   324  	for _, pe := range seenPods {
   325  		for _, c := range pe.pod.Spec.Containers {
   326  			for _, cport := range c.Ports {
   327  				hasSeenPort := func() bool {
   328  					for _, eport := range pe.servicePorts {
   329  						if cport.ContainerPort == eport.Port {
   330  							return true
   331  						}
   332  					}
   333  					return false
   334  				}
   335  				if hasSeenPort() {
   336  					continue
   337  				}
   338  
   339  				a := net.JoinHostPort(pe.pod.Status.PodIP, strconv.FormatUint(uint64(cport.ContainerPort), 10))
   340  				ports := strconv.FormatUint(uint64(cport.ContainerPort), 10)
   341  
   342  				target := model.LabelSet{
   343  					model.AddressLabel:            lv(a),
   344  					podContainerNameLabel:         lv(c.Name),
   345  					podContainerPortNameLabel:     lv(cport.Name),
   346  					podContainerPortNumberLabel:   lv(ports),
   347  					podContainerPortProtocolLabel: lv(string(cport.Protocol)),
   348  				}
   349  				tg.Targets = append(tg.Targets, target.Merge(podLabels(pe.pod)))
   350  			}
   351  		}
   352  	}
   353  
   354  	return tg
   355  }
   356  
   357  func (e *Endpoints) resolvePodRef(ref *apiv1.ObjectReference) *apiv1.Pod {
   358  	if ref == nil || ref.Kind != "Pod" {
   359  		return nil
   360  	}
   361  	p := &apiv1.Pod{}
   362  	p.Namespace = ref.Namespace
   363  	p.Name = ref.Name
   364  
   365  	obj, exists, err := e.podStore.Get(p)
   366  	if err != nil {
   367  		e.logger.WithError(err).Error("resolving pod")
   368  		return nil
   369  	}
   370  	if !exists {
   371  		return nil
   372  	}
   373  	return obj.(*apiv1.Pod)
   374  }
   375  
   376  func (e *Endpoints) addServiceLabels(ns, name string, tg *targetgroup.Group) {
   377  	svc := &apiv1.Service{}
   378  	svc.Namespace = ns
   379  	svc.Name = name
   380  
   381  	obj, exists, err := e.serviceStore.Get(svc)
   382  	if err != nil {
   383  		e.logger.WithError(err).Error("retrieving service")
   384  		return
   385  	}
   386  	if !exists {
   387  		return
   388  	}
   389  	svc = obj.(*apiv1.Service)
   390  
   391  	tg.Labels = tg.Labels.Merge(serviceLabels(svc))
   392  }