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 Mar 1, 2023
1 parent 872156f commit 9f07573
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 1 deletion.
130 changes: 130 additions & 0 deletions pkg/portForward/podmanportforward/portForward.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
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 {
return fmt.Errorf("error while creating port-forwarding for container port %d: %w", port.ContainerPort, err)
}
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 = make(map[api.ForwardedPort]struct{})
}

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),
}
}
6 changes: 5 additions & 1 deletion pkg/remotecmd/kubeexec.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,5 +358,9 @@ func (k *kubeExecProcessHandler) getProcessChildren(pid int, podName string, con
// The parent folder is supposed to be existing, because it should be mounted in the container using the mandatory
// shared volume (more info in the AddOdoMandatoryVolume function from the utils package).
func getPidFileForCommand(def CommandDefinition) string {
return fmt.Sprintf("%s/.odo_cmd_%s.pid", strings.TrimSuffix(storage.SharedDataMountPath, "/"), def.Id)
parentDir := def.PidDirectory
if parentDir == "" {
parentDir = storage.SharedDataMountPath
}
return fmt.Sprintf("%s/.odo_cmd_%s.pid", strings.TrimSuffix(parentDir, "/"), def.Id)
}
4 changes: 4 additions & 0 deletions pkg/remotecmd/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ type CommandDefinition struct {
// Id is any unique (and short) identifier that helps manage the process associated to this command.
Id string

// PidDirectory is the directory where the PID file for this process will be stored.
// The directory needs to be present in the remote container and be writable by the user (in the container) executing the command.
PidDirectory string

// WorkingDir is the working directory from which the command should get executed.
WorkingDir string

Expand Down

0 comments on commit 9f07573

Please sign in to comment.