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

     1  package agentmap
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strconv"
     7  	"strings"
     8  
     9  	"go.opentelemetry.io/otel"
    10  	core "k8s.io/api/core/v1"
    11  	"k8s.io/apimachinery/pkg/util/intstr"
    12  	"k8s.io/apimachinery/pkg/util/validation"
    13  
    14  	"github.com/datawire/k8sapi/pkg/k8sapi"
    15  	"github.com/telepresenceio/telepresence/v2/pkg/agentconfig"
    16  	"github.com/telepresenceio/telepresence/v2/pkg/tracing"
    17  )
    18  
    19  const (
    20  	ServicePortAnnotation = agentconfig.DomainPrefix + "inject-service-port"
    21  	ServiceNameAnnotation = agentconfig.DomainPrefix + "inject-service-name"
    22  	ManagerAppName        = "traffic-manager"
    23  )
    24  
    25  type GeneratorConfig interface {
    26  	// Generate generates a configuration for the given workload. If replaceContainers is given it will be used to configure
    27  	// container replacement EXCEPT if existingConfig is not nil, in which replaceContainers will be
    28  	// ignored and the value from existingConfig used. 0 can be conventionally passed in as replaceContainers in this case.
    29  	Generate(
    30  		ctx context.Context,
    31  		wl k8sapi.Workload,
    32  		existingConfig agentconfig.SidecarExt,
    33  	) (sc agentconfig.SidecarExt, err error)
    34  }
    35  
    36  var GeneratorConfigFunc func(qualifiedAgentImage string) (GeneratorConfig, error) //nolint:gochecknoglobals // extension point
    37  
    38  type BasicGeneratorConfig struct {
    39  	ManagerPort         uint16
    40  	AgentPort           uint16
    41  	APIPort             uint16
    42  	TracingPort         uint16
    43  	QualifiedAgentImage string
    44  	ManagerNamespace    string
    45  	LogLevel            string
    46  	InitResources       *core.ResourceRequirements
    47  	Resources           *core.ResourceRequirements
    48  	PullPolicy          string
    49  	PullSecrets         []core.LocalObjectReference
    50  	AppProtocolStrategy k8sapi.AppProtocolStrategy
    51  	SecurityContext     *core.SecurityContext
    52  }
    53  
    54  func (cfg *BasicGeneratorConfig) Generate(
    55  	ctx context.Context,
    56  	wl k8sapi.Workload,
    57  	existingConfig agentconfig.SidecarExt,
    58  ) (sc agentconfig.SidecarExt, err error) {
    59  	ctx, span := otel.GetTracerProvider().Tracer("").Start(ctx, "agentmap.Generate")
    60  	defer tracing.EndAndRecord(span, err)
    61  
    62  	pod := wl.GetPodTemplate()
    63  	pod.Namespace = wl.GetNamespace()
    64  	cns := pod.Spec.Containers
    65  	for i := range cns {
    66  		cn := &cns[i]
    67  		if cn.Name == agentconfig.ContainerName {
    68  			continue
    69  		}
    70  		ports := cn.Ports
    71  		for pi := range ports {
    72  			if ports[pi].ContainerPort == int32(cfg.AgentPort) {
    73  				return nil, fmt.Errorf(
    74  					"the %s.%s pod container %s is exposing the same port (%d) as the %s sidecar",
    75  					pod.Name, pod.Namespace, cn.Name, cfg.AgentPort, agentconfig.ContainerName)
    76  			}
    77  		}
    78  	}
    79  
    80  	svcs, err := findServicesForPod(ctx, pod, pod.Annotations[ServiceNameAnnotation])
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  
    85  	var ccs []*agentconfig.Container
    86  	pns := make(map[int32]uint16)
    87  	portNumber := func(cnPort int32) uint16 {
    88  		if p, ok := pns[cnPort]; ok {
    89  			// Port already mapped. Reuse that mapping
    90  			return p
    91  		}
    92  		p := cfg.AgentPort + uint16(len(pns))
    93  		pns[cnPort] = p
    94  		return p
    95  	}
    96  
    97  	for _, svc := range svcs {
    98  		svcImpl, _ := k8sapi.ServiceImpl(svc)
    99  		if ccs, err = appendAgentContainerConfigs(ctx, svcImpl, pod, portNumber, ccs, existingConfig, cfg.AppProtocolStrategy); err != nil {
   100  			return nil, err
   101  		}
   102  	}
   103  	if len(ccs) == 0 {
   104  		return nil, fmt.Errorf("found no service with a port that matches a container in pod %s.%s", pod.Name, pod.Namespace)
   105  	}
   106  
   107  	ag := &agentconfig.Sidecar{
   108  		AgentImage:      cfg.QualifiedAgentImage,
   109  		AgentName:       wl.GetName(),
   110  		LogLevel:        cfg.LogLevel,
   111  		Namespace:       wl.GetNamespace(),
   112  		WorkloadName:    wl.GetName(),
   113  		WorkloadKind:    wl.GetKind(),
   114  		ManagerHost:     ManagerAppName + "." + cfg.ManagerNamespace,
   115  		ManagerPort:     cfg.ManagerPort,
   116  		APIPort:         cfg.APIPort,
   117  		TracingPort:     cfg.TracingPort,
   118  		Containers:      ccs,
   119  		InitResources:   cfg.InitResources,
   120  		Resources:       cfg.Resources,
   121  		PullPolicy:      cfg.PullPolicy,
   122  		PullSecrets:     cfg.PullSecrets,
   123  		SecurityContext: cfg.SecurityContext,
   124  	}
   125  	ag.RecordInSpan(span)
   126  	return ag, nil
   127  }
   128  
   129  func appendAgentContainerConfigs(
   130  	ctx context.Context,
   131  	svc *core.Service,
   132  	pod *core.PodTemplateSpec,
   133  	portNumber func(int32) uint16,
   134  	ccs []*agentconfig.Container,
   135  	existingConfig agentconfig.SidecarExt,
   136  	aps k8sapi.AppProtocolStrategy,
   137  ) ([]*agentconfig.Container, error) {
   138  	portNameOrNumber := pod.Annotations[ServicePortAnnotation]
   139  	ports, err := filterServicePorts(svc, portNameOrNumber)
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  	ignoredVolumeMounts := agentconfig.GetIgnoredVolumeMounts(pod.ObjectMeta.Annotations)
   144  nextSvcPort:
   145  	for _, port := range ports {
   146  		cn, i := findContainerMatchingPort(&port, pod.Spec.Containers)
   147  		if cn == nil || cn.Name == agentconfig.ContainerName {
   148  			continue
   149  		}
   150  		var appPort core.ContainerPort
   151  		if i < 0 {
   152  			// Can only happen if the service port is numeric, so it's safe to use TargetPort.IntVal here
   153  			appPort = core.ContainerPort{
   154  				Protocol:      port.Protocol,
   155  				ContainerPort: port.TargetPort.IntVal,
   156  			}
   157  		} else {
   158  			appPort = cn.Ports[i]
   159  		}
   160  
   161  		ic := &agentconfig.Intercept{
   162  			ServiceName:       svc.Name,
   163  			ServiceUID:        svc.UID,
   164  			ServicePortName:   port.Name,
   165  			ServicePort:       uint16(port.Port),
   166  			TargetPortNumeric: port.TargetPort.Type == intstr.Int,
   167  			Protocol:          port.Protocol,
   168  			AppProtocol:       k8sapi.GetAppProto(ctx, aps, &port),
   169  			AgentPort:         portNumber(appPort.ContainerPort),
   170  			ContainerPortName: appPort.Name,
   171  			ContainerPort:     uint16(appPort.ContainerPort),
   172  		}
   173  
   174  		// Validate that we're not being asked to clobber an existing configuration
   175  		var replaceContainer agentconfig.ReplacePolicy
   176  		if existingConfig != nil {
   177  			for _, cc := range existingConfig.AgentConfig().Containers {
   178  				if cc.Name == cn.Name {
   179  					replaceContainer = cc.Replace
   180  					break
   181  				}
   182  			}
   183  		}
   184  
   185  		// The container might already have intercepts declared
   186  		for _, cc := range ccs {
   187  			if cc.Name == cn.Name {
   188  				cc.Intercepts = append(cc.Intercepts, ic)
   189  				continue nextSvcPort
   190  			}
   191  		}
   192  		var mounts []string
   193  		if l := len(cn.VolumeMounts); l > 0 {
   194  			mounts = make([]string, 0, l)
   195  			for _, vm := range cn.VolumeMounts {
   196  				if !ignoredVolumeMounts.IsVolumeIgnored(vm.Name, vm.MountPath) {
   197  					mounts = append(mounts, vm.MountPath)
   198  				}
   199  			}
   200  		}
   201  		ccs = append(ccs, &agentconfig.Container{
   202  			Name:       cn.Name,
   203  			EnvPrefix:  CapsBase26(uint64(len(ccs))) + "_",
   204  			MountPoint: agentconfig.MountPrefixApp + "/" + cn.Name,
   205  			Mounts:     mounts,
   206  			Intercepts: []*agentconfig.Intercept{ic},
   207  			Replace:    replaceContainer,
   208  		})
   209  	}
   210  	return ccs, nil
   211  }
   212  
   213  // filterServicePorts iterates through a list of ports in a service and
   214  // only returns the ports that match the given nameOrNumber. All ports will
   215  // be returned if nameOrNumber is equal to the empty string.
   216  func filterServicePorts(svc *core.Service, nameOrNumber string) ([]core.ServicePort, error) {
   217  	ports := svc.Spec.Ports
   218  	if nameOrNumber == "" {
   219  		return ports, nil
   220  	}
   221  	svcPorts := make([]core.ServicePort, 0)
   222  	if number, err := strconv.Atoi(nameOrNumber); err != nil {
   223  		errs := validation.IsValidPortName(nameOrNumber)
   224  		if len(errs) > 0 {
   225  			return nil, fmt.Errorf(strings.Join(errs, "\n"))
   226  		}
   227  		for _, port := range ports {
   228  			if port.Name == nameOrNumber {
   229  				svcPorts = append(svcPorts, port)
   230  			}
   231  		}
   232  	} else {
   233  		for _, port := range ports {
   234  			pn := int32(0)
   235  			if port.TargetPort.Type == intstr.Int {
   236  				pn = port.TargetPort.IntVal
   237  			}
   238  			if pn == 0 {
   239  				pn = port.Port
   240  			}
   241  			if pn == int32(number) {
   242  				svcPorts = append(svcPorts, port)
   243  			}
   244  		}
   245  	}
   246  	return svcPorts, nil
   247  }