Files
odo/pkg/dev/podmandev/pod.go
2023-08-21 09:26:03 -02:30

372 lines
11 KiB
Go

package podmandev
import (
"context"
"fmt"
"math/rand" // #nosec
"sort"
"time"
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/v2/pkg/devfile/generator"
"github.com/devfile/library/v2/pkg/devfile/parser"
"github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common"
"github.com/redhat-developer/odo/pkg/api"
"github.com/redhat-developer/odo/pkg/component"
"github.com/redhat-developer/odo/pkg/dev/kubedev/utils"
"github.com/redhat-developer/odo/pkg/labels"
"github.com/redhat-developer/odo/pkg/libdevfile"
"github.com/redhat-developer/odo/pkg/odo/commonflags"
odocontext "github.com/redhat-developer/odo/pkg/odo/context"
"github.com/redhat-developer/odo/pkg/storage"
"github.com/redhat-developer/odo/pkg/util"
corev1 "k8s.io/api/core/v1"
"k8s.io/klog"
)
// See https://github.com/devfile/developer-images and https://quay.io/repository/devfile/base-developer-image?tab=tags
const (
portForwardingHelperContainerName = "odo-helper-port-forwarding"
portForwardingHelperImage = "quay.io/devfile/base-developer-image@sha256:27d5ce66a259decb84770ea0d1ce8058a806f39dfcfeed8387f9cf2f29e76480"
)
func (o *DevClient) createPodFromComponent(
ctx context.Context,
debug bool,
buildCommand string,
runCommand string,
debugCommand string,
withHelperContainer bool,
randomPorts bool,
customForwardedPorts []api.ForwardedPort,
usedPorts []int,
customAddress string,
devfileObj parser.DevfileObj,
) (*corev1.Pod, []api.ForwardedPort, error) {
var (
appName = odocontext.GetApplication(ctx)
componentName = odocontext.GetComponentName(ctx)
workingDir = odocontext.GetWorkingDirectory(ctx)
)
podTemplate, err := generator.GetPodTemplateSpec(devfileObj, generator.PodTemplateParams{})
if err != nil {
return nil, nil, err
}
podmanCaps, err := o.podmanClient.GetCapabilities()
if err != nil {
return nil, nil, err
}
if !podmanCaps.Cgroupv2 {
for i := range podTemplate.Spec.Containers {
delete(podTemplate.Spec.Containers[i].Resources.Limits, corev1.ResourceMemory)
}
}
containers := podTemplate.Spec.Containers
if len(containers) == 0 {
return nil, nil, fmt.Errorf("no valid components found in the devfile")
}
var fwPorts []api.ForwardedPort
fwPorts, err = getPortMapping(devfileObj, debug, randomPorts, usedPorts, customForwardedPorts, customAddress)
if err != nil {
return nil, nil, err
}
utils.AddOdoProjectVolume(containers)
utils.AddOdoMandatoryVolume(containers)
volumes := []corev1.Volume{
{
Name: storage.OdoSourceVolume,
VolumeSource: corev1.VolumeSource{
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
ClaimName: getVolumeName(storage.OdoSourceVolume, componentName, appName),
},
},
},
{
Name: storage.SharedDataVolumeName,
VolumeSource: corev1.VolumeSource{
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
ClaimName: getVolumeName(storage.SharedDataVolumeName, componentName, appName),
},
},
},
}
devfileVolumes, err := storage.ListStorage(devfileObj)
if err != nil {
return nil, nil, err
}
for _, devfileVolume := range devfileVolumes {
volumes = append(volumes, corev1.Volume{
Name: devfileVolume.Name,
VolumeSource: corev1.VolumeSource{
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
ClaimName: getVolumeName(devfileVolume.Name, componentName, appName),
},
},
})
err = addVolumeMountToContainer(containers, devfileVolume)
if err != nil {
return nil, nil, err
}
}
containers, err = utils.UpdateContainersEntrypointsIfNeeded(devfileObj, containers, buildCommand, runCommand, debugCommand)
if err != nil {
return nil, nil, err
}
containers = addHostPorts(withHelperContainer, containers, fwPorts, customAddress)
pod := corev1.Pod{
Spec: corev1.PodSpec{
Containers: containers,
Volumes: volumes,
},
}
pod.APIVersion, pod.Kind = corev1.SchemeGroupVersion.WithKind("Pod").ToAPIVersionAndKind()
name, err := util.NamespaceKubernetesObject(componentName, appName)
if err != nil {
return nil, nil, err
}
pod.SetName(name)
runtime := component.GetComponentRuntimeFromDevfileMetadata(devfileObj.Data.GetMetadata())
pod.SetLabels(labels.GetLabels(componentName, appName, runtime, labels.ComponentDevMode, true))
labels.SetProjectType(pod.GetLabels(), component.GetComponentTypeFromDevfileMetadata(devfileObj.Data.GetMetadata()))
if pod.Annotations == nil {
pod.Annotations = make(map[string]string)
}
if vcsUri := util.GetGitOriginPath(workingDir); vcsUri != "" {
pod.Annotations["app.openshift.io/vcs-uri"] = vcsUri
}
return &pod, fwPorts, nil
}
func addHostPorts(withHelperContainer bool, containers []corev1.Container, fwPorts []api.ForwardedPort, customAddress string) []corev1.Container {
if customAddress == "" {
customAddress = "127.0.0.1"
}
if withHelperContainer {
// A side helper container is added and will be responsible for redirecting the traffic,
// so it can work even if the application is listening on the container loopback interface.
for i := range containers {
containers[i].Ports = nil
}
// Add helper container for port-forwarding
pfHelperContainer := corev1.Container{
Name: portForwardingHelperContainerName,
Image: portForwardingHelperImage,
Command: []string{"tail"},
Args: []string{"-f", "/dev/null"},
}
for _, fwPort := range fwPorts {
pfHelperContainer.Ports = append(pfHelperContainer.Ports, corev1.ContainerPort{
// It is intentional here to use the same port as ContainerPort and HostPort, for simplicity.
// In the helper container, a process will be run afterwards and will be listening on this port;
// this process will leverage socat to forward requests to the actual application port.
Name: fwPort.PortName,
ContainerPort: int32(fwPort.LocalPort),
HostPort: int32(fwPort.LocalPort),
HostIP: customAddress,
})
}
containers = append(containers, pfHelperContainer)
} else {
// the original ports in container contains all Devfile endpoints that have been set by the Devfile library.
// We need to filter them out, to set only the ports that we need to port-forward.
for i := range containers {
var containerPorts []corev1.ContainerPort
for _, p := range containers[i].Ports {
for _, fwPort := range fwPorts {
if containers[i].Name == fwPort.ContainerName && int(p.ContainerPort) == fwPort.ContainerPort {
p.HostPort = int32(fwPort.LocalPort)
p.HostIP = customAddress
containerPorts = append(containerPorts, p)
break
}
}
}
containers[i].Ports = containerPorts
}
}
return containers
}
func getVolumeName(volume string, componentName string, appName string) string {
return volume + "-" + componentName + "-" + appName
}
func getPortMapping(devfileObj parser.DevfileObj, debug bool, randomPorts bool, usedPorts []int, definedPorts []api.ForwardedPort, address string) ([]api.ForwardedPort, error) {
if address == "" {
address = "127.0.0.1"
}
containerComponents, err := devfileObj.Data.GetComponents(common.DevfileOptions{
ComponentOptions: common.ComponentOptions{ComponentType: v1alpha2.ContainerComponentType},
})
if err != nil {
return nil, err
}
ceMapping := libdevfile.GetContainerEndpointMapping(containerComponents, debug)
var existingContainerPorts []int
for _, endpoints := range ceMapping {
for _, ep := range endpoints {
existingContainerPorts = append(existingContainerPorts, ep.TargetPort)
}
}
// this list makes sure that we ranged ports[20001-30001] do not coincide with a custom local port
customLocalPorts := make(map[int]struct{})
for _, dPort := range definedPorts {
customLocalPorts[dPort.LocalPort] = struct{}{}
}
isPortUsedInContainer := func(p int) bool {
for _, port := range existingContainerPorts {
if p == port {
return true
}
}
return false
}
// 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.
getCustomLocalPort := func(containerPort int, container string) int {
for _, dp := range definedPorts {
if dp.ContainerPort == containerPort {
if dp.ContainerName != "" {
if dp.ContainerName == container {
return dp.LocalPort
}
} else {
return dp.LocalPort
}
}
}
return 0
}
var result []api.ForwardedPort
startPort := 20001
endPort := startPort + 10000
usedPortsCopy := make([]int, len(usedPorts))
copy(usedPortsCopy, usedPorts)
// Prepare to iterate over the ceMapping in an orderly fashion
// 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
var containers []string
for container := range ceMapping {
containers = append(containers, container)
}
sort.Strings(containers)
for _, containerName := range containers {
endpoints := ceMapping[containerName]
epLoop:
for _, ep := range endpoints {
portName := ep.Name
isDebugPort := libdevfile.IsDebugPort(portName)
if !debug && isDebugPort {
klog.V(4).Infof("not running in Debug mode, so skipping Debug endpoint %s (%d) for container %q",
portName, ep.TargetPort, containerName)
continue
}
var freePort int
if len(definedPorts) != 0 {
freePort = getCustomLocalPort(ep.TargetPort, containerName)
if freePort == 0 {
for {
freePort, err = util.NextFreePort(startPort, endPort, usedPorts, address)
if err != nil {
klog.Infof("%s", err)
continue
}
// ensure that freePort is not a custom local port
if _, isPortUsed := customLocalPorts[freePort]; isPortUsed {
startPort = freePort + 1
continue
}
break
}
startPort = freePort + 1
}
} else if randomPorts {
if len(usedPortsCopy) != 0 {
freePort = usedPortsCopy[0]
usedPortsCopy = usedPortsCopy[1:]
} else {
rand.Seed(time.Now().UnixNano()) // #nosec
for {
freePort = rand.Intn(endPort-startPort+1) + startPort // #nosec
if !isPortUsedInContainer(freePort) && util.IsPortFree(freePort, address) {
break
}
time.Sleep(100 * time.Millisecond)
}
}
} else {
for {
freePort, err = util.NextFreePort(startPort, endPort, usedPorts, address)
if err != nil {
klog.Infof("%s", err)
continue epLoop
}
if !isPortUsedInContainer(freePort) {
break
}
startPort = freePort + 1
time.Sleep(100 * time.Millisecond)
}
startPort = freePort + 1
}
fp := api.ForwardedPort{
Platform: commonflags.PlatformPodman,
PortName: portName,
IsDebug: isDebugPort,
ContainerName: containerName,
LocalAddress: address,
LocalPort: freePort,
ContainerPort: ep.TargetPort,
Exposure: string(ep.Exposure),
Protocol: string(ep.Protocol),
}
result = append(result, fp)
}
}
return result, nil
}
func addVolumeMountToContainer(containers []corev1.Container, devfileVolume storage.LocalStorage) error {
for i := range containers {
if containers[i].Name == devfileVolume.Container {
containers[i].VolumeMounts = append(containers[i].VolumeMounts, corev1.VolumeMount{
Name: devfileVolume.Name,
MountPath: devfileVolume.Path,
})
return nil
}
}
return fmt.Errorf("container %q not found", devfileVolume.Container)
}
func getUsedPorts(ports []api.ForwardedPort) []int {
res := make([]int, 0, len(ports))
for _, port := range ports {
res = append(res, port.LocalPort)
}
return res
}