github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/agentmap/discorvery.go (about)

     1  package agentmap
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"regexp"
     8  
     9  	appsv1 "k8s.io/api/apps/v1"
    10  	core "k8s.io/api/core/v1"
    11  	k8sErrors "k8s.io/apimachinery/pkg/api/errors"
    12  	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
    13  	"k8s.io/apimachinery/pkg/labels"
    14  	"k8s.io/apimachinery/pkg/util/intstr"
    15  
    16  	"github.com/datawire/dlib/dlog"
    17  	"github.com/datawire/k8sapi/pkg/k8sapi"
    18  	"github.com/telepresenceio/telepresence/v2/pkg/agentconfig"
    19  	"github.com/telepresenceio/telepresence/v2/pkg/informer"
    20  )
    21  
    22  var ReplicaSetNameRx = regexp.MustCompile(`\A(.+)-[a-f0-9]+\z`)
    23  
    24  func FindOwnerWorkload(ctx context.Context, obj k8sapi.Object) (k8sapi.Workload, error) {
    25  	dlog.Debugf(ctx, "FindOwnerWorkload(%s,%s,%s)", obj.GetName(), obj.GetNamespace(), obj.GetKind())
    26  	lbs := obj.GetLabels()
    27  	if wlName, ok := lbs[agentconfig.WorkloadNameLabel]; ok {
    28  		return GetWorkload(ctx, wlName, obj.GetNamespace(), lbs[agentconfig.WorkloadKindLabel])
    29  	}
    30  	refs := obj.GetOwnerReferences()
    31  	for i := range refs {
    32  		if or := &refs[i]; or.Controller != nil && *or.Controller {
    33  			if or.Kind == "ReplicaSet" {
    34  				// Try the common case first. Strip replicaset's generated hash and try to
    35  				// get the deployment. If this succeeds, we have saved us a replicaset
    36  				// lookup.
    37  				if m := ReplicaSetNameRx.FindStringSubmatch(or.Name); m != nil {
    38  					if wl, err := GetWorkload(ctx, m[1], obj.GetNamespace(), "Deployment"); err == nil {
    39  						return wl, nil
    40  					}
    41  				}
    42  			}
    43  			wl, err := GetWorkload(ctx, or.Name, obj.GetNamespace(), or.Kind)
    44  			if err != nil {
    45  				return nil, err
    46  			}
    47  			return FindOwnerWorkload(ctx, wl)
    48  		}
    49  	}
    50  	if wl, ok := obj.(k8sapi.Workload); ok {
    51  		return wl, nil
    52  	}
    53  	return nil, fmt.Errorf("unable to find workload owner for %s.%s", obj.GetName(), obj.GetNamespace())
    54  }
    55  
    56  func GetWorkload(ctx context.Context, name, namespace, workloadKind string) (obj k8sapi.Workload, err error) {
    57  	dlog.Debugf(ctx, "GetWorkload(%s,%s,%s)", name, namespace, workloadKind)
    58  	switch workloadKind {
    59  	case "Deployment":
    60  		obj, err = getDeployment(ctx, name, namespace)
    61  	case "ReplicaSet":
    62  		obj, err = k8sapi.GetReplicaSet(ctx, name, namespace)
    63  	case "StatefulSet":
    64  		obj, err = k8sapi.GetStatefulSet(ctx, name, namespace)
    65  	case "":
    66  		for _, wk := range []string{"Deployment", "ReplicaSet", "StatefulSet"} {
    67  			if obj, err = GetWorkload(ctx, name, namespace, wk); err == nil {
    68  				return obj, nil
    69  			}
    70  			if !k8sErrors.IsNotFound(err) {
    71  				return nil, err
    72  			}
    73  		}
    74  		err = k8sErrors.NewNotFound(core.Resource("workload"), name+"."+namespace)
    75  	default:
    76  		return nil, k8sapi.UnsupportedWorkloadKindError(workloadKind)
    77  	}
    78  	return obj, err
    79  }
    80  
    81  func getDeployment(ctx context.Context, name, namespace string) (obj k8sapi.Workload, err error) {
    82  	if f := informer.GetFactory(ctx, namespace); f != nil {
    83  		var dep *appsv1.Deployment
    84  		dep, err = f.Apps().V1().Deployments().Lister().Deployments(namespace).Get(name)
    85  		if err == nil {
    86  			obj = k8sapi.Deployment(dep)
    87  		}
    88  		return obj, err
    89  	}
    90  
    91  	// This shouldn't happen really.
    92  	dlog.Debugf(ctx, "fetching deployment %s.%s using direct API call", name, namespace)
    93  	return k8sapi.GetDeployment(ctx, name, namespace)
    94  }
    95  
    96  func findServicesForPod(ctx context.Context, pod *core.PodTemplateSpec, svcName string) ([]k8sapi.Object, error) {
    97  	switch {
    98  	case svcName != "":
    99  		var svc *core.Service
   100  		var err error
   101  		if f := informer.GetFactory(ctx, pod.Namespace); f != nil {
   102  			svc, err = f.Core().V1().Services().Lister().Services(pod.Namespace).Get(svcName)
   103  		} else {
   104  			// This shouldn't happen really.
   105  			dlog.Debugf(ctx, "fetching service %s.%s using direct API call", svcName, pod.Namespace)
   106  			svc, err = k8sapi.GetK8sInterface(ctx).CoreV1().Services(pod.Namespace).Get(ctx, svcName, meta.GetOptions{})
   107  		}
   108  		if err != nil {
   109  			if k8sErrors.IsNotFound(err) {
   110  				return nil, fmt.Errorf(
   111  					"unable to find service %s specified by annotation %s declared in pod %s.%s",
   112  					svcName, ServiceNameAnnotation, pod.Name, pod.Namespace)
   113  			}
   114  			return nil, err
   115  		}
   116  		return []k8sapi.Object{k8sapi.Service(svc)}, nil
   117  	case len(pod.Labels) > 0:
   118  		lbs := labels.Set(pod.Labels)
   119  		svcs, err := findServicesSelecting(ctx, pod.Namespace, lbs)
   120  		if err != nil {
   121  			return nil, err
   122  		}
   123  		if len(svcs) > 0 {
   124  			return svcs, nil
   125  		}
   126  		return nil, fmt.Errorf("unable to find services that selects pod %s.%s using labels %s", pod.Name, pod.Namespace, lbs)
   127  	default:
   128  		return nil, fmt.Errorf("unable to find a service using pod %s.%s because it has no labels", pod.Name, pod.Namespace)
   129  	}
   130  }
   131  
   132  type objectsStringer []k8sapi.Object
   133  
   134  func (os objectsStringer) String() string {
   135  	b := bytes.Buffer{}
   136  	l := len(os)
   137  	if l == 0 {
   138  		return "no services"
   139  	}
   140  	for i, o := range os {
   141  		if i > 0 {
   142  			if l != 2 {
   143  				b.WriteString(", ")
   144  			}
   145  			if i == l-1 {
   146  				b.WriteString(" and ")
   147  			}
   148  		}
   149  		b.WriteString(o.GetName())
   150  	}
   151  	return b.String()
   152  }
   153  
   154  // findServicesSelecting finds all services that has a selector that matches the given labels.
   155  func findServicesSelecting(ctx context.Context, namespace string, lbs labels.Labels) ([]k8sapi.Object, error) {
   156  	var ms []k8sapi.Object
   157  	var scanned int
   158  	if f := informer.GetFactory(ctx, namespace); f != nil {
   159  		ss, err := f.Core().V1().Services().Lister().Services(namespace).List(labels.Everything())
   160  		if err != nil {
   161  			return nil, err
   162  		}
   163  		scanned = len(ss)
   164  		for _, s := range ss {
   165  			sel := s.Spec.Selector
   166  			if len(sel) > 0 && labels.SelectorFromValidatedSet(sel).Matches(lbs) {
   167  				ms = append(ms, k8sapi.Service(s))
   168  			}
   169  		}
   170  	} else {
   171  		// This shouldn't happen really.
   172  		dlog.Debugf(ctx, "Fetching services in %s using direct API call", namespace)
   173  		l, err := k8sapi.GetK8sInterface(ctx).CoreV1().Services(namespace).List(ctx, meta.ListOptions{})
   174  		if err != nil {
   175  			return nil, err
   176  		}
   177  		items := l.Items
   178  		scanned = len(items)
   179  		for i := range items {
   180  			s := &items[i]
   181  			sel := s.Spec.Selector
   182  			if len(sel) > 0 && labels.SelectorFromValidatedSet(sel).Matches(lbs) {
   183  				ms = append(ms, k8sapi.Service(s))
   184  			}
   185  		}
   186  	}
   187  	dlog.Debugf(ctx, "Scanned %d services in namespace %s and found that %s selects labels %v", scanned, namespace, objectsStringer(ms), lbs)
   188  	return ms, nil
   189  }
   190  
   191  // findContainerMatchingPort finds the container that matches the given ServicePort. The match is
   192  // made using Protocol, and the Name or the ContainerPort field of each port in each container
   193  // depending on if  the service port is symbolic or numeric. The first container with a matching
   194  // port is returned along with the index of the container port that matched.
   195  //
   196  // The first container with no ports at all is returned together with a port index of -1, in case
   197  // no port match could be made and the service port is numeric. This enables intercepts of containers
   198  // that indeed do listen a port but lack a matching port description in the manifest, which is what
   199  // you get if you do:
   200  //
   201  //	kubectl create deploy my-deploy --image my-image
   202  //	kubectl expose deploy my-deploy --port 80 --target-port 8080
   203  func findContainerMatchingPort(port *core.ServicePort, cns []core.Container) (*core.Container, int) {
   204  	// The protocol of the targetPort must match the protocol of the containerPort because it is
   205  	// not illegal to listen with both TCP and UDP on the same port.
   206  	proto := core.ProtocolTCP
   207  	if port.Protocol != "" {
   208  		proto = port.Protocol
   209  	}
   210  	protoEqual := func(p core.Protocol) bool {
   211  		return p == proto || p == "" && proto == core.ProtocolTCP
   212  	}
   213  
   214  	if port.TargetPort.Type == intstr.String {
   215  		portName := port.TargetPort.StrVal
   216  		for ci := range cns {
   217  			cn := &cns[ci]
   218  			for pi := range cn.Ports {
   219  				p := &cn.Ports[pi]
   220  				if p.Name == portName && protoEqual(p.Protocol) {
   221  					return cn, pi
   222  				}
   223  			}
   224  		}
   225  	} else {
   226  		portNum := port.TargetPort.IntVal
   227  		if portNum == 0 {
   228  			// The targetPort default is the value of the port field.
   229  			portNum = port.Port
   230  		}
   231  		for ci := range cns {
   232  			cn := &cns[ci]
   233  			for pi := range cn.Ports {
   234  				p := &cn.Ports[pi]
   235  				if p.ContainerPort == portNum && protoEqual(p.Protocol) {
   236  					return cn, pi
   237  				}
   238  			}
   239  		}
   240  		// As a last resort, also consider containers that don't expose their ports at all. Those
   241  		// containers match all ports because it's unknown what they might be listening to.
   242  		for ci := range cns {
   243  			cn := &cns[ci]
   244  			if len(cn.Ports) == 0 {
   245  				return cn, -1
   246  			}
   247  		}
   248  	}
   249  	return nil, 0
   250  }
   251  
   252  // IsPodRunning returns true if at least one container has state Running and a non-zero StartedAt.
   253  func IsPodRunning(pod *core.Pod) bool {
   254  	for _, cn := range pod.Status.ContainerStatuses {
   255  		if r := cn.State.Running; r != nil && !r.StartedAt.IsZero() {
   256  			// At least one container is running.
   257  			return true
   258  		}
   259  	}
   260  	return false
   261  }
   262  
   263  // AgentContainer returns the pod's traffic-agent container, or nil if the pod doesn't have a traffic-agent.
   264  func AgentContainer(pod *core.Pod) *core.Container {
   265  	return containerByName(agentconfig.ContainerName, pod.Spec.Containers)
   266  }
   267  
   268  // InitContainer returns the pod's tel-agent-init init-container, or nil if the pod doesn't have a tel-agent-init.
   269  func InitContainer(pod *core.Pod) *core.Container {
   270  	return containerByName(agentconfig.InitContainerName, pod.Spec.InitContainers)
   271  }
   272  
   273  func containerByName(name string, cns []core.Container) *core.Container {
   274  	for i := range cns {
   275  		cn := &cns[i]
   276  		if cn.Name == name {
   277  			return cn
   278  		}
   279  	}
   280  	return nil
   281  }