diff --git a/docs/website/docs/command-reference/dev.md b/docs/website/docs/command-reference/dev.md index 98ca75e0646..08689bf50d6 100644 --- a/docs/website/docs/command-reference/dev.md +++ b/docs/website/docs/command-reference/dev.md @@ -244,6 +244,114 @@ The following command will override the `USER` Devfile variable with the `john` odo dev --var USER=john --var-file config.vars ``` + +### Using custom port mapping for port forwarding +Custom local ports can be passed for port forwarding with the help of the `--port-forward` flag. This feature is supported on both podman and cluster. + +This feature can be helpful when you want to provide consistent and predictable port numbers and avoid being assigned a potentially different port number every time `odo dev` is run. + +Supported formats for this flag include: +1. `:` +2. `::` - This format is necessary when multiple container components of a Devfile have the same port number. + +The flag accepts a stringArray, so `--port-forward` flag can be defined multiple times. + +If a custom port mapping is not defined for a port, `odo` will assign a free port in the range of 20001-30001. + +```shell +odo dev --port-forward : --port-forward :: +``` + +
+Example + +```shell +$ odo dev --port-forward 3000:runtime:3000 --port-forward 5000:5858 --debug + __ + / \__ Developing using the "my-nodejs-app" Devfile + \__/ \ Namespace: default + / \__/ odo version: v3.9.0 + \__/ + + ⚠ You are using "default" namespace, odo may not work as expected in the default namespace. + ⚠ You may set a new namespace by running `odo create namespace `, or set an existing one by running `odo set namespace ` + +↪ Running on the cluster in Dev mode + • Waiting for Kubernetes resources ... + ⚠ Pod is Pending + ✓ Pod is Running + ✓ Syncing files into the container [152ms] + ✓ Building your application in container (command: install) [27s] + • Executing the application (command: debug) ... + ✓ Waiting for the application to be ready [1s] + - Forwarding from 127.0.0.1:8000 -> 3000 + + - Forwarding from 127.0.0.1:5000 -> 5858 + + +↪ Dev mode + Status: + Watching for changes in the current directory /tmp/nodejs-debug-2 + + Keyboard Commands: +[Ctrl+c] - Exit and delete resources from the cluster + [p] - Manually apply local changes to the application on the cluster +``` +
+ +Note that `--random-ports` flag cannot be used with `--port-forward` flag. + +### Using custom address for port forwarding +A custom address can be passed for port forwarding with the help of `--address` flag. This feature is supported on both podman and cluster. +The default value is 127.0.0.1. + +```shell +odo dev --address +``` + +
+Example + +```shell +$ odo dev --address 127.0.10.3 + __ + / \__ Developing using the "my-nodejs-app" Devfile + \__/ \ Namespace: default + / \__/ odo version: v3.9.0 + \__/ + + ⚠ You are using "default" namespace, odo may not work as expected in the default namespace. + ⚠ You may set a new namespace by running `odo create namespace `, or set an existing one by running `odo set namespace ` + +↪ Running on the cluster in Dev mode + • Waiting for Kubernetes resources ... + ⚠ Pod is Pending + ✓ Pod is Running + ✓ Syncing files into the container [123ms] + ✓ Building your application in container (command: install) [15s] + • Executing the application (command: run) ... + ✓ Waiting for the application to be ready [1s] + - Forwarding from 127.0.10.3:20001 -> 3000 + + +↪ Dev mode + Status: + Watching for changes in the current directory /tmp/nodejs-debug-2 + + Keyboard Commands: +[Ctrl+c] - Exit and delete resources from the cluster + [p] - Manually apply local changes to the application on the cluster +``` +
+ +:::note +If you are on macOS and using a Cluster platform, you may not be able to run multiple Dev sessions in parallel on address 0.0.0.0 without defining a custom port mapping, or using a different or default address. + +For more information, see the following issues: +1. [Cannot start 2 different Dev sessions on Podman due to conflicting host ports](https://github.com/redhat-developer/odo/issues/6612) +2. [[MacOS] Cannot run 2 dev sessions simultaneously on cluster](https://github.com/redhat-developer/odo/issues/6744) +::: + ### Running on Podman Instead of deploying the container into a Kubernetes cluster, `odo dev` can leverage the podman installation on your system to deploy the container. diff --git a/pkg/dev/interface.go b/pkg/dev/interface.go index 9578babb570..177831b2b36 100644 --- a/pkg/dev/interface.go +++ b/pkg/dev/interface.go @@ -22,6 +22,8 @@ type StartOptions struct { RandomPorts bool // CustomForwardedPorts define custom ports for port forwarding CustomForwardedPorts []api.ForwardedPort + // CustomAddress defines a custom local address for port forwarding; default value is 127.0.0.1 + CustomAddress string // if WatchFiles is set, files changes will trigger a new sync to the container WatchFiles bool // IgnoreLocalhost indicates whether to proceed with port-forwarding regardless of any container ports being bound to the container loopback interface. diff --git a/pkg/dev/kubedev/innerloop.go b/pkg/dev/kubedev/innerloop.go index edffda1c66f..02328222f27 100644 --- a/pkg/dev/kubedev/innerloop.go +++ b/pkg/dev/kubedev/innerloop.go @@ -180,7 +180,7 @@ func (o *DevClient) innerloop(ctx context.Context, parameters common.PushParamet fmt.Fprintln(log.GetStdout()) } - err = o.portForwardClient.StartPortForwarding(ctx, parameters.Devfile, componentName, parameters.StartOptions.Debug, parameters.StartOptions.RandomPorts, log.GetStdout(), parameters.StartOptions.ErrOut, parameters.StartOptions.CustomForwardedPorts) + err = o.portForwardClient.StartPortForwarding(ctx, parameters.Devfile, componentName, parameters.StartOptions.Debug, parameters.StartOptions.RandomPorts, log.GetStdout(), parameters.StartOptions.ErrOut, parameters.StartOptions.CustomForwardedPorts, parameters.StartOptions.CustomAddress) if err != nil { return common.NewErrPortForward(err) } diff --git a/pkg/dev/podmandev/pod.go b/pkg/dev/podmandev/pod.go index 3402dd92811..d506c36fdd6 100644 --- a/pkg/dev/podmandev/pod.go +++ b/pkg/dev/podmandev/pod.go @@ -42,6 +42,7 @@ func createPodFromComponent( randomPorts bool, customForwardedPorts []api.ForwardedPort, usedPorts []int, + customAddress string, ) (*corev1.Pod, []api.ForwardedPort, error) { var ( appName = odocontext.GetApplication(ctx) @@ -60,7 +61,7 @@ func createPodFromComponent( } var fwPorts []api.ForwardedPort - fwPorts, err = getPortMapping(*devfileObj, debug, randomPorts, usedPorts, customForwardedPorts) + fwPorts, err = getPortMapping(*devfileObj, debug, randomPorts, usedPorts, customForwardedPorts, customAddress) if err != nil { return nil, nil, err } @@ -112,7 +113,7 @@ func createPodFromComponent( return nil, nil, err } - containers = addHostPorts(withHelperContainer, containers, fwPorts) + containers = addHostPorts(withHelperContainer, containers, fwPorts, customAddress) pod := corev1.Pod{ Spec: corev1.PodSpec{ @@ -142,7 +143,10 @@ func createPodFromComponent( return &pod, fwPorts, nil } -func addHostPorts(withHelperContainer bool, containers []corev1.Container, fwPorts []api.ForwardedPort) []corev1.Container { +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. @@ -164,6 +168,7 @@ func addHostPorts(withHelperContainer bool, containers []corev1.Container, fwPor Name: fwPort.PortName, ContainerPort: int32(fwPort.LocalPort), HostPort: int32(fwPort.LocalPort), + HostIP: customAddress, }) } containers = append(containers, pfHelperContainer) @@ -176,6 +181,7 @@ func addHostPorts(withHelperContainer bool, containers []corev1.Container, fwPor 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 } @@ -191,7 +197,10 @@ 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) ([]api.ForwardedPort, error) { +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}, }) @@ -268,7 +277,7 @@ func getPortMapping(devfileObj parser.DevfileObj, debug bool, randomPorts bool, freePort = getCustomLocalPort(ep.TargetPort, containerName) if freePort == 0 { for { - freePort, err = util.NextFreePort(startPort, endPort, usedPorts) + freePort, err = util.NextFreePort(startPort, endPort, usedPorts, address) if err != nil { klog.Infof("%s", err) continue @@ -290,7 +299,7 @@ func getPortMapping(devfileObj parser.DevfileObj, debug bool, randomPorts bool, rand.Seed(time.Now().UnixNano()) // #nosec for { freePort = rand.Intn(endPort-startPort+1) + startPort // #nosec - if !isPortUsedInContainer(freePort) && util.IsPortFree(freePort) { + if !isPortUsedInContainer(freePort) && util.IsPortFree(freePort, address) { break } time.Sleep(100 * time.Millisecond) @@ -298,7 +307,7 @@ func getPortMapping(devfileObj parser.DevfileObj, debug bool, randomPorts bool, } } else { for { - freePort, err = util.NextFreePort(startPort, endPort, usedPorts) + freePort, err = util.NextFreePort(startPort, endPort, usedPorts, address) if err != nil { klog.Infof("%s", err) continue epLoop @@ -316,7 +325,7 @@ func getPortMapping(devfileObj parser.DevfileObj, debug bool, randomPorts bool, PortName: portName, IsDebug: isDebugPort, ContainerName: containerName, - LocalAddress: "127.0.0.1", + LocalAddress: address, LocalPort: freePort, ContainerPort: ep.TargetPort, Exposure: string(ep.Exposure), diff --git a/pkg/dev/podmandev/pod_test.go b/pkg/dev/podmandev/pod_test.go index f23393de040..ead043ed9d2 100644 --- a/pkg/dev/podmandev/pod_test.go +++ b/pkg/dev/podmandev/pod_test.go @@ -140,6 +140,7 @@ func Test_createPodFromComponent(t *testing.T) { debugCommand string forwardLocalhost bool customForwardedPorts []api.ForwardedPort + customAddress string } tests := []struct { name string @@ -310,6 +311,7 @@ func Test_createPodFromComponent(t *testing.T) { ContainerPort: 8080, HostPort: 20001, Protocol: corev1.ProtocolTCP, + HostIP: "127.0.0.1", }) return pod }, @@ -351,6 +353,7 @@ func Test_createPodFromComponent(t *testing.T) { Name: "http", ContainerPort: 20001, HostPort: 20001, + HostIP: "127.0.0.1", }) return pod }, @@ -396,6 +399,7 @@ func Test_createPodFromComponent(t *testing.T) { ContainerPort: 8080, HostPort: 20001, Protocol: corev1.ProtocolTCP, + HostIP: "127.0.0.1", }) return pod }, @@ -441,6 +445,7 @@ func Test_createPodFromComponent(t *testing.T) { Name: "http", ContainerPort: 20001, HostPort: 20001, + HostIP: "127.0.0.1", }) return pod }, @@ -487,12 +492,14 @@ func Test_createPodFromComponent(t *testing.T) { ContainerPort: 8080, HostPort: 20001, Protocol: corev1.ProtocolTCP, + HostIP: "127.0.0.1", }) pod.Spec.Containers[0].Ports = append(pod.Spec.Containers[0].Ports, corev1.ContainerPort{ Name: "debug", ContainerPort: 5858, HostPort: 20002, Protocol: corev1.ProtocolTCP, + HostIP: "127.0.0.1", }) return pod }, @@ -548,11 +555,13 @@ func Test_createPodFromComponent(t *testing.T) { Name: "http", ContainerPort: 20001, HostPort: 20001, + HostIP: "127.0.0.1", }) pod.Spec.Containers[1].Ports = append(pod.Spec.Containers[1].Ports, corev1.ContainerPort{ Name: "debug", ContainerPort: 20002, HostPort: 20002, + HostIP: "127.0.0.1", }) return pod }, @@ -689,18 +698,21 @@ func Test_createPodFromComponent(t *testing.T) { ContainerPort: 20001, HostPort: 20003, Protocol: corev1.ProtocolTCP, + HostIP: "127.0.0.1", }) pod.Spec.Containers[0].Ports = append(pod.Spec.Containers[0].Ports, corev1.ContainerPort{ Name: "debug", ContainerPort: 20002, HostPort: 20004, Protocol: corev1.ProtocolTCP, + HostIP: "127.0.0.1", }) pod.Spec.Containers[0].Ports = append(pod.Spec.Containers[0].Ports, corev1.ContainerPort{ Name: "debug-1", ContainerPort: 5858, HostPort: 20005, Protocol: corev1.ProtocolTCP, + HostIP: "127.0.0.1", }) return pod }, @@ -769,16 +781,19 @@ func Test_createPodFromComponent(t *testing.T) { Name: "http", ContainerPort: 20003, HostPort: 20003, + HostIP: "127.0.0.1", }) pod.Spec.Containers[1].Ports = append(pod.Spec.Containers[1].Ports, corev1.ContainerPort{ Name: "debug", ContainerPort: 20004, HostPort: 20004, + HostIP: "127.0.0.1", }) pod.Spec.Containers[1].Ports = append(pod.Spec.Containers[1].Ports, corev1.ContainerPort{ Name: "debug-1", ContainerPort: 20005, HostPort: 20005, + HostIP: "127.0.0.1", }) return pod }, @@ -851,12 +866,14 @@ func Test_createPodFromComponent(t *testing.T) { ContainerPort: 20001, HostPort: 20002, Protocol: corev1.ProtocolTCP, + HostIP: "127.0.0.1", }) pod.Spec.Containers[0].Ports = append(pod.Spec.Containers[0].Ports, corev1.ContainerPort{ Name: "http-1", ContainerPort: 8080, HostPort: 20003, Protocol: corev1.ProtocolTCP, + HostIP: "127.0.0.1", }) return pod }, @@ -920,11 +937,13 @@ func Test_createPodFromComponent(t *testing.T) { Name: "http", ContainerPort: 20002, HostPort: 20002, + HostIP: "127.0.0.1", }) pod.Spec.Containers[1].Ports = append(pod.Spec.Containers[1].Ports, corev1.ContainerPort{ Name: "http-1", ContainerPort: 20003, HostPort: 20003, + HostIP: "127.0.0.1", }) return pod }, @@ -1002,12 +1021,14 @@ func Test_createPodFromComponent(t *testing.T) { ContainerPort: 8080, HostPort: 8080, Protocol: corev1.ProtocolTCP, + HostIP: "127.0.0.1", }, { Name: "debug", ContainerPort: 5858, HostPort: 20001, Protocol: corev1.ProtocolTCP, + HostIP: "127.0.0.1", }, } container2 := pod.Spec.Containers[0].DeepCopy() @@ -1018,6 +1039,7 @@ func Test_createPodFromComponent(t *testing.T) { ContainerPort: 5000, HostPort: 20002, Protocol: corev1.ProtocolTCP, + HostIP: "127.0.0.1", }, } pod.Spec.Containers = append(pod.Spec.Containers, *container2) @@ -1109,11 +1131,13 @@ func Test_createPodFromComponent(t *testing.T) { ContainerPort: 8080, HostPort: 8080, Protocol: corev1.ProtocolTCP, + HostIP: "127.0.0.1", }, { Name: "debug", ContainerPort: 5858, HostPort: 20001, Protocol: corev1.ProtocolTCP, + HostIP: "127.0.0.1", }, } container2 := pod.Spec.Containers[0].DeepCopy() @@ -1124,6 +1148,7 @@ func Test_createPodFromComponent(t *testing.T) { ContainerPort: 5000, HostPort: 5000, Protocol: corev1.ProtocolTCP, + HostIP: "127.0.0.1", }, } pod.Spec.Containers = append(pod.Spec.Containers, *container2) @@ -1223,12 +1248,14 @@ func Test_createPodFromComponent(t *testing.T) { ContainerPort: 8080, HostPort: 20001, Protocol: corev1.ProtocolTCP, + HostIP: "127.0.0.1", }, { Name: "debug", ContainerPort: 5858, HostPort: 20003, Protocol: corev1.ProtocolTCP, + HostIP: "127.0.0.1", }, } container2 := pod.Spec.Containers[0].DeepCopy() @@ -1239,12 +1266,14 @@ func Test_createPodFromComponent(t *testing.T) { ContainerPort: 9000, HostPort: 20002, Protocol: corev1.ProtocolTCP, + HostIP: "127.0.0.1", }, { Name: "http-5000", ContainerPort: 5000, HostPort: 5000, Protocol: corev1.ProtocolTCP, + HostIP: "127.0.0.1", }, } pod.Spec.Containers = append(pod.Spec.Containers, *container2) @@ -1289,6 +1318,91 @@ func Test_createPodFromComponent(t *testing.T) { }, }, }, + + { + name: "basic component + application endpoint + debug endpoint + container ports known - with debug / using customAddress", + args: args{ + devfileObj: func() parser.DevfileObj { + data, _ := data.NewDevfileData(string(data.APISchemaVersion200)) + _ = data.AddCommands([]v1alpha2.Command{command}) + cmp := baseComponent.DeepCopy() + cmp.Container.Endpoints = append(cmp.Container.Endpoints, v1alpha2.Endpoint{ + Name: "http", + TargetPort: 20001, + }) + cmp.Container.Endpoints = append(cmp.Container.Endpoints, v1alpha2.Endpoint{ + Name: "debug", + TargetPort: 20002, + }) + cmp.Container.Endpoints = append(cmp.Container.Endpoints, v1alpha2.Endpoint{ + Name: "debug-1", + TargetPort: 5858, + }) + _ = data.AddComponents([]v1alpha2.Component{*cmp}) + return parser.DevfileObj{ + Data: data, + } + }, + componentName: devfileName, + appName: appName, + debug: true, + customAddress: "192.168.0.1", + }, + wantPod: func(basePod *corev1.Pod) *corev1.Pod { + pod := basePod.DeepCopy() + pod.Spec.Containers[0].Ports = append(pod.Spec.Containers[0].Ports, corev1.ContainerPort{ + Name: "http", + ContainerPort: 20001, + HostPort: 20003, + Protocol: corev1.ProtocolTCP, + HostIP: "192.168.0.1", + }) + pod.Spec.Containers[0].Ports = append(pod.Spec.Containers[0].Ports, corev1.ContainerPort{ + Name: "debug", + ContainerPort: 20002, + HostPort: 20004, + Protocol: corev1.ProtocolTCP, + HostIP: "192.168.0.1", + }) + pod.Spec.Containers[0].Ports = append(pod.Spec.Containers[0].Ports, corev1.ContainerPort{ + Name: "debug-1", + ContainerPort: 5858, + HostPort: 20005, + Protocol: corev1.ProtocolTCP, + HostIP: "192.168.0.1", + }) + return pod + }, + wantFwPorts: []api.ForwardedPort{ + { + Platform: "podman", + ContainerName: "mycomponent", + PortName: "http", + LocalAddress: "192.168.0.1", + LocalPort: 20003, + ContainerPort: 20001, + IsDebug: false, + }, + { + Platform: "podman", + ContainerName: "mycomponent", + PortName: "debug", + LocalAddress: "192.168.0.1", + LocalPort: 20004, + ContainerPort: 20002, + IsDebug: true, + }, + { + Platform: "podman", + ContainerName: "mycomponent", + PortName: "debug-1", + LocalAddress: "192.168.0.1", + LocalPort: 20005, + ContainerPort: 5858, + IsDebug: true, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1308,6 +1422,7 @@ func Test_createPodFromComponent(t *testing.T) { false, tt.args.customForwardedPorts, []int{20001, 20002, 20003, 20004, 20005}, + tt.args.customAddress, ) if (err != nil) != tt.wantErr { t.Errorf("createPodFromComponent() error = %v, wantErr %v", err, tt.wantErr) diff --git a/pkg/dev/podmandev/reconcile.go b/pkg/dev/podmandev/reconcile.go index 2629876f82f..6fbf6ec2a25 100644 --- a/pkg/dev/podmandev/reconcile.go +++ b/pkg/dev/podmandev/reconcile.go @@ -142,7 +142,7 @@ func (o *DevClient) reconcile( if options.ForwardLocalhost { // Port-forwarding is enabled by executing dedicated socat commands - err = o.portForwardClient.StartPortForwarding(ctx, devfileObj, componentName, options.Debug, options.RandomPorts, options.Out, options.ErrOut, fwPorts) + err = o.portForwardClient.StartPortForwarding(ctx, devfileObj, componentName, options.Debug, options.RandomPorts, options.Out, options.ErrOut, fwPorts, options.CustomAddress) if err != nil { return common.NewErrPortForward(err) } @@ -209,6 +209,7 @@ func (o *DevClient) deployPod(ctx context.Context, options dev.StartOptions) (*c options.RandomPorts, options.CustomForwardedPorts, o.usedPorts, + options.CustomAddress, ) if err != nil { return nil, nil, err diff --git a/pkg/kclient/interface.go b/pkg/kclient/interface.go index 1849f90f881..4f53469444f 100644 --- a/pkg/kclient/interface.go +++ b/pkg/kclient/interface.go @@ -119,7 +119,7 @@ type ClientInterface interface { // SetupPortForwarding creates port-forwarding for the pod on the port pairs provided in the // ["":""] format. errOut is used by the client-go library to output any errors // encountered while the port-forwarding is running - SetupPortForwarding(pod *corev1.Pod, portPairs []string, out io.Writer, errOut io.Writer, stopChan chan struct{}) error + SetupPortForwarding(pod *corev1.Pod, portPairs []string, out io.Writer, errOut io.Writer, stopChan chan struct{}, address string) error // projects.go CreateNewProject(projectName string, wait bool) error diff --git a/pkg/kclient/mock_Client.go b/pkg/kclient/mock_Client.go index 6fa6bb3d47b..9216d9037aa 100644 --- a/pkg/kclient/mock_Client.go +++ b/pkg/kclient/mock_Client.go @@ -1398,17 +1398,17 @@ func (mr *MockClientInterfaceMockRecorder) SetNamespace(ns interface{}) *gomock. } // SetupPortForwarding mocks base method. -func (m *MockClientInterface) SetupPortForwarding(pod *v12.Pod, portPairs []string, out, errOut io.Writer, stopChan chan struct{}) error { +func (m *MockClientInterface) SetupPortForwarding(pod *v12.Pod, portPairs []string, out, errOut io.Writer, stopChan chan struct{}, address string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetupPortForwarding", pod, portPairs, out, errOut, stopChan) + ret := m.ctrl.Call(m, "SetupPortForwarding", pod, portPairs, out, errOut, stopChan, address) ret0, _ := ret[0].(error) return ret0 } // SetupPortForwarding indicates an expected call of SetupPortForwarding. -func (mr *MockClientInterfaceMockRecorder) SetupPortForwarding(pod, portPairs, out, errOut, stopChan interface{}) *gomock.Call { +func (mr *MockClientInterfaceMockRecorder) SetupPortForwarding(pod, portPairs, out, errOut, stopChan, address interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetupPortForwarding", reflect.TypeOf((*MockClientInterface)(nil).SetupPortForwarding), pod, portPairs, out, errOut, stopChan) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetupPortForwarding", reflect.TypeOf((*MockClientInterface)(nil).SetupPortForwarding), pod, portPairs, out, errOut, stopChan, address) } // TryWithBlockOwnerDeletion mocks base method. diff --git a/pkg/kclient/port_forwarding.go b/pkg/kclient/port_forwarding.go index ce13544ac7b..6c28e89add9 100644 --- a/pkg/kclient/port_forwarding.go +++ b/pkg/kclient/port_forwarding.go @@ -9,7 +9,10 @@ import ( "k8s.io/client-go/transport/spdy" ) -func (c *Client) SetupPortForwarding(pod *corev1.Pod, portPairs []string, out io.Writer, errOut io.Writer, stopChan chan struct{}) error { +func (c *Client) SetupPortForwarding(pod *corev1.Pod, portPairs []string, out io.Writer, errOut io.Writer, stopChan chan struct{}, address string) error { + if address == "" { + address = "localhost" + } transport, upgrader, err := spdy.RoundTripperFor(c.GetClientConfig()) if err != nil { return err @@ -20,7 +23,7 @@ func (c *Client) SetupPortForwarding(pod *corev1.Pod, portPairs []string, out io dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", req.URL()) // passing nil for readyChan because it's eventually being closed if it's not nil // passing nil for out because we only care for error, not for output messages; we want to print our own messages - fw, err := portforward.New(dialer, portPairs, stopChan, nil, out, errOut) + fw, err := portforward.NewOnAddresses(dialer, []string{address}, portPairs, stopChan, nil, out, errOut) if err != nil { return err } diff --git a/pkg/odo/cli/dev/dev.go b/pkg/odo/cli/dev/dev.go index 4a24fc8e6ff..68ad295041e 100644 --- a/pkg/odo/cli/dev/dev.go +++ b/pkg/odo/cli/dev/dev.go @@ -15,6 +15,7 @@ import ( "github.com/spf13/cobra" "k8s.io/klog/v2" ktemplates "k8s.io/kubectl/pkg/util/templates" + netutils "k8s.io/utils/net" "github.com/redhat-developer/odo/pkg/api" "github.com/redhat-developer/odo/pkg/component" @@ -68,6 +69,7 @@ type DevOptions struct { ignoreLocalhostFlag bool forwardLocalhostFlag bool portForwardFlag []string + addressFlag string } var _ genericclioptions.Runnable = (*DevOptions)(nil) @@ -144,7 +146,12 @@ func (o *DevOptions) Validate(ctx context.Context) error { if o.randomPortsFlag && o.portForwardFlag != nil { return errors.New("--random-ports and --port-forward cannot be used together") } - + // Validate the custom address and return an error (if any) early on, if we do not validate here, it will only throw an error at the stage of port forwarding. + if o.addressFlag != "" { + if err := validateCustomAddress(o.addressFlag); err != nil { + return err + } + } if o.portForwardFlag != nil { containerEndpointMapping, err := libdevfile.GetDevfileContainerEndpointMapping(devfileObj, true) if err != nil { @@ -157,7 +164,7 @@ func (o *DevOptions) Validate(ctx context.Context) error { } o.forwardedPorts = forwardedPorts - errStrings, err := validatePortForwardFlagData(forwardedPorts, containerEndpointMapping) + errStrings, err := validatePortForwardFlagData(forwardedPorts, containerEndpointMapping, o.addressFlag) if len(errStrings) != 0 { log.Error("There are following issues with values provided by --port-forward flag:") for _, errStr := range errStrings { @@ -249,6 +256,7 @@ func (o *DevOptions) Run(ctx context.Context) (err error) { ForwardLocalhost: o.forwardLocalhostFlag, Variables: variables, CustomForwardedPorts: o.forwardedPorts, + CustomAddress: o.addressFlag, Out: o.out, ErrOut: o.errOut, }, @@ -299,8 +307,8 @@ It forwards endpoints with any exposure values ('public', 'internal' or 'none') devCmd.Flags().BoolVar(&o.forwardLocalhostFlag, "forward-localhost", false, "Whether to enable port-forwarding if app is listening on the container loopback interface. Applicable only if platform is podman.") devCmd.Flags().StringArrayVar(&o.portForwardFlag, "port-forward", nil, - "Define custom port mapping for port forwarding. Acceptable formats: LOCAL_PORT:REMOTE_PORT, LOCAL_PORT:CONTAINER_NAME:REMOTE_PORT. Currently, it is applicable only if platform is cluster.") - + "Define custom port mapping for port forwarding. Acceptable formats: LOCAL_PORT:REMOTE_PORT, LOCAL_PORT:CONTAINER_NAME:REMOTE_PORT.") + devCmd.Flags().StringVar(&o.addressFlag, "address", "127.0.0.1", "Define custom address for port forwarding.") clientset.Add(devCmd, clientset.BINDING, clientset.DEV, @@ -327,7 +335,7 @@ It forwards endpoints with any exposure values ('public', 'internal' or 'none') // 1. Every container port defined by the flag is present in the devfile // 2. Every local port defined by the flag is unique // 3. If multiple containers have the same container port, the validation fails and asks the user to provide container names -func validatePortForwardFlagData(forwardedPorts []api.ForwardedPort, containerEndpointMapping map[string][]v1alpha2.Endpoint) ([]string, error) { +func validatePortForwardFlagData(forwardedPorts []api.ForwardedPort, containerEndpointMapping map[string][]v1alpha2.Endpoint, address string) ([]string, error) { var errors []string // Validate that local ports present in forwardedPorts are unique var localPorts = make(map[int]struct{}) @@ -349,7 +357,9 @@ func validatePortForwardFlagData(forwardedPorts []api.ForwardedPort, containerEn portContainerMapping[endpoint.TargetPort] = append(portContainerMapping[endpoint.TargetPort], container) } } - + if address == "" { + address = "127.0.0.1" + } // Check that all container ports are valid and present in the Devfile portLoop: for _, fPort := range forwardedPorts { @@ -388,6 +398,12 @@ portLoop: errors = append(errors, fmt.Sprintf("container port %d not found in the devfile container endpoints", fPort.ContainerPort)) } } + for _, fPort := range forwardedPorts { + if !util.IsPortFree(fPort.LocalPort, address) { + errors = append(errors, fmt.Sprintf("local port %d is already in use on address %s", fPort.LocalPort, address)) + } + } + if len(errors) != 0 { return errors, fmt.Errorf("values for --port-forward flag are invalid") } @@ -424,3 +440,15 @@ func parsePortForwardFlag(portForwardFlag []string) (forwardedPorts []api.Forwar } return forwardedPorts, nil } + +// validateCustomAddress validates if the provided ip address is valid; +// it uses the same checks as defined by func parseAddresses() in "k8s.io/client-go/tools/portforward" +func validateCustomAddress(address string) error { + if address == "localhost" { + return nil + } + if netutils.ParseIPSloppy(address).To4() != nil || netutils.ParseIPSloppy(address) != nil { + return nil + } + return fmt.Errorf("%s is an invalid ip address", address) +} diff --git a/pkg/odo/cli/dev/dev_test.go b/pkg/odo/cli/dev/dev_test.go index f6c7e0d9352..bbad8a348af 100644 --- a/pkg/odo/cli/dev/dev_test.go +++ b/pkg/odo/cli/dev/dev_test.go @@ -1,17 +1,26 @@ package dev import ( + "fmt" "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/redhat-developer/odo/pkg/api" + "net" + "net/http" + "net/http/httptest" "testing" ) func Test_validatePortForwardFlagData(t *testing.T) { + type serverCloser interface { + Close() + } type args struct { forwardedPorts []api.ForwardedPort containerEndpointMapping map[string][]v1alpha2.Endpoint + address string + setupServerFunc func(address string) (serverCloser, error) } tests := []struct { name string @@ -160,10 +169,45 @@ func Test_validatePortForwardFlagData(t *testing.T) { wantErr: true, wantErrStrings: []string{"container port 3000 does not match any endpoints of container \"runtime\" in the devfile", "multiple container components (runtime, tools) found with same container port 5858 in the devfile, port forwarding must be defined with format ::", "container \"invisible\" not found in the devfile", "local port 9000 is used more than once, please use unique local ports"}, }, + { + name: "local port is busy", + args: args{ + forwardedPorts: []api.ForwardedPort{ + {LocalPort: 9000, ContainerPort: 9000}, + }, + containerEndpointMapping: map[string][]v1alpha2.Endpoint{ + "runtime": {{TargetPort: 9000}}, + }, + setupServerFunc: func(address string) (serverCloser, error) { + l, err := net.Listen("tcp", fmt.Sprintf("%s:9000", address)) + if err != nil { + return nil, err + } + s := &httptest.Server{ + Listener: l, + Config: &http.Server{}, + } + s.Start() + + return s, nil + }, + address: "localhost", + }, + wantErr: true, + wantErrStrings: []string{"local port 9000 is already in use on address localhost"}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - errStrings, err := validatePortForwardFlagData(tt.args.forwardedPorts, tt.args.containerEndpointMapping) + if tt.args.setupServerFunc != nil { + sCloser, err := tt.args.setupServerFunc(tt.args.address) + if err != nil { + t.Errorf("failed to setup server: %s", err.Error()) + return + } + defer sCloser.Close() + } + errStrings, err := validatePortForwardFlagData(tt.args.forwardedPorts, tt.args.containerEndpointMapping, tt.args.address) if (err != nil) != tt.wantErr { t.Errorf("validatePortForwardFlagData() error = %v, wantErr %v", err, tt.wantErr) return @@ -285,3 +329,72 @@ func Test_parsePortForwardFlag(t *testing.T) { }) } } + +func Test_validateCustomAddress(t *testing.T) { + type args struct { + address string + } + tests := []struct { + name string + args args + wantErr bool + }{ + // TODO: Add test cases. + { + name: "--address=localhost", + args: args{ + address: "localhost", + }, + wantErr: false, + }, + { + name: "--address is a valid IPv4", + args: args{ + address: "192.168.0.1", + }, + wantErr: false, + }, + { + name: "--address=0.0.0.0", + args: args{ + address: "0.0.0.0", + }, + wantErr: false, + }, + { + name: "--address is a valid IPv6 address", + args: args{ + address: "ABCD:EF01:2345:6789:ABCD:EF01:2345:6789", + }, + wantErr: false, + }, + { + name: "--address=::1", + args: args{ + address: "::1", + }, + wantErr: false, + }, + { + name: "--address is not a valid address", + args: args{ + address: "e9:9e:06:ee:8c:4c", + }, + wantErr: true, + }, + { + name: "--address=something", + args: args{ + address: "something", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := validateCustomAddress(tt.args.address); (err != nil) != tt.wantErr { + t.Errorf("validateCustomAddress() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/portForward/interface.go b/pkg/portForward/interface.go index f4c5c84f4f8..ab841f50f7f 100644 --- a/pkg/portForward/interface.go +++ b/pkg/portForward/interface.go @@ -25,6 +25,7 @@ type Client interface { out io.Writer, errOut io.Writer, definedPorts []api.ForwardedPort, + customAddress string, ) error // StopPortForwarding stops the port forwarding for the specified component. diff --git a/pkg/portForward/kubeportforward/portForward.go b/pkg/portForward/kubeportforward/portForward.go index bddea02c0d2..187d2844ee7 100644 --- a/pkg/portForward/kubeportforward/portForward.go +++ b/pkg/portForward/kubeportforward/portForward.go @@ -50,7 +50,7 @@ func NewPFClient(kubernetesClient kclient.ClientInterface, stateClient state.Cli } } -func (o *PFClient) StartPortForwarding(ctx context.Context, devFileObj parser.DevfileObj, componentName string, debug bool, randomPorts bool, out io.Writer, errOut io.Writer, definedPorts []api.ForwardedPort) error { +func (o *PFClient) StartPortForwarding(ctx context.Context, devFileObj parser.DevfileObj, componentName string, debug bool, randomPorts bool, out io.Writer, errOut io.Writer, definedPorts []api.ForwardedPort, customAddress string) error { if randomPorts && len(definedPorts) != 0 { return errors.New("cannot use randomPorts and custom definePorts together") } @@ -75,11 +75,11 @@ func (o *PFClient) StartPortForwarding(ctx context.Context, devFileObj parser.De var portPairs map[string][]string if len(definedPorts) != 0 { - portPairs = getCustomPortPairs(definedPorts, ceMapping) + portPairs = getCustomPortPairs(definedPorts, ceMapping, customAddress) } else if randomPorts { portPairs = randomPortPairsFromContainerEndpoints(ceMapping) } else { - portPairs = portPairsFromContainerEndpoints(ceMapping) + portPairs = portPairsFromContainerEndpoints(ceMapping, customAddress) } var portPairsSlice []string for _, v1 := range portPairs { @@ -111,7 +111,7 @@ func (o *PFClient) StartPortForwarding(ctx context.Context, devFileObj parser.De backo := watch.NewExpBackoff() for { o.finishedChan = make(chan struct{}, 1) - portsBuf := NewPortWriter(log.GetStdout(), len(portPairsSlice), ceMapping) + portsBuf := NewPortWriter(log.GetStdout(), len(portPairsSlice), ceMapping, customAddress) go func() { portsBuf.Wait() @@ -122,7 +122,7 @@ func (o *PFClient) StartPortForwarding(ctx context.Context, devFileObj parser.De devstateChan <- err }() - err = o.kubernetesClient.SetupPortForwarding(pod, portPairsSlice, portsBuf, errOut, o.stopChan) + err = o.kubernetesClient.SetupPortForwarding(pod, portPairsSlice, portsBuf, errOut, o.stopChan, customAddress) if err != nil { fmt.Fprintf(errOut, "Failed to setup port-forwarding: %v\n", err) d := backo.Delay() @@ -171,7 +171,7 @@ func (o *PFClient) GetForwardedPorts() map[string][]v1alpha2.Endpoint { // getCustomPortPairs assigns custom port on localhost to a container port if provided by the definedPorts config, // if not, it assigns a port starting from 20001 as done in portPairsFromContainerEndpoints -func getCustomPortPairs(definedPorts []api.ForwardedPort, ceMapping map[string][]v1alpha2.Endpoint) map[string][]string { +func getCustomPortPairs(definedPorts []api.ForwardedPort, ceMapping map[string][]v1alpha2.Endpoint, address string) map[string][]string { portPairs := make(map[string][]string) usedPorts := make(map[int]struct{}) for _, dPort := range definedPorts { @@ -210,7 +210,7 @@ func getCustomPortPairs(definedPorts []api.ForwardedPort, ceMapping map[string][ if freePort == 0 { for { var err error - freePort, err = util.NextFreePort(startPort, endPort, nil) + freePort, err = util.NextFreePort(startPort, endPort, nil, address) if err != nil { klog.Infof("%s", err) continue @@ -250,13 +250,13 @@ func randomPortPairsFromContainerEndpoints(ceMap map[string][]v1alpha2.Endpoint) // portPairsFromContainerEndpoints assigns a port on localhost to each port in the provided containerEndpoints map // it returns a map of the format "":{":", ":"} // "container1": {"20001:3000", "20002:3001"} -func portPairsFromContainerEndpoints(ceMap map[string][]v1alpha2.Endpoint) map[string][]string { +func portPairsFromContainerEndpoints(ceMap map[string][]v1alpha2.Endpoint, address string) map[string][]string { portPairs := make(map[string][]string) startPort := 20001 endPort := startPort + 10000 for name, ports := range ceMap { for _, p := range ports { - freePort, err := util.NextFreePort(startPort, endPort, nil) + freePort, err := util.NextFreePort(startPort, endPort, nil, address) if err != nil { klog.Infof("%s", err) continue diff --git a/pkg/portForward/kubeportforward/portForward_test.go b/pkg/portForward/kubeportforward/portForward_test.go index bf4a4332f40..5b2b536b0ab 100644 --- a/pkg/portForward/kubeportforward/portForward_test.go +++ b/pkg/portForward/kubeportforward/portForward_test.go @@ -72,7 +72,7 @@ func Test_getCompleteCustomPortPairs(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotPortPairs := getCustomPortPairs(tt.args.definedPorts, tt.args.ceMapping) + gotPortPairs := getCustomPortPairs(tt.args.definedPorts, tt.args.ceMapping, "") if diff := cmp.Diff(gotPortPairs, tt.wantPortPairs); diff != "" { t.Errorf("getCompleteCustomPortPairs() (got vs want) diff = %v", diff) } diff --git a/pkg/portForward/kubeportforward/writer.go b/pkg/portForward/kubeportforward/writer.go index 938a658cb1d..ce23626e8ee 100644 --- a/pkg/portForward/kubeportforward/writer.go +++ b/pkg/portForward/kubeportforward/writer.go @@ -23,27 +23,32 @@ type PortWriter struct { end chan bool len int // mapping indicates the list of endpoints open by containers - mapping map[string][]v1alpha2.Endpoint - fwPorts []api.ForwardedPort + mapping map[string][]v1alpha2.Endpoint + fwPorts []api.ForwardedPort + customAddress string } // NewPortWriter creates a writer that will write the content in buffer, // and Wait will return after strings "Forwarding from 127.0.0.1:" has been written "len" times -func NewPortWriter(buffer io.Writer, len int, mapping map[string][]v1alpha2.Endpoint) *PortWriter { +func NewPortWriter(buffer io.Writer, len int, mapping map[string][]v1alpha2.Endpoint, customAddress string) *PortWriter { return &PortWriter{ - buffer: buffer, - len: len, - end: make(chan bool), - mapping: mapping, + buffer: buffer, + len: len, + end: make(chan bool), + mapping: mapping, + customAddress: customAddress, } } func (o *PortWriter) Write(buf []byte) (n int, err error) { + if o.customAddress == "" { + o.customAddress = "127.0.0.1" + } s := string(buf) - if strings.HasPrefix(s, "Forwarding from 127.0.0.1") { + if strings.HasPrefix(s, fmt.Sprintf("Forwarding from %s", o.customAddress)) { - fwPort, err := getForwardedPort(o.mapping, s) + fwPort, err := getForwardedPort(o.mapping, s, o.customAddress) if err == nil { o.fwPorts = append(o.fwPorts, fwPort) } else { @@ -68,8 +73,11 @@ func (o *PortWriter) GetForwardedPorts() []api.ForwardedPort { return o.fwPorts } -func getForwardedPort(mapping map[string][]v1alpha2.Endpoint, s string) (api.ForwardedPort, error) { - regex := regexp.MustCompile(`Forwarding from 127.0.0.1:([0-9]+) -> ([0-9]+)`) +func getForwardedPort(mapping map[string][]v1alpha2.Endpoint, s string, address string) (api.ForwardedPort, error) { + if address == "" { + address = "127.0.0.1" + } + regex := regexp.MustCompile(fmt.Sprintf(`Forwarding from %s:([0-9]+) -> ([0-9]+)`, address)) matches := regex.FindStringSubmatch(s) if len(matches) < 3 { return api.ForwardedPort{}, errors.New("unable to analyze port forwarding string") @@ -83,7 +91,7 @@ func getForwardedPort(mapping map[string][]v1alpha2.Endpoint, s string) (api.For return api.ForwardedPort{}, err } fp := api.ForwardedPort{ - LocalAddress: "127.0.0.1", + LocalAddress: address, LocalPort: localPort, ContainerPort: remotePort, } diff --git a/pkg/portForward/kubeportforward/writer_test.go b/pkg/portForward/kubeportforward/writer_test.go index 3342ac52efe..ea1c89bdf50 100644 --- a/pkg/portForward/kubeportforward/writer_test.go +++ b/pkg/portForward/kubeportforward/writer_test.go @@ -13,6 +13,7 @@ func Test_getForwardedPort(t *testing.T) { type args struct { mapping map[string][]v1alpha2.Endpoint s string + address string } tests := []struct { name string @@ -45,6 +46,32 @@ func Test_getForwardedPort(t *testing.T) { }, wantErr: false, }, + { + name: "find port in container and use custom address", + args: args{ + mapping: map[string][]v1alpha2.Endpoint{ + "container1": { + v1alpha2.Endpoint{Name: "port-11", TargetPort: 3000}, + v1alpha2.Endpoint{Name: "debug-11", TargetPort: 4200}, + }, + "container2": { + v1alpha2.Endpoint{Name: "port-21", TargetPort: 80}, + v1alpha2.Endpoint{Name: "port-22", TargetPort: 8080}, + }, + }, + s: "Forwarding from 192.168.0.1:40407 -> 3000", + address: "192.168.0.1", + }, + want: api.ForwardedPort{ + ContainerName: "container1", + PortName: "port-11", + LocalAddress: "192.168.0.1", + IsDebug: false, + LocalPort: 40407, + ContainerPort: 3000, + }, + wantErr: false, + }, { name: "string error", args: args{ @@ -91,7 +118,7 @@ func Test_getForwardedPort(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := getForwardedPort(tt.args.mapping, tt.args.s) + got, err := getForwardedPort(tt.args.mapping, tt.args.s, tt.args.address) if (err != nil) != tt.wantErr { t.Errorf("getForwardedPort() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/pkg/portForward/podmanportforward/portForward.go b/pkg/portForward/podmanportforward/portForward.go index ec8b143d4b9..84dc456d791 100644 --- a/pkg/portForward/podmanportforward/portForward.go +++ b/pkg/portForward/podmanportforward/portForward.go @@ -45,6 +45,7 @@ func (o *PFClient) StartPortForwarding( out io.Writer, errOut io.Writer, definedPorts []api.ForwardedPort, + customAddress string, ) error { var appliedPorts []api.ForwardedPort for port := range o.appliedPorts { diff --git a/pkg/util/util.go b/pkg/util/util.go index d40c7224e2d..5feb4ca4ca3 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -781,9 +781,12 @@ func SafeGetBool(b *bool) bool { return *b } -// IsPortFree checks if the port on localhost is free to use -func IsPortFree(port int) bool { - address := fmt.Sprintf("0.0.0.0:%d", port) +// IsPortFree checks if the port on a given address is free to use +func IsPortFree(port int, localAddress string) bool { + if localAddress == "" { + localAddress = "127.0.0.1" + } + address := fmt.Sprintf("%s:%d", localAddress, port) listener, err := net.Listen("tcp", address) if err != nil { return false @@ -795,7 +798,8 @@ func IsPortFree(port int) bool { // NextFreePort returns the next free port on system, starting at start // end finishing at end. // If no port is found in the range [start, end], 0 is returned -func NextFreePort(start, end int, usedPorts []int) (int, error) { +func NextFreePort(start, end int, usedPorts []int, address string) (int, error) { + port := start for { for _, usedPort := range usedPorts { @@ -803,7 +807,7 @@ func NextFreePort(start, end int, usedPorts []int) (int, error) { return port, nil } } - if IsPortFree(port) { + if IsPortFree(port, address) { return port, nil } port++ diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go index 0c4192ade35..d7da0a871d0 100644 --- a/pkg/util/util_test.go +++ b/pkg/util/util_test.go @@ -2742,7 +2742,8 @@ func TestIsPortFree(t *testing.T) { } type args struct { port int - portProvider func() (int, serverCloser, error) + portProvider func(address string) (int, serverCloser, error) + address string } type test struct { name string @@ -2763,7 +2764,7 @@ func TestIsPortFree(t *testing.T) { { name: "random port bound on 127.0.0.1", args: args{ - portProvider: func() (int, serverCloser, error) { + portProvider: func(address string) (int, serverCloser, error) { s := httptest.NewServer(nil) _, p, err := net.SplitHostPort(strings.TrimPrefix(s.URL, "http://")) if err != nil { @@ -2781,7 +2782,7 @@ func TestIsPortFree(t *testing.T) { { name: "random port bound on 127.0.0.1 and checking 0 as input", args: args{ - portProvider: func() (int, serverCloser, error) { + portProvider: func(address string) (int, serverCloser, error) { s := httptest.NewServer(nil) return 0, s, nil }, @@ -2791,9 +2792,9 @@ func TestIsPortFree(t *testing.T) { { name: "random port bound on 0.0.0.0 and checking 0 as input", args: args{ - portProvider: func() (int, serverCloser, error) { + portProvider: func(address string) (int, serverCloser, error) { // Intentionally not using httptest.Server, which listens to 127.0.0.1 - l, err := net.Listen("tcp", "0.0.0.0:0") + l, err := net.Listen("tcp", fmt.Sprintf("%s:0", address)) if err != nil { return 0, nil, err } @@ -2805,15 +2806,16 @@ func TestIsPortFree(t *testing.T) { return 0, s, nil }, + address: "0.0.0.0", }, want: true, }, { name: "random port bound on 0.0.0.0", args: args{ - portProvider: func() (int, serverCloser, error) { + portProvider: func(address string) (int, serverCloser, error) { // Intentionally not using httptest.Server, which listens to 127.0.0.1 - l, err := net.Listen("tcp", "0.0.0.0:0") + l, err := net.Listen("tcp", fmt.Sprintf("%s:0", address)) if err != nil { return 0, nil, err } @@ -2833,6 +2835,7 @@ func TestIsPortFree(t *testing.T) { } return port, s, nil }, + address: "0.0.0.0", }, want: false, }, @@ -2844,7 +2847,7 @@ func TestIsPortFree(t *testing.T) { var s serverCloser var err error if tt.args.portProvider != nil { - port, s, err = tt.args.portProvider() + port, s, err = tt.args.portProvider(tt.args.address) if s != nil { defer s.Close() } @@ -2854,7 +2857,7 @@ func TestIsPortFree(t *testing.T) { } } - if got := IsPortFree(port); got != tt.want { + if got := IsPortFree(port, tt.args.address); got != tt.want { t.Errorf("IsPortFree() = %v, want %v", got, tt.want) } }) diff --git a/tests/examples/source/devfiles/go-devfiles/devfile.yaml b/tests/examples/source/devfiles/go-devfiles/devfile.yaml new file mode 100644 index 00000000000..a9fac1cb683 --- /dev/null +++ b/tests/examples/source/devfiles/go-devfiles/devfile.yaml @@ -0,0 +1,56 @@ +commands: +- exec: + commandLine: go build main.go + component: runtime + env: + - name: GOPATH + value: ${PROJECT_SOURCE}/.go + - name: GOCACHE + value: ${PROJECT_SOURCE}/.cache + group: + isDefault: true + kind: build + workingDir: ${PROJECT_SOURCE} + id: build +- exec: + commandLine: ./main + component: runtime + group: + isDefault: true + kind: run + workingDir: ${PROJECT_SOURCE} + id: run +components: +- container: + args: + - tail + - -f + - /dev/null + endpoints: + - name: http-go + targetPort: 8080 + image: registry.access.redhat.com/ubi9/go-toolset:1.18.9-14 + memoryLimit: 1024Mi + mountSources: true + name: runtime +metadata: + description: Go (version 1.18.x) is an open source programming language that makes + it easy to build simple, reliable, and efficient software. + displayName: Go Runtime + icon: https://raw.githubusercontent.com/devfile-samples/devfile-stack-icons/main/golang.svg + language: Go + name: my-go-app + projectType: Go + provider: Red Hat + tags: + - Go + version: 1.0.2 +schemaVersion: 2.1.0 +starterProjects: +- description: A Go project with a simple HTTP server + git: + checkoutFrom: + revision: main + remotes: + origin: https://github.com/devfile-samples/devfile-stack-go.git + name: go-starter diff --git a/tests/helper/helper_dev.go b/tests/helper/helper_dev.go index 8c2820dfd27..952b235587f 100644 --- a/tests/helper/helper_dev.go +++ b/tests/helper/helper_dev.go @@ -1,6 +1,7 @@ package helper import ( + "fmt" "os" "regexp" "time" @@ -112,6 +113,7 @@ type DevSession struct { session *gexec.Session stopped bool console *expect.Console + address string } type DevSessionOpts struct { @@ -121,6 +123,7 @@ type DevSessionOpts struct { TimeoutInSeconds int NoRandomPorts bool NoWatch bool + CustomAddress string } // StartDevMode starts a dev session with `odo dev` @@ -143,6 +146,9 @@ func StartDevMode(options DevSessionOpts) (devSession DevSession, out []byte, er if options.NoWatch { args = append(args, "--no-watch") } + if options.CustomAddress != "" { + args = append(args, "--address", options.CustomAddress) + } args = append(args, options.CmdlineArgs...) cmd := Cmd("odo", args...) cmd.Cmd.Stdin = c.Tty() @@ -158,6 +164,7 @@ func StartDevMode(options DevSessionOpts) (devSession DevSession, out []byte, er result := DevSession{ session: session, console: c, + address: options.CustomAddress, } outContents := session.Out.Contents() errContents := session.Err.Contents() @@ -169,7 +176,7 @@ func StartDevMode(options DevSessionOpts) (devSession DevSession, out []byte, er if err != nil { return DevSession{}, nil, nil, nil, err } - return result, outContents, errContents, getPorts(string(outContents)), nil + return result, outContents, errContents, getPorts(string(outContents), options.CustomAddress), nil } @@ -246,7 +253,7 @@ func (o DevSession) GetInfo() ([]byte, []byte, map[string]string, error) { if err != nil { return nil, nil, nil, err } - return outContents, errContents, getPorts(string(outContents)), nil + return outContents, errContents, getPorts(string(outContents), o.address), nil } func (o DevSession) CheckNotSynced(timeout time.Duration) { @@ -283,6 +290,9 @@ func WaitForDevModeToContain(options DevSessionOpts, substring string, stopSessi if options.RunOnPodman { args = append(args, "--platform", "podman") } + if options.CustomAddress != "" { + args = append(args, "--address", options.CustomAddress) + } session := Cmd("odo", args...).AddEnv(options.EnvVars...).Runner().session if checkErrOut { WaitForErroutToContain(substring, 360, 10, session) @@ -291,6 +301,7 @@ func WaitForDevModeToContain(options DevSessionOpts, substring string, stopSessi } result := DevSession{ session: session, + address: options.CustomAddress, } if stopSessionAfter { defer func() { @@ -315,9 +326,12 @@ func WaitForDevModeToContain(options DevSessionOpts, substring string, stopSessi // getPorts returns a map of ports redirected depending on the information in s // // `- Forwarding from 127.0.0.1:20001 -> 3000` will return { "3000": "127.0.0.1:20001" } -func getPorts(s string) map[string]string { +func getPorts(s, address string) map[string]string { + if address == "" { + address = "127.0.0.1" + } result := map[string]string{} - re := regexp.MustCompile("(127.0.0.1:[0-9]+) -> ([0-9]+)") + re := regexp.MustCompile(fmt.Sprintf("(%s:[0-9]+) -> ([0-9]+)", address)) matches := re.FindAllStringSubmatch(s, -1) for _, match := range matches { result[match[2]] = match[1] diff --git a/tests/integration/cmd_dev_debug_test.go b/tests/integration/cmd_dev_debug_test.go index 9d1019754cc..f3f421fa3d4 100644 --- a/tests/integration/cmd_dev_debug_test.go +++ b/tests/integration/cmd_dev_debug_test.go @@ -78,7 +78,7 @@ var _ = Describe("odo dev debug command tests", func() { It("should connect to relevant custom ports forwarded", func() { By("connecting to the application port", func() { - helper.HttpWaitForWithStatus("http://"+ports[ContainerPort], "Hello from Node.js Starter Application!", 12, 5, 200) + helper.HttpWaitForWithStatus(fmt.Sprintf("http://%s", ports[ContainerPort]), "Hello from Node.js Starter Application!", 12, 5, 200) }) By("expecting a ws connection when tried to connect on default debug port locally", func() { // 400 response expected because the endpoint expects a websocket request and we are doing a HTTP GET diff --git a/tests/integration/cmd_dev_test.go b/tests/integration/cmd_dev_test.go index e760af95bbd..43122702006 100644 --- a/tests/integration/cmd_dev_test.go +++ b/tests/integration/cmd_dev_test.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "regexp" + "runtime" "sort" "strconv" "strings" @@ -936,6 +937,111 @@ ComponentSettings: }) } + for _, podman := range []bool{true, false} { + podman := podman + Context(fmt.Sprintf("multiple dev sessions with different project are running on same platform (podman=%v), same port", podman), helper.LabelPodmanIf(podman, func() { + const ( + nodejsContainerPort = "3000" + goContainerPort = "8080" + nodejsCustomAddress = "127.0.10.3" + goCustomAddress = "127.0.10.1" + ) + var ( + nodejsProject, goProject string + nodejsDevSession, goDevSession helper.DevSession + nodejsPorts, goPorts map[string]string + nodejsLocalPort = helper.GetCustomStartPort() + goLocalPort = nodejsLocalPort + 1 + + nodejsURL = fmt.Sprintf("%s:%d", nodejsCustomAddress, nodejsLocalPort) + goURL = fmt.Sprintf("%s:%d", goCustomAddress, goLocalPort) + ) + BeforeEach(func() { + if runtime.GOOS == "darwin" { + Skip("cannot run this test out of the box on macOS because the test uses a custom address in the range 127.0.0/8 and for macOS we need to ensure the addresses are open for request before using them; Ref: https://superuser.com/questions/458875/how-do-you-get-loopback-addresses-other-than-127-0-0-1-to-work-on-os-x#458877") + } + nodejsProject = helper.CreateNewContext() + goProject = helper.CreateNewContext() + helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), nodejsProject) + helper.CopyExampleDevFile(filepath.Join("source", "devfiles", "nodejs", "devfile.yaml"), filepath.Join(nodejsProject, "devfile.yaml")) + helper.CopyExample(filepath.Join("source", "go"), goProject) + helper.CopyExampleDevFile(filepath.Join("source", "devfiles", "go-devfiles", "devfile.yaml"), filepath.Join(goProject, "devfile.yaml")) + }) + AfterEach(func() { + helper.DeleteDir(nodejsProject) + helper.DeleteDir(goProject) + }) + When("odo dev session is run for nodejs component", func() { + BeforeEach(func() { + helper.Chdir(nodejsProject) + var err error + nodejsDevSession, _, _, nodejsPorts, err = helper.StartDevMode(helper.DevSessionOpts{ + CmdlineArgs: []string{"--port-forward", fmt.Sprintf("%d:%s", nodejsLocalPort, nodejsContainerPort)}, + RunOnPodman: podman, + TimeoutInSeconds: 0, + NoRandomPorts: true, + CustomAddress: nodejsCustomAddress, + }) + Expect(err).ToNot(HaveOccurred()) + }) + AfterEach(func() { + nodejsDevSession.Stop() + nodejsDevSession.WaitEnd() + helper.Chdir(commonVar.Context) + }) + When("odo dev session is run for go project on the same port but different address", func() { + BeforeEach(func() { + helper.Chdir(goProject) + var err error + goDevSession, _, _, goPorts, err = helper.StartDevMode(helper.DevSessionOpts{ + CmdlineArgs: []string{"--port-forward", fmt.Sprintf("%d:%s", goLocalPort, goContainerPort)}, + RunOnPodman: podman, + TimeoutInSeconds: 0, + NoRandomPorts: true, + CustomAddress: goCustomAddress, + }) + Expect(err).ToNot(HaveOccurred()) + }) + AfterEach(func() { + goDevSession.Stop() + goDevSession.WaitEnd() + helper.Chdir(commonVar.Context) + }) + It("should be able to run both the sessions", func() { + Expect(nodejsPorts[nodejsContainerPort]).To(BeEquivalentTo(nodejsURL)) + Expect(goPorts[goContainerPort]).To(BeEquivalentTo(goURL)) + helper.HttpWaitForWithStatus(fmt.Sprintf("http://%s", nodejsURL), "Hello from Node.js Starter Application!", 1, 0, 200) + helper.HttpWaitForWithStatus(fmt.Sprintf("http://%s", goURL), "Hello, !", 1, 0, 200) + }) + When("go and nodejs files are modified", func() { + BeforeEach(func() { + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + _, _, _, err := nodejsDevSession.WaitSync() + Expect(err).ToNot(HaveOccurred()) + }() + go func() { + defer wg.Done() + _, _, _, err := goDevSession.WaitSync() + Expect(err).ToNot(HaveOccurred()) + }() + helper.ReplaceString(filepath.Join(goProject, "main.go"), "Hello, %s!", "H3110, %s!") + helper.ReplaceString(filepath.Join(nodejsProject, "server.js"), "Hello from Node.js", "H3110 from Node.js") + wg.Wait() + }) + It("should be possible to access both the projects on same address and port", func() { + Expect(nodejsPorts[nodejsContainerPort]).To(BeEquivalentTo(nodejsURL)) + Expect(goPorts[goContainerPort]).To(BeEquivalentTo(goURL)) + helper.HttpWaitForWithStatus(fmt.Sprintf("http://%s", nodejsURL), "H3110 from Node.js Starter Application!", 1, 0, 200) + helper.HttpWaitForWithStatus(fmt.Sprintf("http://%s", goURL), "H3110, !", 1, 0, 200) + }) + }) + }) + }) + })) + } for _, podman := range []bool{true, false} { podman := podman Context("port-forwarding for the component", helper.LabelPodmanIf(podman, func() { @@ -976,261 +1082,276 @@ ComponentSettings: }) }) }) - - for _, customPortForwarding := range []bool{true, false} { - customPortForwarding := customPortForwarding - var NoRandomPorts bool - if customPortForwarding { - NoRandomPorts = true + for _, customAddress := range []bool{true, false} { + customAddress := customAddress + var localAddress string + if customAddress { + localAddress = "0.0.0.0" } - When("devfile has single endpoint", func() { - var ( - LocalPort int - ) - const ( - ContainerPort = "3000" - ) - BeforeEach(func() { - LocalPort = helper.GetCustomStartPort() - helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), commonVar.Context) - helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile.yaml")).ShouldPass() - }) - - When("running odo dev", func() { - var devSession helper.DevSession - var ports map[string]string + for _, customPortForwarding := range []bool{true, false} { + customPortForwarding := customPortForwarding + var NoRandomPorts bool + if customPortForwarding { + NoRandomPorts = true + } + When("devfile has single endpoint", func() { + var ( + localPort int + ) + const ( + containerPort = "3000" + ) BeforeEach(func() { - var err error - opts := []string{} - if customPortForwarding { - opts = []string{fmt.Sprintf("--port-forward=%d:%s", LocalPort, ContainerPort)} - } - if manual { - opts = append(opts, "--no-watch") - } - devSession, _, _, ports, err = helper.StartDevMode(helper.DevSessionOpts{ - CmdlineArgs: opts, - NoRandomPorts: NoRandomPorts, - RunOnPodman: podman, - }) - Expect(err).ToNot(HaveOccurred()) + localPort = helper.GetCustomStartPort() + helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), commonVar.Context) + helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile.yaml")).ShouldPass() }) - AfterEach(func() { - devSession.Stop() - devSession.WaitEnd() - }) - - It(fmt.Sprintf("should expose the endpoint on localhost (podman=%v, manual=%v, customPortForwarding=%v)", podman, manual, customPortForwarding), func() { - url := fmt.Sprintf("http://%s", ports[ContainerPort]) - if customPortForwarding { - Expect(url).To(ContainSubstring(strconv.Itoa(LocalPort))) - } - resp, err := http.Get(url) - Expect(err).ToNot(HaveOccurred()) - defer resp.Body.Close() - - body, _ := io.ReadAll(resp.Body) - helper.MatchAllInOutput(string(body), []string{"Hello from Node.js Starter Application!"}) - Expect(err).ToNot(HaveOccurred()) - }) - - When("modifying memoryLimit for container in Devfile", func() { - var stdout string - var stderr string + When("running odo dev", func() { + var devSession helper.DevSession + var ports map[string]string BeforeEach(func() { - if manual { - if os.Getenv("SKIP_KEY_PRESS") == "true" { - Skip("This is a unix-terminal specific scenario, skipping") - } + var err error + opts := []string{} + if customPortForwarding { + opts = []string{fmt.Sprintf("--port-forward=%d:%s", localPort, containerPort)} } - var ( - wg sync.WaitGroup - err error - stdoutBytes []byte - stderrBytes []byte - ) - wg.Add(1) - go func() { - defer wg.Done() - stdoutBytes, stderrBytes, ports, err = devSession.WaitSync() - Expect(err).Should(Succeed()) - stdout = string(stdoutBytes) - stderr = string(stderrBytes) - }() - src := "memoryLimit: 1024Mi" - dst := "memoryLimit: 1023Mi" - helper.ReplaceString("devfile.yaml", src, dst) if manual { - devSession.PressKey('p') + opts = append(opts, "--no-watch") } - wg.Wait() + devSession, _, _, ports, err = helper.StartDevMode(helper.DevSessionOpts{ + CmdlineArgs: opts, + NoRandomPorts: NoRandomPorts, + RunOnPodman: podman, + CustomAddress: localAddress, + }) + Expect(err).ToNot(HaveOccurred()) + }) - It(fmt.Sprintf("should react on the Devfile modification (podman=%v, manual=%v, customPortForwarding=%v)", podman, manual, customPortForwarding), func() { - if podman { - By("warning users that odo dev needs to be restarted", func() { - Expect(stdout).To(ContainSubstring( - "Detected changes in the Devfile, but this is not supported yet on Podman. Please restart 'odo dev' for such changes to be applied.")) - }) - } else { - By("not warning users that odo dev needs to be restarted", func() { - warning := "Please restart 'odo dev'" - Expect(stdout).ShouldNot(ContainSubstring(warning)) - Expect(stderr).ShouldNot(ContainSubstring(warning)) - }) - By("updating the pod", func() { - podName := commonVar.CliRunner.GetRunningPodNameByComponent(cmpName, commonVar.Project) - bufferOutput := commonVar.CliRunner.Run("get", "pods", podName, "-o", "jsonpath='{.spec.containers[0].resources.requests.memory}'").Out.Contents() - output := string(bufferOutput) - Expect(output).To(ContainSubstring("1023Mi")) - }) + AfterEach(func() { + devSession.Stop() + devSession.WaitEnd() + }) - By("exposing the endpoint", func() { - Eventually(func(g Gomega) { - url := fmt.Sprintf("http://%s", ports[ContainerPort]) - if customPortForwarding { - Expect(url).To(ContainSubstring(strconv.Itoa(LocalPort))) - } - resp, err := http.Get(url) - g.Expect(err).ToNot(HaveOccurred()) - defer resp.Body.Close() - - body, _ := io.ReadAll(resp.Body) - for _, i := range []string{"Hello from Node.js Starter Application!"} { - g.Expect(string(body)).To(ContainSubstring(i)) - } - g.Expect(err).ToNot(HaveOccurred()) - }).WithPolling(1 * time.Second).WithTimeout(20 * time.Second).Should(Succeed()) - }) + It(fmt.Sprintf("should expose the endpoint on localhost (podman=%v, manual=%v, customPortForwarding=%v, customAddress=%v)", podman, manual, customPortForwarding, customAddress), func() { + url := fmt.Sprintf("http://%s", ports[containerPort]) + if customPortForwarding { + Expect(url).To(ContainSubstring(strconv.Itoa(localPort))) } + resp, err := http.Get(url) + Expect(err).ToNot(HaveOccurred()) + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + helper.MatchAllInOutput(string(body), []string{"Hello from Node.js Starter Application!"}) + Expect(err).ToNot(HaveOccurred()) }) - }) - }) - }) - When("devfile has multiple endpoints", func() { - var ( - LocalPort1, LocalPort2, LocalPort3 int - ) - const ( - // ContainerPort are hard-coded from devfile-with-multiple-endpoints.yaml - // Note 1: Debug endpoints will not be exposed for this instance, so we do not add custom mapping for them. - // Note 2: We add custom mapping for all the endpoints so that none of them are assigned random ports from the 20001-30001 range; - // Note 2(contd.): this is to avoid a race condition where a test running in parallel is also assigned similar ranged port the one here, and we fail to access either of them. - ContainerPort1 = "3000" - ContainerPort2 = "4567" - ContainerPort3 = "7890" - ) - BeforeEach(func() { - LocalPort1 = helper.GetCustomStartPort() - LocalPort2 = LocalPort1 + 1 - LocalPort3 = LocalPort1 + 2 - helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project-with-multiple-endpoints"), commonVar.Context) - helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile-with-multiple-endpoints.yaml")).ShouldPass() - }) + When("modifying memoryLimit for container in Devfile", func() { + var stdout string + var stderr string + BeforeEach(func() { + if manual { + if os.Getenv("SKIP_KEY_PRESS") == "true" { + Skip("This is a unix-terminal specific scenario, skipping") + } + } + var ( + wg sync.WaitGroup + err error + stdoutBytes []byte + stderrBytes []byte + ) + wg.Add(1) + go func() { + defer wg.Done() + stdoutBytes, stderrBytes, ports, err = devSession.WaitSync() + Expect(err).Should(Succeed()) + stdout = string(stdoutBytes) + stderr = string(stderrBytes) + }() + src := "memoryLimit: 1024Mi" + dst := "memoryLimit: 1023Mi" + helper.ReplaceString("devfile.yaml", src, dst) + if manual { + devSession.PressKey('p') + } + wg.Wait() + }) - When("running odo dev", func() { - var devSession helper.DevSession - var ports map[string]string - BeforeEach(func() { - opts := []string{} - if customPortForwarding { - opts = []string{fmt.Sprintf("--port-forward=%d:%s", LocalPort1, ContainerPort1), fmt.Sprintf("--port-forward=%d:%s", LocalPort2, ContainerPort2), fmt.Sprintf("--port-forward=%d:%s", LocalPort3, ContainerPort3)} - } - if manual { - opts = append(opts, "--no-watch") - } - var err error - devSession, _, _, ports, err = helper.StartDevMode(helper.DevSessionOpts{ - CmdlineArgs: opts, - NoRandomPorts: NoRandomPorts, - RunOnPodman: podman, + It(fmt.Sprintf("should react on the Devfile modification (podman=%v, manual=%v, customPortForwarding=%v, customAddress=%v)", podman, manual, customPortForwarding, customAddress), func() { + if podman { + By("warning users that odo dev needs to be restarted", func() { + Expect(stdout).To(ContainSubstring( + "Detected changes in the Devfile, but this is not supported yet on Podman. Please restart 'odo dev' for such changes to be applied.")) + }) + } else { + By("not warning users that odo dev needs to be restarted", func() { + warning := "Please restart 'odo dev'" + Expect(stdout).ShouldNot(ContainSubstring(warning)) + Expect(stderr).ShouldNot(ContainSubstring(warning)) + }) + By("updating the pod", func() { + podName := commonVar.CliRunner.GetRunningPodNameByComponent(cmpName, commonVar.Project) + bufferOutput := commonVar.CliRunner.Run("get", "pods", podName, "-o", "jsonpath='{.spec.containers[0].resources.requests.memory}'").Out.Contents() + output := string(bufferOutput) + Expect(output).To(ContainSubstring("1023Mi")) + }) + + By("exposing the endpoint", func() { + Eventually(func(g Gomega) { + url := fmt.Sprintf("http://%s", ports[containerPort]) + if customPortForwarding { + Expect(url).To(ContainSubstring(strconv.Itoa(localPort))) + } + if customAddress { + Expect(url).To(ContainSubstring(localAddress)) + } + resp, err := http.Get(url) + g.Expect(err).ToNot(HaveOccurred()) + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + for _, i := range []string{"Hello from Node.js Starter Application!"} { + g.Expect(string(body)).To(ContainSubstring(i)) + } + g.Expect(err).ToNot(HaveOccurred()) + }).WithPolling(1 * time.Second).WithTimeout(20 * time.Second).Should(Succeed()) + }) + } + }) }) - Expect(err).ToNot(HaveOccurred()) }) + }) - AfterEach(func() { - devSession.Stop() - devSession.WaitEnd() + When("devfile has multiple endpoints", func() { + var ( + localPort1, localPort2, localPort3 int + ) + const ( + // ContainerPort are hard-coded from devfile-with-multiple-endpoints.yaml + // Note 1: Debug endpoints will not be exposed for this instance, so we do not add custom mapping for them. + // Note 2: We add custom mapping for all the endpoints so that none of them are assigned random ports from the 20001-30001 range; + // Note 2(contd.): this is to avoid a race condition where a test running in parallel is also assigned similar ranged port the one here, and we fail to access either of them. + containerPort1 = "3000" + containerPort2 = "4567" + containerPort3 = "7890" + ) + BeforeEach(func() { + localPort1 = helper.GetCustomStartPort() + localPort2 = localPort1 + 1 + localPort3 = localPort1 + 2 + helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project-with-multiple-endpoints"), commonVar.Context) + helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile-with-multiple-endpoints.yaml")).ShouldPass() }) - It(fmt.Sprintf("should expose all endpoints on localhost regardless of exposure(podman=%v, manual=%v, customPortForwarding=%v)", podman, manual, customPortForwarding), func() { - By("not exposing debug endpoints", func() { - for _, p := range []int{5005, 5006} { - _, found := ports[strconv.Itoa(p)] - Expect(found).To(BeFalse(), fmt.Sprintf("debug port %d should not be forwarded", p)) + When("running odo dev", func() { + var devSession helper.DevSession + var ports map[string]string + BeforeEach(func() { + opts := []string{} + if customPortForwarding { + opts = []string{fmt.Sprintf("--port-forward=%d:%s", localPort1, containerPort1), fmt.Sprintf("--port-forward=%d:%s", localPort2, containerPort2), fmt.Sprintf("--port-forward=%d:%s", localPort3, containerPort3)} + } + if manual { + opts = append(opts, "--no-watch") } + var err error + devSession, _, _, ports, err = helper.StartDevMode(helper.DevSessionOpts{ + CmdlineArgs: opts, + NoRandomPorts: NoRandomPorts, + RunOnPodman: podman, + CustomAddress: localAddress, + }) + Expect(err).ToNot(HaveOccurred()) }) - getServerResponse := func(containerPort, localPort string) (string, error) { - url := fmt.Sprintf("http://%s", ports[containerPort]) - if customPortForwarding { - Expect(url).To(ContainSubstring(localPort)) + AfterEach(func() { + devSession.Stop() + devSession.WaitEnd() + }) + + It(fmt.Sprintf("should expose all endpoints on localhost regardless of exposure(podman=%v, manual=%v, customPortForwarding=%v, customAddress=%v)", podman, manual, customPortForwarding, customAddress), func() { + By("not exposing debug endpoints", func() { + for _, p := range []int{5005, 5006} { + _, found := ports[strconv.Itoa(p)] + Expect(found).To(BeFalse(), fmt.Sprintf("debug port %d should not be forwarded", p)) + } + }) + + getServerResponse := func(containerPort, localPort string) (string, error) { + url := fmt.Sprintf("http://%s", ports[containerPort]) + if customPortForwarding { + Expect(url).To(ContainSubstring(localPort)) + } + if customAddress { + Expect(url).To(ContainSubstring(localAddress)) + } + resp, err := http.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + return string(body), nil } - resp, err := http.Get(url) - if err != nil { - return "", err + containerPorts := []string{containerPort1, containerPort2, containerPort3} + localPorts := []int{localPort1, localPort2, localPort3} + + for i := range containerPorts { + containerPort := containerPorts[i] + localPort := localPorts[i] + By(fmt.Sprintf("exposing a port targeting container port %s", containerPort), func() { + r, err := getServerResponse(containerPort, strconv.Itoa(localPort)) + Expect(err).ShouldNot(HaveOccurred()) + helper.MatchAllInOutput(r, []string{"Hello from Node.js Starter Application!"}) + }) } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - return string(body), nil - } - containerPorts := []string{ContainerPort1, ContainerPort2, ContainerPort3} - localPorts := []int{LocalPort1, LocalPort2, LocalPort3} - - for i := range containerPorts { - containerPort := containerPorts[i] - localPort := localPorts[i] - By(fmt.Sprintf("exposing a port targeting container port %s", containerPort), func() { - r, err := getServerResponse(containerPort, strconv.Itoa(localPort)) - Expect(err).ShouldNot(HaveOccurred()) - helper.MatchAllInOutput(r, []string{"Hello from Node.js Starter Application!"}) - }) - } + helper.ReplaceString("server.js", "Hello from Node.js", "H3110 from Node.js") - helper.ReplaceString("server.js", "Hello from Node.js", "H3110 from Node.js") + if manual { + if os.Getenv("SKIP_KEY_PRESS") == "true" { + Skip("This is a unix-terminal specific scenario, skipping") + } - if manual { - if os.Getenv("SKIP_KEY_PRESS") == "true" { - Skip("This is a unix-terminal specific scenario, skipping") + devSession.PressKey('p') } - devSession.PressKey('p') - } + var stdout, stderr []byte + var err error + stdout, stderr, _, err = devSession.WaitSync() + Expect(err).Should(Succeed()) - var stdout, stderr []byte - var err error - stdout, stderr, _, err = devSession.WaitSync() - Expect(err).Should(Succeed()) + By("not warning users that odo dev needs to be restarted because the Devfile has not changed", func() { + warning := "Please restart 'odo dev'" + if podman { + warning = "Detected changes in the Devfile, but this is not supported yet on Podman. Please restart 'odo dev' for such changes to be applied." + } + Expect(stdout).ShouldNot(ContainSubstring(warning)) + Expect(stderr).ShouldNot(ContainSubstring(warning)) + }) - By("not warning users that odo dev needs to be restarted because the Devfile has not changed", func() { - warning := "Please restart 'odo dev'" - if podman { - warning = "Detected changes in the Devfile, but this is not supported yet on Podman. Please restart 'odo dev' for such changes to be applied." + for i := range containerPorts { + containerPort := containerPorts[i] + localPort := localPorts[i] + By(fmt.Sprintf("returning the right response when querying port forwarded for container port %s", containerPort), + func() { + Eventually(func(g Gomega) string { + r, err := getServerResponse(containerPort, strconv.Itoa(localPort)) + g.Expect(err).ShouldNot(HaveOccurred()) + return r + }, 180, 10).Should(Equal("H3110 from Node.js Starter Application!")) + }) } - Expect(stdout).ShouldNot(ContainSubstring(warning)) - Expect(stderr).ShouldNot(ContainSubstring(warning)) }) - - for i := range containerPorts { - containerPort := containerPorts[i] - localPort := localPorts[i] - By(fmt.Sprintf("returning the right response when querying port forwarded for container port %s", containerPort), - func() { - Eventually(func(g Gomega) string { - r, err := getServerResponse(containerPort, strconv.Itoa(localPort)) - g.Expect(err).ShouldNot(HaveOccurred()) - return r - }, 180, 10).Should(Equal("H3110 from Node.js Starter Application!")) - }) - } }) - }) - }) + }) + } } } }))