Use custom address for port forwarding (#6766)

* Custom address for port forwarding

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Add integration tests

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Use private variables for custom address and ports

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Update pkg/util/util.go

Co-authored-by: Armel Soro <armel@rm3l.org>

* Assign custom address to HostIP for podman

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Add validation for free port when --port-forward is provided in the Validate() method, add integration test for running parallel dev sessions on same platform, same port and different addresses

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Fix unit test failure

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Attempt at fixing windows failure

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Add documentation

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Use default value for --address flag

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Changes from review

Co-authored-by: Armel Soro <asoro@redhat.com>
Signed-off-by: Parthvi Vala <pvala@redhat.com>

---------

Signed-off-by: Parthvi Vala <pvala@redhat.com>
Co-authored-by: Armel Soro <armel@rm3l.org>
Co-authored-by: Armel Soro <asoro@redhat.com>
This commit is contained in:
Parthvi Vala
2023-05-02 17:04:19 +05:30
committed by GitHub
parent 191ee6f45f
commit 010e9634d3
23 changed files with 913 additions and 299 deletions

View File

@@ -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. `<LOCAL_PORT>:<CONTAINER_PORT>`
2. `<LOCAL_PORT>:<CONTAINER_NAME>:<CONTAINER_PORT>` - 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 <LOCAL_PORT_1>:<CONTAINER_PORT_1> --port-forward <LOCAL_PORT_2>:<CONTAINER_NAME>:<CONTAINER_PORT_2>
```
<details>
<summary>Example</summary>
```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 <name>`, or set an existing one by running `odo set namespace <name>`
↪ 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
```
</details>
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 <IP_ADDRESS>
```
<details>
<summary>Example</summary>
```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 <name>`, or set an existing one by running `odo set namespace <name>`
↪ 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
```
</details>
:::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.

View File

@@ -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.

View File

@@ -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)
}

View File

@@ -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),

View File

@@ -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)

View File

@@ -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

View File

@@ -119,7 +119,7 @@ type ClientInterface interface {
// SetupPortForwarding creates port-forwarding for the pod on the port pairs provided in the
// ["<localhost-port>":"<remote-pod-port>"] 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

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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 <localPort>:<containerName>:<containerPort>", "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)
}
})
}
}

View File

@@ -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.

View File

@@ -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 "<container-name>":{"<local-port-1>:<remote-port-1>", "<local-port-2>:<remote-port-2>"}
// "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

View File

@@ -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)
}

View File

@@ -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,
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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++

View File

@@ -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)
}
})

View File

@@ -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

View File

@@ -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]

View File

@@ -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

View File

@@ -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()
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() {
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
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,
CustomAddress: localAddress,
})
Expect(err).ToNot(HaveOccurred())
})
AfterEach(func() {
devSession.Stop()
devSession.WaitEnd()
})
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("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()
})
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())
})
}
})
})
})
})
When("running odo dev", func() {
var devSession helper.DevSession
var ports map[string]string
When("devfile has multiple endpoints", func() {
var (
localPort1, localPort2, localPort3 int
)
const (
// ContainerPort<N> 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() {
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())
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()
})
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() {
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())
})
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
}
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")
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()
})
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"))
})
var stdout, stderr []byte
var err error
stdout, stderr, _, err = devSession.WaitSync()
Expect(err).Should(Succeed())
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())
})
}
})
})
})
})
When("devfile has multiple endpoints", func() {
var (
LocalPort1, LocalPort2, LocalPort3 int
)
const (
// ContainerPort<N> 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("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,
})
Expect(err).ToNot(HaveOccurred())
})
AfterEach(func() {
devSession.Stop()
devSession.WaitEnd()
})
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))
}
})
getServerResponse := func(containerPort, localPort string) (string, error) {
url := fmt.Sprintf("http://%s", ports[containerPort])
if customPortForwarding {
Expect(url).To(ContainSubstring(localPort))
}
resp, err := http.Get(url)
if err != nil {
return "", err
}
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!"})
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))
})
}
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")
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!"))
})
}
devSession.PressKey('p')
}
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))
})
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!"))
})
}
})
})
})
}
}
}
}))