...

Source file src/github.com/redhat-developer/odo/pkg/dev/podmandev/pod.go

Documentation: github.com/redhat-developer/odo/pkg/dev/podmandev

     1  package podmandev
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math/rand" // #nosec
     7  	"sort"
     8  	"time"
     9  
    10  	"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
    11  	"github.com/devfile/library/v2/pkg/devfile/generator"
    12  	"github.com/devfile/library/v2/pkg/devfile/parser"
    13  	"github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common"
    14  
    15  	"github.com/redhat-developer/odo/pkg/api"
    16  	"github.com/redhat-developer/odo/pkg/component"
    17  	"github.com/redhat-developer/odo/pkg/dev/kubedev/utils"
    18  	"github.com/redhat-developer/odo/pkg/labels"
    19  	"github.com/redhat-developer/odo/pkg/libdevfile"
    20  	"github.com/redhat-developer/odo/pkg/odo/commonflags"
    21  	odocontext "github.com/redhat-developer/odo/pkg/odo/context"
    22  	"github.com/redhat-developer/odo/pkg/storage"
    23  	"github.com/redhat-developer/odo/pkg/util"
    24  
    25  	corev1 "k8s.io/api/core/v1"
    26  	"k8s.io/klog"
    27  )
    28  
    29  // See https://github.com/devfile/developer-images and https://quay.io/repository/devfile/base-developer-image?tab=tags
    30  const (
    31  	portForwardingHelperContainerName = "odo-helper-port-forwarding"
    32  	portForwardingHelperImage         = "quay.io/devfile/base-developer-image@sha256:27d5ce66a259decb84770ea0d1ce8058a806f39dfcfeed8387f9cf2f29e76480"
    33  )
    34  
    35  func (o *DevClient) createPodFromComponent(
    36  	ctx context.Context,
    37  	debug bool,
    38  	buildCommand string,
    39  	runCommand string,
    40  	debugCommand string,
    41  	withHelperContainer bool,
    42  	randomPorts bool,
    43  	customForwardedPorts []api.ForwardedPort,
    44  	usedPorts []int,
    45  	customAddress string,
    46  	devfileObj parser.DevfileObj,
    47  ) (*corev1.Pod, []api.ForwardedPort, error) {
    48  	var (
    49  		appName       = odocontext.GetApplication(ctx)
    50  		componentName = odocontext.GetComponentName(ctx)
    51  		workingDir    = odocontext.GetWorkingDirectory(ctx)
    52  	)
    53  
    54  	podTemplate, err := generator.GetPodTemplateSpec(devfileObj, generator.PodTemplateParams{})
    55  	if err != nil {
    56  		return nil, nil, err
    57  	}
    58  
    59  	podmanCaps, err := o.podmanClient.GetCapabilities()
    60  	if err != nil {
    61  		return nil, nil, err
    62  	}
    63  
    64  	if !podmanCaps.Cgroupv2 {
    65  		for i := range podTemplate.Spec.Containers {
    66  			delete(podTemplate.Spec.Containers[i].Resources.Limits, corev1.ResourceMemory)
    67  		}
    68  	}
    69  
    70  	containers := podTemplate.Spec.Containers
    71  	if len(containers) == 0 {
    72  		return nil, nil, fmt.Errorf("no valid components found in the devfile")
    73  	}
    74  
    75  	var fwPorts []api.ForwardedPort
    76  	fwPorts, err = getPortMapping(devfileObj, debug, randomPorts, usedPorts, customForwardedPorts, customAddress)
    77  	if err != nil {
    78  		return nil, nil, err
    79  	}
    80  
    81  	utils.AddOdoProjectVolume(containers)
    82  	utils.AddOdoMandatoryVolume(containers)
    83  
    84  	volumes := []corev1.Volume{
    85  		{
    86  			Name: storage.OdoSourceVolume,
    87  			VolumeSource: corev1.VolumeSource{
    88  				PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
    89  					ClaimName: getVolumeName(storage.OdoSourceVolume, componentName, appName),
    90  				},
    91  			},
    92  		},
    93  		{
    94  			Name: storage.SharedDataVolumeName,
    95  			VolumeSource: corev1.VolumeSource{
    96  				PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
    97  					ClaimName: getVolumeName(storage.SharedDataVolumeName, componentName, appName),
    98  				},
    99  			},
   100  		},
   101  	}
   102  
   103  	devfileVolumes, err := storage.ListStorage(devfileObj)
   104  	if err != nil {
   105  		return nil, nil, err
   106  	}
   107  
   108  	for _, devfileVolume := range devfileVolumes {
   109  		volumes = append(volumes, corev1.Volume{
   110  			Name: devfileVolume.Name,
   111  			VolumeSource: corev1.VolumeSource{
   112  				PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
   113  					ClaimName: getVolumeName(devfileVolume.Name, componentName, appName),
   114  				},
   115  			},
   116  		})
   117  		err = addVolumeMountToContainer(containers, devfileVolume)
   118  		if err != nil {
   119  			return nil, nil, err
   120  		}
   121  	}
   122  
   123  	containers, err = utils.UpdateContainersEntrypointsIfNeeded(devfileObj, containers, buildCommand, runCommand, debugCommand)
   124  	if err != nil {
   125  		return nil, nil, err
   126  	}
   127  
   128  	containers = addHostPorts(withHelperContainer, containers, fwPorts, customAddress)
   129  
   130  	pod := corev1.Pod{
   131  		Spec: corev1.PodSpec{
   132  			Containers: containers,
   133  			Volumes:    volumes,
   134  		},
   135  	}
   136  
   137  	pod.APIVersion, pod.Kind = corev1.SchemeGroupVersion.WithKind("Pod").ToAPIVersionAndKind()
   138  	name, err := util.NamespaceKubernetesObject(componentName, appName)
   139  	if err != nil {
   140  		return nil, nil, err
   141  	}
   142  	pod.SetName(name)
   143  
   144  	runtime := component.GetComponentRuntimeFromDevfileMetadata(devfileObj.Data.GetMetadata())
   145  	pod.SetLabels(labels.GetLabels(componentName, appName, runtime, labels.ComponentDevMode, true))
   146  	labels.SetProjectType(pod.GetLabels(), component.GetComponentTypeFromDevfileMetadata(devfileObj.Data.GetMetadata()))
   147  
   148  	if pod.Annotations == nil {
   149  		pod.Annotations = make(map[string]string)
   150  	}
   151  	if vcsUri := util.GetGitOriginPath(workingDir); vcsUri != "" {
   152  		pod.Annotations["app.openshift.io/vcs-uri"] = vcsUri
   153  	}
   154  
   155  	return &pod, fwPorts, nil
   156  }
   157  
   158  func addHostPorts(withHelperContainer bool, containers []corev1.Container, fwPorts []api.ForwardedPort, customAddress string) []corev1.Container {
   159  	if customAddress == "" {
   160  		customAddress = "127.0.0.1"
   161  	}
   162  	if withHelperContainer {
   163  		// A side helper container is added and will be responsible for redirecting the traffic,
   164  		// so it can work even if the application is listening on the container loopback interface.
   165  		for i := range containers {
   166  			containers[i].Ports = nil
   167  		}
   168  		// Add helper container for port-forwarding
   169  		pfHelperContainer := corev1.Container{
   170  			Name:    portForwardingHelperContainerName,
   171  			Image:   portForwardingHelperImage,
   172  			Command: []string{"tail"},
   173  			Args:    []string{"-f", "/dev/null"},
   174  		}
   175  		for _, fwPort := range fwPorts {
   176  			pfHelperContainer.Ports = append(pfHelperContainer.Ports, corev1.ContainerPort{
   177  				// It is intentional here to use the same port as ContainerPort and HostPort, for simplicity.
   178  				// In the helper container, a process will be run afterwards and will be listening on this port;
   179  				// this process will leverage socat to forward requests to the actual application port.
   180  				Name:          fwPort.PortName,
   181  				ContainerPort: int32(fwPort.LocalPort),
   182  				HostPort:      int32(fwPort.LocalPort),
   183  				HostIP:        customAddress,
   184  			})
   185  		}
   186  		containers = append(containers, pfHelperContainer)
   187  	} else {
   188  		// the original ports in container contains all Devfile endpoints that have been set by the Devfile library.
   189  		// We need to filter them out, to set only the ports that we need to port-forward.
   190  		for i := range containers {
   191  			var containerPorts []corev1.ContainerPort
   192  			for _, p := range containers[i].Ports {
   193  				for _, fwPort := range fwPorts {
   194  					if containers[i].Name == fwPort.ContainerName && int(p.ContainerPort) == fwPort.ContainerPort {
   195  						p.HostPort = int32(fwPort.LocalPort)
   196  						p.HostIP = customAddress
   197  						containerPorts = append(containerPorts, p)
   198  						break
   199  					}
   200  				}
   201  			}
   202  			containers[i].Ports = containerPorts
   203  		}
   204  	}
   205  	return containers
   206  }
   207  
   208  func getVolumeName(volume string, componentName string, appName string) string {
   209  	return volume + "-" + componentName + "-" + appName
   210  }
   211  
   212  func getPortMapping(devfileObj parser.DevfileObj, debug bool, randomPorts bool, usedPorts []int, definedPorts []api.ForwardedPort, address string) ([]api.ForwardedPort, error) {
   213  	if address == "" {
   214  		address = "127.0.0.1"
   215  	}
   216  	containerComponents, err := devfileObj.Data.GetComponents(common.DevfileOptions{
   217  		ComponentOptions: common.ComponentOptions{ComponentType: v1alpha2.ContainerComponentType},
   218  	})
   219  	if err != nil {
   220  		return nil, err
   221  	}
   222  	ceMapping := libdevfile.GetContainerEndpointMapping(containerComponents, debug)
   223  
   224  	var existingContainerPorts []int
   225  	for _, endpoints := range ceMapping {
   226  		for _, ep := range endpoints {
   227  			existingContainerPorts = append(existingContainerPorts, ep.TargetPort)
   228  		}
   229  	}
   230  
   231  	// this list makes sure that we ranged ports[20001-30001] do not coincide with a custom local port
   232  	customLocalPorts := make(map[int]struct{})
   233  	for _, dPort := range definedPorts {
   234  		customLocalPorts[dPort.LocalPort] = struct{}{}
   235  	}
   236  
   237  	isPortUsedInContainer := func(p int) bool {
   238  		for _, port := range existingContainerPorts {
   239  			if p == port {
   240  				return true
   241  			}
   242  		}
   243  		return false
   244  	}
   245  
   246  	// getCustomLocalPort analyzes the definedPorts i.e. custom port forwarding to see if a containerPort has a custom localPort, if a container name is provided, it also takes that into account.
   247  	getCustomLocalPort := func(containerPort int, container string) int {
   248  		for _, dp := range definedPorts {
   249  			if dp.ContainerPort == containerPort {
   250  				if dp.ContainerName != "" {
   251  					if dp.ContainerName == container {
   252  						return dp.LocalPort
   253  					}
   254  				} else {
   255  					return dp.LocalPort
   256  				}
   257  			}
   258  		}
   259  		return 0
   260  	}
   261  
   262  	var result []api.ForwardedPort
   263  	startPort := 20001
   264  	endPort := startPort + 10000
   265  	usedPortsCopy := make([]int, len(usedPorts))
   266  	copy(usedPortsCopy, usedPorts)
   267  
   268  	// Prepare to iterate over the ceMapping in an orderly fashion
   269  	// This ensures we iterate over the ceMapping in the same way every time, obtain the same result every time and avoid any flakes with tests
   270  	var containers []string
   271  	for container := range ceMapping {
   272  		containers = append(containers, container)
   273  	}
   274  	sort.Strings(containers)
   275  
   276  	for _, containerName := range containers {
   277  		endpoints := ceMapping[containerName]
   278  	epLoop:
   279  		for _, ep := range endpoints {
   280  			portName := ep.Name
   281  			isDebugPort := libdevfile.IsDebugPort(portName)
   282  			if !debug && isDebugPort {
   283  				klog.V(4).Infof("not running in Debug mode, so skipping Debug endpoint %s (%d) for container %q",
   284  					portName, ep.TargetPort, containerName)
   285  				continue
   286  			}
   287  			var freePort int
   288  			if len(definedPorts) != 0 {
   289  				freePort = getCustomLocalPort(ep.TargetPort, containerName)
   290  				if freePort == 0 {
   291  					for {
   292  						freePort, err = util.NextFreePort(startPort, endPort, usedPorts, address)
   293  						if err != nil {
   294  							klog.Infof("%s", err)
   295  							continue
   296  						}
   297  						// ensure that freePort is not a custom local port
   298  						if _, isPortUsed := customLocalPorts[freePort]; isPortUsed {
   299  							startPort = freePort + 1
   300  							continue
   301  						}
   302  						break
   303  					}
   304  					startPort = freePort + 1
   305  				}
   306  			} else if randomPorts {
   307  				if len(usedPortsCopy) != 0 {
   308  					freePort = usedPortsCopy[0]
   309  					usedPortsCopy = usedPortsCopy[1:]
   310  				} else {
   311  					rand.Seed(time.Now().UnixNano()) // #nosec
   312  					for {
   313  						freePort = rand.Intn(endPort-startPort+1) + startPort // #nosec
   314  						if !isPortUsedInContainer(freePort) && util.IsPortFree(freePort, address) {
   315  							break
   316  						}
   317  						time.Sleep(100 * time.Millisecond)
   318  					}
   319  				}
   320  			} else {
   321  				for {
   322  					freePort, err = util.NextFreePort(startPort, endPort, usedPorts, address)
   323  					if err != nil {
   324  						klog.Infof("%s", err)
   325  						continue epLoop
   326  					}
   327  					if !isPortUsedInContainer(freePort) {
   328  						break
   329  					}
   330  					startPort = freePort + 1
   331  					time.Sleep(100 * time.Millisecond)
   332  				}
   333  				startPort = freePort + 1
   334  			}
   335  			fp := api.ForwardedPort{
   336  				Platform:      commonflags.PlatformPodman,
   337  				PortName:      portName,
   338  				IsDebug:       isDebugPort,
   339  				ContainerName: containerName,
   340  				LocalAddress:  address,
   341  				LocalPort:     freePort,
   342  				ContainerPort: ep.TargetPort,
   343  				Exposure:      string(ep.Exposure),
   344  				Protocol:      string(ep.Protocol),
   345  			}
   346  			result = append(result, fp)
   347  		}
   348  	}
   349  	return result, nil
   350  }
   351  
   352  func addVolumeMountToContainer(containers []corev1.Container, devfileVolume storage.LocalStorage) error {
   353  	for i := range containers {
   354  		if containers[i].Name == devfileVolume.Container {
   355  			containers[i].VolumeMounts = append(containers[i].VolumeMounts, corev1.VolumeMount{
   356  				Name:      devfileVolume.Name,
   357  				MountPath: devfileVolume.Path,
   358  			})
   359  			return nil
   360  		}
   361  	}
   362  	return fmt.Errorf("container %q not found", devfileVolume.Container)
   363  }
   364  
   365  func getUsedPorts(ports []api.ForwardedPort) []int {
   366  	res := make([]int, 0, len(ports))
   367  	for _, port := range ports {
   368  		res = append(res, port.LocalPort)
   369  	}
   370  	return res
   371  }
   372  

View as plain text