Skip to content

Commit

Permalink
Make port-forwarding work on Podman with apps listening on the loopba…
Browse files Browse the repository at this point in the history
…ck interface, via a new `--forward-localhost` flag (#6629)

* Embed platform.Client interface in platform-specific interfaces

This avoids repeating the same methods in both interfaces,
and makes the intent clearer.

* Verify interface compliance of PodmanCli at compile time

This is recommended in the Coding Conventions guidelines [1].
Specifically, what's important here is checking that it meets the 'platform.Client' contract.

[1] https://github.com/redhat-developer/odo/wiki/Dev:-Coding-Conventions#verify-interface-compliance

* Move K8s-specific implementation of port-forwarding to a dedicated package

This paves the way to providing a different implementation for Podman

* Remove GetPortsToForward method from the portForward.Client interface

Current implementation relies on the Devfile object,
so it makes more sense to be in the libdevfile package.

* Monitor and send appropriate status events after starting a remote command process

This allows callers to get more meaningful events about the process.

* Implement port-forwarding logic on Podman

As explained in [1], this makes use of a helper sidecar container
(aptly named "odo-helper-port-forwarding") to be added to the Pod Spec created by odo.
In this scope, port-forwarding will be equivalent of executing a socat command in this
helper container, like so:

socat -d tcp-listen:20002,reuseaddr,fork tcp:localhost:5858

In the command above, this will open up port 20001 on the helper container,
and forwarding requests to localhost:5858
(which would be in the application container, part of the same Pod)

[1] #6510

* Update portForward.Client interface methods

Specifically, the 'StartPortForwarding' method
can now accept an explicit list of ports that needs to
be forwarded, if the caller can compute provide such information.

This is currently useful on Podman where the ports
(even the random ones) are known in advance.

* Add helper sidecar container to the Pod Spec generated on Podman

As explained in [1], this helper sidecar container (aptly named "odo-helper-port-forwarding")
will hold the actual container/host ports mapping in the spec
(to overcome the limitation of using hostPort against a container app listening on the loopback interface);
this running container will be used to execute the actual port-forwarding commands (based on socat), e.g:

```
apiVersion: v1
kind: Pod
metadata:
  name: my-component-app
  labels:
    app: my-component-app
spec:
  containers:
  - name: runtime
    command: [ 'tail', '-f', '/dev/null']
    # Omitted for brevity, but imagine an application being executed by odo
    # and listening on both 0.0.0.0:3000 and 127.0.0.1:5858

  - name: odo-helper-port-forwarding
    image: quay.io/devfile/base-developer-image:ubi8-latest
    command: [ 'tail', '-f', '/dev/null']
    ports:
    - containerPort: 20001
      # A command will be executed by odo, e.g.: 'socat -d -d tcp-listen:20001,reuseaddr,fork tcp:localhost:3000'
      hostPort: 20001
    - containerPort: 20002
      # A command will be executed by odo, e.g.: 'socat -d -d tcp-listen:20002,reuseaddr,fork tcp:localhost:5858'
      hostPort: 20002

# ... Omitted for brevity
```

In this scope, port-forwarding from 20002 to 5858 for example will be equivalent of executing
a socat command in this helper container, like so:

socat -d tcp-listen:20002,reuseaddr,fork tcp:localhost:5858

In the command above, this will open up port 20001 on the helper container,
and forwarding requests to localhost:5858
(which would be in the 'runtime' container, part of the same Pod)

[1] #6510

* Inject the right portForward client depending on the platform

* Delegate port-forwarding on Podman to the appropriate client

* Implement --forward-localhost on Podman

* Add unit and integration test cases

* Cover more debug-related test cases on Podman

* Expose the endpoint protocol so as to instruct socat to listen and forward the right protocol

* Fix sub-deps of EXEC and PORT_FORWARD, as suggested in review
  • Loading branch information
rm3l authored Mar 6, 2023
1 parent 8b4ccf5 commit 29b5a38
Show file tree
Hide file tree
Showing 24 changed files with 1,300 additions and 355 deletions.
1 change: 1 addition & 0 deletions pkg/api/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type ForwardedPort struct {
LocalPort int `json:"localPort"`
ContainerPort int `json:"containerPort"`
Exposure string `json:"exposure,omitempty"`
Protocol string `json:"protocol,omitempty"`
}

type ConnectionData struct {
Expand Down
3 changes: 3 additions & 0 deletions pkg/dev/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ type StartOptions struct {
// IgnoreLocalhost indicates whether to proceed with port-forwarding regardless of any container ports being bound to the container loopback interface.
// Applicable to Podman only.
IgnoreLocalhost bool
// ForwardLocalhost is a flag indicating if we inject a side container that will make port-forwarding work with container apps listening on the loopback interface.
// Applicable to Podman only.
ForwardLocalhost bool
// Variables to override in the Devfile
Variables map[string]string
}
Expand Down
147 changes: 108 additions & 39 deletions pkg/dev/podmandev/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ import (
"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 createPodFromComponent(
devfileObj parser.DevfileObj,
componentName string,
Expand All @@ -31,6 +37,7 @@ func createPodFromComponent(
buildCommand string,
runCommand string,
debugCommand string,
withHelperContainer bool,
randomPorts bool,
usedPorts []int,
) (*corev1.Pod, []api.ForwardedPort, error) {
Expand All @@ -42,23 +49,14 @@ func createPodFromComponent(
return nil, nil, fmt.Errorf("no valid components found in the devfile")
}

containers, err = utils.UpdateContainersEntrypointsIfNeeded(devfileObj, containers, buildCommand, runCommand, debugCommand)
fwPorts, err := getPortMapping(devfileObj, debug, randomPorts, usedPorts)
if err != nil {
return nil, nil, err
}

utils.AddOdoProjectVolume(&containers)
utils.AddOdoMandatoryVolume(&containers)

// get the endpoint/port information for containers in devfile
containerComponents, err := devfileObj.Data.GetComponents(common.DevfileOptions{
ComponentOptions: common.ComponentOptions{ComponentType: v1alpha2.ContainerComponentType},
})
if err != nil {
return nil, nil, err
}
ceMapping := libdevfile.GetContainerEndpointMapping(containerComponents, debug)
fwPorts := addHostPorts(containers, ceMapping, debug, randomPorts, usedPorts)

volumes := []corev1.Volume{
{
Name: storage.OdoSourceVolume,
Expand Down Expand Up @@ -98,6 +96,13 @@ func createPodFromComponent(
}
}

containers, err = utils.UpdateContainersEntrypointsIfNeeded(devfileObj, containers, buildCommand, runCommand, debugCommand)
if err != nil {
return nil, nil, err
}

containers = addHostPorts(withHelperContainer, containers, fwPorts)

pod := corev1.Pod{
Spec: corev1.PodSpec{
Containers: containers,
Expand All @@ -119,25 +124,93 @@ func createPodFromComponent(
return &pod, fwPorts, nil
}

func addHostPorts(withHelperContainer bool, containers []corev1.Container, fwPorts []api.ForwardedPort) []corev1.Container {
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),
})
}
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)
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 addHostPorts(containers []corev1.Container, ceMapping map[string][]v1alpha2.Endpoint, debug bool, randomPorts bool, usedPorts []int) []api.ForwardedPort {
func getPortMapping(devfileObj parser.DevfileObj, debug bool, randomPorts bool, usedPorts []int) ([]api.ForwardedPort, error) {
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)
}
}

isPortUsedInContainer := func(p int) bool {
for _, port := range existingContainerPorts {
if p == port {
return true
}
}
return false
}

var result []api.ForwardedPort
startPort := 20001
endPort := startPort + 10000
usedPortsCopy := make([]int, len(usedPorts))
copy(usedPortsCopy, usedPorts)
for i := range containers {
var ports []corev1.ContainerPort
for _, port := range containers[i].Ports {
containerName := containers[i].Name
portName := port.Name
for containerName, endpoints := range ceMapping {
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 container Debug port: %v:%v:%v",
containerName, portName, port.ContainerPort)
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
Expand All @@ -149,46 +222,42 @@ func addHostPorts(containers []corev1.Container, ceMapping map[string][]v1alpha2
rand.Seed(time.Now().UnixNano()) //#nosec
for {
freePort = rand.Intn(endPort-startPort+1) + startPort //#nosec
if util.IsPortFree(freePort) {
if !isPortUsedInContainer(freePort) && util.IsPortFree(freePort) {
break
}
time.Sleep(100 * time.Millisecond)
}
}
} else {
var err error
freePort, err = util.NextFreePort(startPort, endPort, usedPorts)
if err != nil {
klog.Infof("%s", err)
continue
for {
freePort, err = util.NextFreePort(startPort, endPort, usedPorts)
if err != nil {
klog.Infof("%s", err)
continue epLoop
}
if !isPortUsedInContainer(freePort) {
break
}
startPort = freePort + 1
time.Sleep(100 * time.Millisecond)
}
startPort = freePort + 1
}
// Find the endpoint in the container-endpoint mapping
containerPort := int(port.ContainerPort)
fp := api.ForwardedPort{
Platform: commonflags.PlatformPodman,
PortName: portName,
IsDebug: isDebugPort,
ContainerName: containerName,
LocalAddress: "127.0.0.1",
LocalPort: freePort,
ContainerPort: containerPort,
}

for _, ep := range ceMapping[containerName] {
if ep.TargetPort == containerPort {
fp.Exposure = string(ep.Exposure)
break
}
ContainerPort: ep.TargetPort,
Exposure: string(ep.Exposure),
Protocol: string(ep.Protocol),
}
result = append(result, fp)
port.HostPort = int32(freePort)
ports = append(ports, port)
}
containers[i].Ports = ports
}
return result
return result, nil
}

func addVolumeMountToContainer(containers []corev1.Container, devfileVolume storage.LocalStorage) error {
Expand Down
Loading

0 comments on commit 29b5a38

Please sign in to comment.