github.com/netdata/go.d.plugin@v0.58.1/agent/discovery/sd/kubernetes/pod.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 podTargetGroup struct {
    21  	targets []model.Target
    22  	source  string
    23  }
    24  
    25  func (p podTargetGroup) Provider() string        { return "sd:k8s:pod" }
    26  func (p podTargetGroup) Source() string          { return fmt.Sprintf("%s(%s)", p.Provider(), p.source) }
    27  func (p podTargetGroup) Targets() []model.Target { return p.targets }
    28  
    29  type PodTarget 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  	NodeName       string
    41  	PodIP          string
    42  	ControllerName string
    43  	ControllerKind string
    44  	ContName       string
    45  	Image          string
    46  	Env            map[string]any
    47  	Port           string
    48  	PortName       string
    49  	PortProtocol   string
    50  }
    51  
    52  func (p PodTarget) Hash() uint64 { return p.hash }
    53  func (p PodTarget) TUID() string { return p.tuid }
    54  
    55  func newPodDiscoverer(pod, cmap, secret cache.SharedInformer) *podDiscoverer {
    56  
    57  	if pod == nil || cmap == nil || secret == nil {
    58  		panic("nil pod or cmap or secret informer")
    59  	}
    60  
    61  	queue := workqueue.NewWithConfig(workqueue.QueueConfig{Name: "pod"})
    62  
    63  	_, _ = pod.AddEventHandler(cache.ResourceEventHandlerFuncs{
    64  		AddFunc:    func(obj any) { enqueue(queue, obj) },
    65  		UpdateFunc: func(_, obj any) { enqueue(queue, obj) },
    66  		DeleteFunc: func(obj any) { enqueue(queue, obj) },
    67  	})
    68  
    69  	return &podDiscoverer{
    70  		Logger:         log,
    71  		podInformer:    pod,
    72  		cmapInformer:   cmap,
    73  		secretInformer: secret,
    74  		queue:          queue,
    75  	}
    76  }
    77  
    78  type podDiscoverer struct {
    79  	*logger.Logger
    80  	model.Base
    81  
    82  	podInformer    cache.SharedInformer
    83  	cmapInformer   cache.SharedInformer
    84  	secretInformer cache.SharedInformer
    85  	queue          *workqueue.Type
    86  }
    87  
    88  func (p *podDiscoverer) String() string {
    89  	return "k8s pod"
    90  }
    91  
    92  func (p *podDiscoverer) Discover(ctx context.Context, in chan<- []model.TargetGroup) {
    93  	p.Info("instance is started")
    94  	defer p.Info("instance is stopped")
    95  	defer p.queue.ShutDown()
    96  
    97  	go p.podInformer.Run(ctx.Done())
    98  	go p.cmapInformer.Run(ctx.Done())
    99  	go p.secretInformer.Run(ctx.Done())
   100  
   101  	if !cache.WaitForCacheSync(ctx.Done(),
   102  		p.podInformer.HasSynced, p.cmapInformer.HasSynced, p.secretInformer.HasSynced) {
   103  		p.Error("failed to sync caches")
   104  		return
   105  	}
   106  
   107  	go p.run(ctx, in)
   108  
   109  	<-ctx.Done()
   110  }
   111  
   112  func (p *podDiscoverer) run(ctx context.Context, in chan<- []model.TargetGroup) {
   113  	for {
   114  		item, shutdown := p.queue.Get()
   115  		if shutdown {
   116  			return
   117  		}
   118  		p.handleQueueItem(ctx, in, item)
   119  	}
   120  }
   121  
   122  func (p *podDiscoverer) handleQueueItem(ctx context.Context, in chan<- []model.TargetGroup, item any) {
   123  	defer p.queue.Done(item)
   124  
   125  	key := item.(string)
   126  	namespace, name, err := cache.SplitMetaNamespaceKey(key)
   127  	if err != nil {
   128  		return
   129  	}
   130  
   131  	obj, ok, err := p.podInformer.GetStore().GetByKey(key)
   132  	if err != nil {
   133  		return
   134  	}
   135  
   136  	if !ok {
   137  		tgg := &podTargetGroup{source: podSourceFromNsName(namespace, name)}
   138  		send(ctx, in, tgg)
   139  		return
   140  	}
   141  
   142  	pod, err := toPod(obj)
   143  	if err != nil {
   144  		return
   145  	}
   146  
   147  	tgg := p.buildTargetGroup(pod)
   148  
   149  	for _, tgt := range tgg.Targets() {
   150  		tgt.Tags().Merge(p.Tags())
   151  	}
   152  
   153  	send(ctx, in, tgg)
   154  
   155  }
   156  
   157  func (p *podDiscoverer) buildTargetGroup(pod *corev1.Pod) model.TargetGroup {
   158  	if pod.Status.PodIP == "" || len(pod.Spec.Containers) == 0 {
   159  		return &podTargetGroup{
   160  			source: podSource(pod),
   161  		}
   162  	}
   163  	return &podTargetGroup{
   164  		source:  podSource(pod),
   165  		targets: p.buildTargets(pod),
   166  	}
   167  }
   168  
   169  func (p *podDiscoverer) buildTargets(pod *corev1.Pod) (targets []model.Target) {
   170  	var name, kind string
   171  	for _, ref := range pod.OwnerReferences {
   172  		if ref.Controller != nil && *ref.Controller {
   173  			name = ref.Name
   174  			kind = ref.Kind
   175  			break
   176  		}
   177  	}
   178  
   179  	for _, container := range pod.Spec.Containers {
   180  		env := p.collectEnv(pod.Namespace, container)
   181  
   182  		if len(container.Ports) == 0 {
   183  			tgt := &PodTarget{
   184  				tuid:           podTUID(pod, container),
   185  				Address:        pod.Status.PodIP,
   186  				Namespace:      pod.Namespace,
   187  				Name:           pod.Name,
   188  				Annotations:    mapAny(pod.Annotations),
   189  				Labels:         mapAny(pod.Labels),
   190  				NodeName:       pod.Spec.NodeName,
   191  				PodIP:          pod.Status.PodIP,
   192  				ControllerName: name,
   193  				ControllerKind: kind,
   194  				ContName:       container.Name,
   195  				Image:          container.Image,
   196  				Env:            mapAny(env),
   197  			}
   198  			hash, err := calcHash(tgt)
   199  			if err != nil {
   200  				continue
   201  			}
   202  			tgt.hash = hash
   203  
   204  			targets = append(targets, tgt)
   205  		} else {
   206  			for _, port := range container.Ports {
   207  				portNum := strconv.FormatUint(uint64(port.ContainerPort), 10)
   208  				tgt := &PodTarget{
   209  					tuid:           podTUIDWithPort(pod, container, port),
   210  					Address:        net.JoinHostPort(pod.Status.PodIP, portNum),
   211  					Namespace:      pod.Namespace,
   212  					Name:           pod.Name,
   213  					Annotations:    mapAny(pod.Annotations),
   214  					Labels:         mapAny(pod.Labels),
   215  					NodeName:       pod.Spec.NodeName,
   216  					PodIP:          pod.Status.PodIP,
   217  					ControllerName: name,
   218  					ControllerKind: kind,
   219  					ContName:       container.Name,
   220  					Image:          container.Image,
   221  					Env:            mapAny(env),
   222  					Port:           portNum,
   223  					PortName:       port.Name,
   224  					PortProtocol:   string(port.Protocol),
   225  				}
   226  				hash, err := calcHash(tgt)
   227  				if err != nil {
   228  					continue
   229  				}
   230  				tgt.hash = hash
   231  
   232  				targets = append(targets, tgt)
   233  			}
   234  		}
   235  	}
   236  
   237  	return targets
   238  }
   239  
   240  func (p *podDiscoverer) collectEnv(ns string, container corev1.Container) map[string]string {
   241  	vars := make(map[string]string)
   242  
   243  	// When a key exists in multiple sources,
   244  	// the value associated with the last source will take precedence.
   245  	// Values defined by an Env with a duplicate key will take precedence.
   246  	//
   247  	// Order (https://github.com/kubernetes/kubectl/blob/master/pkg/describe/describe.go)
   248  	// - envFrom: configMapRef, secretRef
   249  	// - env: value || valueFrom: fieldRef, resourceFieldRef, secretRef, configMap
   250  
   251  	for _, src := range container.EnvFrom {
   252  		switch {
   253  		case src.ConfigMapRef != nil:
   254  			p.envFromConfigMap(vars, ns, src)
   255  		case src.SecretRef != nil:
   256  			p.envFromSecret(vars, ns, src)
   257  		}
   258  	}
   259  
   260  	for _, env := range container.Env {
   261  		if env.Name == "" || isVar(env.Name) {
   262  			continue
   263  		}
   264  		switch {
   265  		case env.Value != "":
   266  			vars[env.Name] = env.Value
   267  		case env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil:
   268  			p.valueFromSecret(vars, ns, env)
   269  		case env.ValueFrom != nil && env.ValueFrom.ConfigMapKeyRef != nil:
   270  			p.valueFromConfigMap(vars, ns, env)
   271  		}
   272  	}
   273  
   274  	if len(vars) == 0 {
   275  		return nil
   276  	}
   277  	return vars
   278  }
   279  
   280  func (p *podDiscoverer) valueFromConfigMap(vars map[string]string, ns string, env corev1.EnvVar) {
   281  	if env.ValueFrom.ConfigMapKeyRef.Name == "" || env.ValueFrom.ConfigMapKeyRef.Key == "" {
   282  		return
   283  	}
   284  
   285  	sr := env.ValueFrom.ConfigMapKeyRef
   286  	key := ns + "/" + sr.Name
   287  
   288  	item, exist, err := p.cmapInformer.GetStore().GetByKey(key)
   289  	if err != nil || !exist {
   290  		return
   291  	}
   292  
   293  	cmap, err := toConfigMap(item)
   294  	if err != nil {
   295  		return
   296  	}
   297  
   298  	if v, ok := cmap.Data[sr.Key]; ok {
   299  		vars[env.Name] = v
   300  	}
   301  }
   302  
   303  func (p *podDiscoverer) valueFromSecret(vars map[string]string, ns string, env corev1.EnvVar) {
   304  	if env.ValueFrom.SecretKeyRef.Name == "" || env.ValueFrom.SecretKeyRef.Key == "" {
   305  		return
   306  	}
   307  
   308  	secretKey := env.ValueFrom.SecretKeyRef
   309  	key := ns + "/" + secretKey.Name
   310  
   311  	item, exist, err := p.secretInformer.GetStore().GetByKey(key)
   312  	if err != nil || !exist {
   313  		return
   314  	}
   315  
   316  	secret, err := toSecret(item)
   317  	if err != nil {
   318  		return
   319  	}
   320  
   321  	if v, ok := secret.Data[secretKey.Key]; ok {
   322  		vars[env.Name] = string(v)
   323  	}
   324  }
   325  
   326  func (p *podDiscoverer) envFromConfigMap(vars map[string]string, ns string, src corev1.EnvFromSource) {
   327  	if src.ConfigMapRef.Name == "" {
   328  		return
   329  	}
   330  
   331  	key := ns + "/" + src.ConfigMapRef.Name
   332  	item, exist, err := p.cmapInformer.GetStore().GetByKey(key)
   333  	if err != nil || !exist {
   334  		return
   335  	}
   336  
   337  	cmap, err := toConfigMap(item)
   338  	if err != nil {
   339  		return
   340  	}
   341  
   342  	for k, v := range cmap.Data {
   343  		vars[src.Prefix+k] = v
   344  	}
   345  }
   346  
   347  func (p *podDiscoverer) envFromSecret(vars map[string]string, ns string, src corev1.EnvFromSource) {
   348  	if src.SecretRef.Name == "" {
   349  		return
   350  	}
   351  
   352  	key := ns + "/" + src.SecretRef.Name
   353  	item, exist, err := p.secretInformer.GetStore().GetByKey(key)
   354  	if err != nil || !exist {
   355  		return
   356  	}
   357  
   358  	secret, err := toSecret(item)
   359  	if err != nil {
   360  		return
   361  	}
   362  
   363  	for k, v := range secret.Data {
   364  		vars[src.Prefix+k] = string(v)
   365  	}
   366  }
   367  
   368  func podTUID(pod *corev1.Pod, container corev1.Container) string {
   369  	return fmt.Sprintf("%s_%s_%s",
   370  		pod.Namespace,
   371  		pod.Name,
   372  		container.Name,
   373  	)
   374  }
   375  
   376  func podTUIDWithPort(pod *corev1.Pod, container corev1.Container, port corev1.ContainerPort) string {
   377  	return fmt.Sprintf("%s_%s_%s_%s_%s",
   378  		pod.Namespace,
   379  		pod.Name,
   380  		container.Name,
   381  		strings.ToLower(string(port.Protocol)),
   382  		strconv.FormatUint(uint64(port.ContainerPort), 10),
   383  	)
   384  }
   385  
   386  func podSourceFromNsName(namespace, name string) string {
   387  	return namespace + "/" + name
   388  }
   389  
   390  func podSource(pod *corev1.Pod) string {
   391  	return podSourceFromNsName(pod.Namespace, pod.Name)
   392  }
   393  
   394  func toPod(obj any) (*corev1.Pod, error) {
   395  	pod, ok := obj.(*corev1.Pod)
   396  	if !ok {
   397  		return nil, fmt.Errorf("received unexpected object type: %T", obj)
   398  	}
   399  	return pod, nil
   400  }
   401  
   402  func toConfigMap(obj any) (*corev1.ConfigMap, error) {
   403  	cmap, ok := obj.(*corev1.ConfigMap)
   404  	if !ok {
   405  		return nil, fmt.Errorf("received unexpected object type: %T", obj)
   406  	}
   407  	return cmap, nil
   408  }
   409  
   410  func toSecret(obj any) (*corev1.Secret, error) {
   411  	secret, ok := obj.(*corev1.Secret)
   412  	if !ok {
   413  		return nil, fmt.Errorf("received unexpected object type: %T", obj)
   414  	}
   415  	return secret, nil
   416  }
   417  
   418  func isVar(name string) bool {
   419  	// Variable references $(VAR_NAME) are expanded using the previous defined
   420  	// environment variables in the container and any service environment
   421  	// variables.
   422  	return strings.IndexByte(name, '$') != -1
   423  }
   424  
   425  func mapAny(src map[string]string) map[string]any {
   426  	if src == nil {
   427  		return nil
   428  	}
   429  	m := make(map[string]any, len(src))
   430  	for k, v := range src {
   431  		m[k] = v
   432  	}
   433  	return m
   434  }