github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/scrape/discovery/kubernetes/ingress.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  	"strings"
    22  
    23  	"github.com/sirupsen/logrus"
    24  	v1 "k8s.io/api/networking/v1"
    25  	"k8s.io/api/networking/v1beta1"
    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  var (
    34  	ingressAddCount    = eventCount.WithLabelValues("ingress", "add")
    35  	ingressUpdateCount = eventCount.WithLabelValues("ingress", "update")
    36  	ingressDeleteCount = eventCount.WithLabelValues("ingress", "delete")
    37  )
    38  
    39  // Ingress implements discovery of Kubernetes ingress.
    40  type Ingress struct {
    41  	logger   logrus.FieldLogger
    42  	informer cache.SharedInformer
    43  	store    cache.Store
    44  	queue    *workqueue.Type
    45  }
    46  
    47  // NewIngress returns a new ingress discovery.
    48  func NewIngress(l logrus.FieldLogger, inf cache.SharedInformer) *Ingress {
    49  	s := &Ingress{logger: l, informer: inf, store: inf.GetStore(), queue: workqueue.NewNamed("ingress")}
    50  	s.informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
    51  		AddFunc: func(o interface{}) {
    52  			ingressAddCount.Inc()
    53  			s.enqueue(o)
    54  		},
    55  		DeleteFunc: func(o interface{}) {
    56  			ingressDeleteCount.Inc()
    57  			s.enqueue(o)
    58  		},
    59  		UpdateFunc: func(_, o interface{}) {
    60  			ingressUpdateCount.Inc()
    61  			s.enqueue(o)
    62  		},
    63  	})
    64  	return s
    65  }
    66  
    67  func (i *Ingress) enqueue(obj interface{}) {
    68  	key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
    69  	if err != nil {
    70  		return
    71  	}
    72  
    73  	i.queue.Add(key)
    74  }
    75  
    76  // Run implements the Discoverer interface.
    77  func (i *Ingress) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
    78  	defer i.queue.ShutDown()
    79  
    80  	if !cache.WaitForCacheSync(ctx.Done(), i.informer.HasSynced) {
    81  		if ctx.Err() != context.Canceled {
    82  			i.logger.Error("ingress informer unable to sync cache")
    83  		}
    84  		return
    85  	}
    86  
    87  	go func() {
    88  		for {
    89  			if !i.process(ctx, ch) {
    90  				return
    91  			}
    92  		}
    93  	}()
    94  
    95  	// Block until the target provider is explicitly canceled.
    96  	<-ctx.Done()
    97  }
    98  
    99  func (i *Ingress) process(ctx context.Context, ch chan<- []*targetgroup.Group) bool {
   100  	keyObj, quit := i.queue.Get()
   101  	if quit {
   102  		return false
   103  	}
   104  	defer i.queue.Done(keyObj)
   105  	key := keyObj.(string)
   106  
   107  	namespace, name, err := cache.SplitMetaNamespaceKey(key)
   108  	if err != nil {
   109  		return true
   110  	}
   111  
   112  	o, exists, err := i.store.GetByKey(key)
   113  	if err != nil {
   114  		return true
   115  	}
   116  	if !exists {
   117  		send(ctx, ch, &targetgroup.Group{Source: ingressSourceFromNamespaceAndName(namespace, name)})
   118  		return true
   119  	}
   120  
   121  	var ia ingressAdaptor
   122  	switch ingress := o.(type) {
   123  	case *v1.Ingress:
   124  		ia = newIngressAdaptorFromV1(ingress)
   125  	case *v1beta1.Ingress:
   126  		ia = newIngressAdaptorFromV1beta1(ingress)
   127  	default:
   128  		i.logger.WithError(fmt.Errorf("received unexpected object: %v", o)).
   129  			Error("converting to Ingress object")
   130  		return true
   131  	}
   132  	send(ctx, ch, i.buildIngress(ia))
   133  	return true
   134  }
   135  
   136  func ingressSource(s ingressAdaptor) string {
   137  	return ingressSourceFromNamespaceAndName(s.namespace(), s.name())
   138  }
   139  
   140  func ingressSourceFromNamespaceAndName(namespace, name string) string {
   141  	return "ingress/" + namespace + "/" + name
   142  }
   143  
   144  const (
   145  	ingressNameLabel               = metaLabelPrefix + "ingress_name"
   146  	ingressLabelPrefix             = metaLabelPrefix + "ingress_label_"
   147  	ingressLabelPresentPrefix      = metaLabelPrefix + "ingress_labelpresent_"
   148  	ingressAnnotationPrefix        = metaLabelPrefix + "ingress_annotation_"
   149  	ingressAnnotationPresentPrefix = metaLabelPrefix + "ingress_annotationpresent_"
   150  	ingressSchemeLabel             = metaLabelPrefix + "ingress_scheme"
   151  	ingressHostLabel               = metaLabelPrefix + "ingress_host"
   152  	ingressPathLabel               = metaLabelPrefix + "ingress_path"
   153  	ingressClassNameLabel          = metaLabelPrefix + "ingress_class_name"
   154  )
   155  
   156  func ingressLabels(ingress ingressAdaptor) model.LabelSet {
   157  	// Each label and annotation will create two key-value pairs in the map.
   158  	ls := make(model.LabelSet, 2*(len(ingress.labels())+len(ingress.annotations()))+2)
   159  	ls[ingressNameLabel] = lv(ingress.name())
   160  	ls[namespaceLabel] = lv(ingress.namespace())
   161  	if cls := ingress.ingressClassName(); cls != nil {
   162  		ls[ingressClassNameLabel] = lv(*cls)
   163  	}
   164  
   165  	for k, v := range ingress.labels() {
   166  		ln := sanitizeLabelName(k)
   167  		ls[model.LabelName(ingressLabelPrefix+ln)] = lv(v)
   168  		ls[model.LabelName(ingressLabelPresentPrefix+ln)] = presentValue
   169  	}
   170  
   171  	for k, v := range ingress.annotations() {
   172  		ln := sanitizeLabelName(k)
   173  		ls[model.LabelName(ingressAnnotationPrefix+ln)] = lv(v)
   174  		ls[model.LabelName(ingressAnnotationPresentPrefix+ln)] = presentValue
   175  	}
   176  	return ls
   177  }
   178  
   179  func pathsFromIngressPaths(ingressPaths []string) []string {
   180  	if ingressPaths == nil {
   181  		return []string{"/"}
   182  	}
   183  	paths := make([]string, len(ingressPaths))
   184  	for n, p := range ingressPaths {
   185  		path := p
   186  		if p == "" {
   187  			path = "/"
   188  		}
   189  		paths[n] = path
   190  	}
   191  	return paths
   192  }
   193  
   194  func (*Ingress) buildIngress(ingress ingressAdaptor) *targetgroup.Group {
   195  	tg := &targetgroup.Group{
   196  		Source: ingressSource(ingress),
   197  	}
   198  	tg.Labels = ingressLabels(ingress)
   199  
   200  	for _, rule := range ingress.rules() {
   201  		scheme := "http"
   202  		paths := pathsFromIngressPaths(rule.paths())
   203  
   204  	out:
   205  		for _, pattern := range ingress.tlsHosts() {
   206  			if matchesHostnamePattern(pattern, rule.host()) {
   207  				scheme = "https"
   208  				break out
   209  			}
   210  		}
   211  
   212  		for _, path := range paths {
   213  			tg.Targets = append(tg.Targets, model.LabelSet{
   214  				model.AddressLabel: lv(rule.host()),
   215  				ingressSchemeLabel: lv(scheme),
   216  				ingressHostLabel:   lv(rule.host()),
   217  				ingressPathLabel:   lv(path),
   218  			})
   219  		}
   220  	}
   221  
   222  	return tg
   223  }
   224  
   225  // matchesHostnamePattern returns true if the host matches a wildcard DNS
   226  // pattern or pattern and host are equal.
   227  func matchesHostnamePattern(pattern, host string) bool {
   228  	if pattern == host {
   229  		return true
   230  	}
   231  
   232  	patternParts := strings.Split(pattern, ".")
   233  	hostParts := strings.Split(host, ".")
   234  
   235  	// If the first element of the pattern is not a wildcard, give up.
   236  	if len(patternParts) == 0 || patternParts[0] != "*" {
   237  		return false
   238  	}
   239  
   240  	// A wildcard match require the pattern to have the same length as the host
   241  	// path.
   242  	if len(patternParts) != len(hostParts) {
   243  		return false
   244  	}
   245  
   246  	for i := 1; i < len(patternParts); i++ {
   247  		if patternParts[i] != hostParts[i] {
   248  			return false
   249  		}
   250  	}
   251  
   252  	return true
   253  }