diff --git a/pkg/portForward/podmanportforward/portForward.go b/pkg/portForward/podmanportforward/portForward.go new file mode 100644 index 00000000000..17a38b0686b --- /dev/null +++ b/pkg/portForward/podmanportforward/portForward.go @@ -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), + } +} diff --git a/pkg/remotecmd/kubeexec.go b/pkg/remotecmd/kubeexec.go index dc0a7afe719..f62404f246f 100644 --- a/pkg/remotecmd/kubeexec.go +++ b/pkg/remotecmd/kubeexec.go @@ -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) } diff --git a/pkg/remotecmd/types.go b/pkg/remotecmd/types.go index f05b1234c77..44b36fb23be 100644 --- a/pkg/remotecmd/types.go +++ b/pkg/remotecmd/types.go @@ -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