Skip to content

Commit

Permalink
Implement port-forwarding logic on Podman
Browse files Browse the repository at this point in the history
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] redhat-developer#6510
  • Loading branch information
rm3l committed Feb 10, 2023
1 parent 9ae62a8 commit aa1fb2e
Showing 1 changed file with 131 additions and 0 deletions.
131 changes: 131 additions & 0 deletions pkg/portForward/podmanportforward/portForward.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package podmanportforward

import (
"fmt"
"io"
"reflect"
"strings"
"sync"

"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/v2/pkg/devfile/parser"
"k8s.io/klog"

"github.com/redhat-developer/odo/pkg/api"
"github.com/redhat-developer/odo/pkg/exec"
"github.com/redhat-developer/odo/pkg/portForward"
"github.com/redhat-developer/odo/pkg/remotecmd"
)

const pfHelperContainer = "odo-helper-port-forwarding"

type PFClient struct {
remoteProcessHandler remotecmd.RemoteProcessHandler

appliedPorts map[api.ForwardedPort]struct{}
}

var _ portForward.Client = (*PFClient)(nil)

func NewPFClient(execClient exec.Client) *PFClient {
return &PFClient{
remoteProcessHandler: remotecmd.NewKubeExecProcessHandler(execClient),
appliedPorts: make(map[api.ForwardedPort]struct{}),
}
}

func (o *PFClient) StartPortForwarding(
devFileObj parser.DevfileObj,
componentName string,
debug bool,
randomPorts bool,
out io.Writer,
errOut io.Writer,
definedPorts []api.ForwardedPort,
) error {
var appliedPorts []api.ForwardedPort
for port, _ := range o.appliedPorts {
appliedPorts = append(appliedPorts, port)
}
if reflect.DeepEqual(appliedPorts, definedPorts) {
klog.V(3).Infof("Port forwarding should already be running for defined ports: %v", definedPorts)
return nil
}

o.StopPortForwarding(componentName)

outputHandler := func(fwPort api.ForwardedPort) remotecmd.CommandOutputHandler {
return func(status remotecmd.RemoteProcessStatus, stdout []string, stderr []string, err error) {
klog.V(4).Infof("Status for port-forwarding (from %s:%d -> %d): %s", fwPort.LocalAddress, fwPort.LocalPort, fwPort.ContainerPort, status)
klog.V(4).Info(strings.Join(stdout, "\n"))
klog.V(4).Info(strings.Join(stderr, "\n"))
switch status {
case remotecmd.Running:
o.appliedPorts[fwPort] = struct{}{}
case remotecmd.Stopped, remotecmd.Errored:
delete(o.appliedPorts, fwPort)
if status == remotecmd.Stopped {
fmt.Fprintf(out, "Stopped port-forwarding from %s:%d -> %d", fwPort.LocalAddress, fwPort.LocalPort, fwPort.ContainerPort)
}
}
}
}

for _, port := range definedPorts {
err := o.remoteProcessHandler.StartProcessForCommand(getCommandDefinition(port), getPodName(componentName), pfHelperContainer, outputHandler(port))
if err != nil {
klog.V(4).Infof("error while creating port-forwarding for container port %d: %v", port.ContainerPort, err)
continue
}
o.appliedPorts[port] = struct{}{}
}
return nil
}

func (o *PFClient) StopPortForwarding(componentName string) {
if len(o.appliedPorts) == 0 {
return
}

var wg sync.WaitGroup
wg.Add(len(o.appliedPorts))
for port, _ := range o.appliedPorts {
port := port
go func() {
defer wg.Done()
err := o.remoteProcessHandler.StopProcessForCommand(getCommandDefinition(port), getPodName(componentName), pfHelperContainer)
if err != nil {
klog.V(4).Infof("error while stopping port-forwarding for container port %d: %v", port.ContainerPort, err)
}
}()
}
wg.Wait()

o.appliedPorts = nil
}

func (o *PFClient) GetForwardedPorts() map[string][]v1alpha2.Endpoint {
result := make(map[string][]v1alpha2.Endpoint)
for port, _ := range o.appliedPorts {
result[port.ContainerName] = append(result[port.ContainerName], v1alpha2.Endpoint{
Name: port.PortName,
TargetPort: port.ContainerPort,
Exposure: v1alpha2.EndpointExposure(port.Exposure),
})
}
return result
}

func getPodName(componentName string) string {
return fmt.Sprintf("%s-app", componentName)
}

func getCommandDefinition(port api.ForwardedPort) remotecmd.CommandDefinition {
return remotecmd.CommandDefinition{
Id: fmt.Sprintf("pf-%s", port.PortName),
// PidDirectory needs to be writable
PidDirectory: "/projects/",
//TODO(rm3l) Use the right L4 protocol: tcp or udp?
CmdLine: fmt.Sprintf("socat -d tcp-listen:%d,reuseaddr,fork tcp:localhost:%d", port.LocalPort, port.ContainerPort),
}
}

0 comments on commit aa1fb2e

Please sign in to comment.