diff --git a/pkg/dev/interface.go b/pkg/dev/interface.go index df137f30ed6..eded4ce005e 100644 --- a/pkg/dev/interface.go +++ b/pkg/dev/interface.go @@ -20,6 +20,9 @@ type StartOptions struct { RandomPorts bool // 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. + // Applicable to Podman only. + IgnoreLocalhost bool // Variables to override in the Devfile Variables map[string]string } diff --git a/pkg/dev/podmandev/podmandev.go b/pkg/dev/podmandev/podmandev.go index 875b4c55f68..f38baf5daab 100644 --- a/pkg/dev/podmandev/podmandev.go +++ b/pkg/dev/podmandev/podmandev.go @@ -107,6 +107,7 @@ func (o *DevClient) Start( DevfileRunCmd: options.RunCommand, Variables: options.Variables, RandomPorts: options.RandomPorts, + IgnoreLocalhost: options.IgnoreLocalhost, WatchFiles: options.WatchFiles, WatchCluster: false, Out: out, @@ -188,13 +189,14 @@ func (o *DevClient) watchHandler(ctx context.Context, pushParams adapters.PushPa printWarningsOnDevfileChanges(ctx, watchParams) startOptions := dev.StartOptions{ - IgnorePaths: watchParams.FileIgnores, - Debug: watchParams.Debug, - BuildCommand: watchParams.DevfileBuildCmd, - RunCommand: watchParams.DevfileRunCmd, - RandomPorts: watchParams.RandomPorts, - WatchFiles: watchParams.WatchFiles, - Variables: watchParams.Variables, + IgnorePaths: watchParams.FileIgnores, + Debug: watchParams.Debug, + BuildCommand: watchParams.DevfileBuildCmd, + RunCommand: watchParams.DevfileRunCmd, + RandomPorts: watchParams.RandomPorts, + IgnoreLocalhost: watchParams.IgnoreLocalhost, + WatchFiles: watchParams.WatchFiles, + Variables: watchParams.Variables, } return o.reconcile(ctx, watchParams.Out, watchParams.ErrOut, startOptions, componentStatus) } diff --git a/pkg/dev/podmandev/reconcile.go b/pkg/dev/podmandev/reconcile.go index 0d6511d0b7b..ccfdf2e274f 100644 --- a/pkg/dev/podmandev/reconcile.go +++ b/pkg/dev/podmandev/reconcile.go @@ -2,6 +2,7 @@ package podmandev import ( "context" + "errors" "fmt" "io" "path/filepath" @@ -18,6 +19,7 @@ import ( "github.com/redhat-developer/odo/pkg/libdevfile" "github.com/redhat-developer/odo/pkg/log" odocontext "github.com/redhat-developer/odo/pkg/odo/context" + "github.com/redhat-developer/odo/pkg/port" "github.com/redhat-developer/odo/pkg/watch" corev1 "k8s.io/api/core/v1" @@ -113,6 +115,14 @@ func (o *DevClient) reconcile( componentStatus.RunExecuted = true } + // By default, Podman will not forward to container applications listening on the loopback interface. + // So we are trying to detect such cases and act accordingly. + // See https://github.com/redhat-developer/odo/issues/6510#issuecomment-1439986558 + err = o.handleLoopbackPorts(options, pod, fwPorts) + if err != nil { + return err + } + for _, fwPort := range fwPorts { s := fmt.Sprintf("Forwarding from %s:%d -> %d", fwPort.LocalAddress, fwPort.LocalPort, fwPort.ContainerPort) fmt.Fprintf(out, " - %s", log.SboldColor(color.FgGreen, s)) @@ -194,3 +204,46 @@ func (o *DevClient) deployPod(ctx context.Context, options dev.StartOptions) (*c spinner.End(true) return pod, fwPorts, nil } + +// handleLoopbackPorts tries to detect if any of the ports to forward (in fwPorts) is actually bound to the loopback interface within the specified pod. +// If that is the case, it will either return an error if options.IgnoreLocalhost is false, or no error otherwise. +// +// Note that this method should be called after the process representing the application (run or debug command) is actually started in the pod. +func (o *DevClient) handleLoopbackPorts(options dev.StartOptions, pod *corev1.Pod, fwPorts []api.ForwardedPort) error { + if len(pod.Spec.Containers) == 0 { + return nil + } + + loopbackPorts, err := port.DetectRemotePortsBoundOnLoopback(o.execClient, pod.Name, pod.Spec.Containers[0].Name, fwPorts) + if err != nil { + return fmt.Errorf("unable to detect container ports bound on the loopback interface: %w", err) + } + + if len(loopbackPorts) == 0 { + return nil + } + + klog.V(5).Infof("detected %d ports bound on the loopback interface in the pod: %v", len(loopbackPorts), loopbackPorts) + list := make([]string, 0, len(loopbackPorts)) + for _, p := range loopbackPorts { + list = append(list, fmt.Sprintf("%s (%d)", p.PortName, p.ContainerPort)) + } + msg := fmt.Sprintf(`Detected that the following port(s) can be reached only via the container loopback interface: %s. +Port forwarding on Podman currently does not work with applications listening on the loopback interface. +Either change the application to make those port(s) reachable on all interfaces (0.0.0.0), or rerun 'odo dev' with `, strings.Join(list, ", ")) + if options.IgnoreLocalhost { + msg += "'--forward-localhost' to make port-forwarding work with such ports." + } else { + msg += `any of the following options: +- --ignore-localhost: no error will be returned by odo, but forwarding to those ports might not work on Podman. +- --forward-localhost: odo will inject a dedicated side container to redirect traffic to such ports.` + } + if !options.IgnoreLocalhost { + log.Errorf(msg) + return errors.New("cannot make port forwarding work with applications listening only on the loopback interface") + } + // No error, but only a warning if using --ignore-localhost + log.Warningf(msg) + + return nil +} diff --git a/pkg/odo/cli/dev/dev.go b/pkg/odo/cli/dev/dev.go index 99dd63a8509..bd0dc661453 100644 --- a/pkg/odo/cli/dev/dev.go +++ b/pkg/odo/cli/dev/dev.go @@ -2,6 +2,7 @@ package dev import ( "context" + "errors" "fmt" "io" "path/filepath" @@ -50,11 +51,12 @@ type DevOptions struct { cancel context.CancelFunc // Flags - noWatchFlag bool - randomPortsFlag bool - debugFlag bool - buildCommandFlag string - runCommandFlag string + noWatchFlag bool + randomPortsFlag bool + debugFlag bool + buildCommandFlag string + runCommandFlag string + ignoreLocalhostFlag bool } var _ genericclioptions.Runnable = (*DevOptions)(nil) @@ -105,6 +107,9 @@ func (o *DevOptions) Validate(ctx context.Context) error { platform := fcontext.GetPlatform(ctx, commonflags.PlatformCluster) switch platform { case commonflags.PlatformCluster: + if o.ignoreLocalhostFlag { + return errors.New("--ignore-localhost cannot be used when running in cluster mode") + } if o.clientset.KubernetesClient == nil { return kclient.NewNoConnectionError() } @@ -180,13 +185,14 @@ func (o *DevOptions) Run(ctx context.Context) (err error) { o.out, o.errOut, dev.StartOptions{ - IgnorePaths: o.ignorePaths, - Debug: o.debugFlag, - BuildCommand: o.buildCommandFlag, - RunCommand: o.runCommandFlag, - RandomPorts: o.randomPortsFlag, - WatchFiles: !o.noWatchFlag, - Variables: variables, + IgnorePaths: o.ignorePaths, + Debug: o.debugFlag, + BuildCommand: o.buildCommandFlag, + RunCommand: o.runCommandFlag, + RandomPorts: o.randomPortsFlag, + WatchFiles: !o.noWatchFlag, + IgnoreLocalhost: o.ignoreLocalhostFlag, + Variables: variables, }, ) } @@ -226,6 +232,11 @@ It forwards endpoints with any exposure values ('public', 'internal' or 'none') "Alternative build command. The default one will be used if this flag is not set.") devCmd.Flags().StringVar(&o.runCommandFlag, "run-command", "", "Alternative run command to execute. The default one will be used if this flag is not set.") + devCmd.Flags().BoolVar(&o.ignoreLocalhostFlag, "ignore-localhost", false, + "Whether to ignore errors related to port-forwarding apps listening on the container loopback interface. Applicable only if platform is podman.") + // TODO Unhide when moving Podman out of the experimental mode : https://github.com/redhat-developer/odo/issues/6592 + _ = devCmd.Flags().MarkHidden("ignore-localhost") + clientset.Add(devCmd, clientset.BINDING, clientset.DEV, diff --git a/pkg/port/port.go b/pkg/port/port.go new file mode 100644 index 00000000000..db912a6d08d --- /dev/null +++ b/pkg/port/port.go @@ -0,0 +1,244 @@ +package port + +import ( + "fmt" + "net" + "regexp" + "strconv" + "strings" + + "k8s.io/klog" + + "github.com/redhat-developer/odo/pkg/api" + "github.com/redhat-developer/odo/pkg/exec" + "github.com/redhat-developer/odo/pkg/remotecmd" +) + +// Order of values in the TCP States enum. +// See https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/net/tcp_states.h#n12 +var connectionStates = []string{ + "ESTABLISHED", + "SYN_SENT", + "SYN_RECV", + "FIN_WAIT1", + "FIN_WAIT2", + "TIME_WAIT", + "CLOSE", + "CLOSE_WAIT", + "LAST_ACK", + "LISTEN", + "CLOSING", + "NEW_SYN_RECV", + + "MAX_STATES", // Leave at the end! +} + +// every 2 other characters +var ipv4HexRegExp = regexp.MustCompile(".{2}") + +type Connection struct { + LocalAddress string + LocalPort int + RemoteAddress string + RemotePort int + State string +} + +func (c Connection) String() string { + return fmt.Sprintf("[%s] %s:%d -> %s:%d", c.State, c.LocalAddress, c.LocalPort, c.RemoteAddress, c.RemotePort) +} + +// DetectRemotePortsBoundOnLoopback filters the given ports by returning only those that are actually bound to the loopback interface in the specified container. +func DetectRemotePortsBoundOnLoopback(execClient exec.Client, podName string, containerName string, ports []api.ForwardedPort) ([]api.ForwardedPort, error) { + if len(ports) == 0 { + return nil, nil + } + + listening, err := GetListeningConnections(execClient, podName, containerName) + if err != nil { + return nil, err + } + var boundToLocalhost []api.ForwardedPort + for _, p := range ports { + for _, conn := range listening { + if p.ContainerPort != conn.LocalPort { + continue + } + klog.V(6).Infof("found listening connection matching container port %d: %s", p.ContainerPort, conn.String()) + ip := net.ParseIP(conn.LocalAddress) + if ip == nil { + klog.V(6).Infof("invalid IP address: %q", conn.LocalAddress) + continue + } + if ip.IsLoopback() { + boundToLocalhost = append(boundToLocalhost, p) + break + } + } + } + return boundToLocalhost, nil +} + +// GetListeningConnections retrieves information about ports being listened and on which local address in the specified container. +// It works by parsing information from the /proc/net/{tcp,tcp6,udp,udp6} files, and is able to parse both IPv4 and IPv6 addresses. +// See https://www.kernel.org/doc/Documentation/networking/proc_net_tcp.txt for more information about the structure of these files. +func GetListeningConnections(execClient exec.Client, podName string, containerName string) ([]Connection, error) { + return GetConnections(execClient, podName, containerName, func(state int) bool { + return stateToString(state) == "LISTEN" + }) +} + +// GetConnections retrieves information about connections in the specified container. +// It works by parsing information from the /proc/net/{tcp,tcp6,udp,udp6} files, and is able to parse both IPv4 and IPv6 addresses. +// See https://www.kernel.org/doc/Documentation/networking/proc_net_tcp.txt for more information about the structure of these files. +// The specified predicate allows to filter the connections based on the state. +func GetConnections(execClient exec.Client, podName string, containerName string, statePredicate func(state int) bool) ([]Connection, error) { + cmd := []string{ + remotecmd.ShellExecutable, "-c", + "cat /proc/net/tcp /proc/net/udp /proc/net/tcp6 /proc/net/udp6", + } + stdout, _, err := execClient.ExecuteCommand(cmd, podName, containerName, false, nil, nil) + if err != nil { + return nil, err + } + + hexToInt := func(hex string) (int, error) { + i, parseErr := strconv.ParseInt(hex, 16, 32) + if parseErr != nil { + return 0, parseErr + } + return int(i), nil + } + + hexRevIpV4ToString := func(hex string) (string, error) { + parts := ipv4HexRegExp.FindAllString(hex, -1) + result := make([]string, 0, len(parts)) + for i := len(parts) - 1; i >= 0; i-- { + toInt, parseErr := hexToInt(parts[i]) + if parseErr != nil { + return "", parseErr + } + result = append(result, fmt.Sprintf("%d", toInt)) + } + return strings.Join(result, "."), nil + } + + hexRevIpV6ToString := func(hex string) (string, error) { + // In IPv6, each group of the address is 2 bytes long (4 hex characters). + // See https://www.rfc-editor.org/rfc/rfc4291#page-4 + i := []string{ + hex[30:32], + hex[28:30], + hex[26:28], + hex[24:26], + hex[22:24], + hex[20:22], + hex[18:20], + hex[16:18], + hex[14:16], + hex[12:14], + hex[10:12], + hex[8:10], + hex[6:8], + hex[4:6], + hex[2:4], + hex[0:2], + } + return fmt.Sprintf("%s%s:%s%s:%s%s:%s%s:%s%s:%s%s:%s%s:%s%s", + i[12], i[13], i[14], i[15], + i[8], i[9], i[10], i[11], + i[4], i[5], i[7], i[7], + i[0], i[1], i[2], i[3]), nil + } + + parseAddrAndPort := func(s string) (addr string, port int, err error) { + addrPortList := strings.Split(s, ":") + if len(addrPortList) != 2 { + return "", 0, fmt.Errorf("invalid format - must be :, but was %q", s) + } + + addrHex := addrPortList[0] + switch len(addrHex) { + case 8: + addr, err = hexRevIpV4ToString(addrHex) + case 32: + addr, err = hexRevIpV6ToString(addrHex) + default: + err = fmt.Errorf("length must be 8 (IPv4) or 32 (IPv6), but was %d", len(addrHex)) + } + if err != nil { + return "", 0, fmt.Errorf("could not decode address info from %q: %w", s, err) + } + + portHex := addrPortList[1] + port, err = hexToInt(portHex) + if err != nil { + return "", 0, fmt.Errorf("could not decode port info from %q: %w", s, err) + } + return addr, port, nil + } + + var connections []Connection + for _, l := range stdout { + if strings.Contains(l, "local_address") { + // ignore header lines + continue + } + + /* + We are interested only in the first 4 values, which provide information about the local address, port and the connection state. + See https://www.kernel.org/doc/Documentation/networking/proc_net_tcp.txt + + 46: 010310AC:9C4C 030310AC:1770 01 + | | | | | |--> connection state + | | | | |------> remote TCP port number + | | | |-------------> remote IPv4 address + | | |--------------------> local TCP port number + | |---------------------------> local IPv4 address + |----------------------------------> number of entry + */ + split := strings.SplitN(strings.TrimSpace(l), " ", 5) + if len(split) < 4 { + klog.V(5).Infof("ignored line %q because it has less than 4 space-separated elements", l) + continue + } + stateHex := split[3] + state, err := hexToInt(stateHex) + if err != nil { + klog.V(5).Infof("[warn] could not decode state info from line %q: %v", l, err) + continue + } + if statePredicate != nil && !statePredicate(state) { + klog.V(5).Infof("ignored line because state value does not pass predicate: %q", l) + continue + } + + localAddr, localPort, err := parseAddrAndPort(split[1]) + if err != nil { + klog.V(5).Infof("ignored line because it is not possible to determine local addr and port: %q", l) + continue + } + remoteAddr, remotePort, err := parseAddrAndPort(split[2]) + if err != nil { + klog.V(5).Infof("ignored line because it is not possible to determine remote addr and port: %q", l) + continue + } + + connections = append(connections, Connection{ + LocalAddress: localAddr, + LocalPort: localPort, + RemoteAddress: remoteAddr, + RemotePort: remotePort, + State: stateToString(state), + }) + } + + return connections, nil +} + +func stateToString(state int) string { + if state < 1 || state > len(connectionStates) { + return "" + } + return connectionStates[state-1] +} diff --git a/pkg/port/port_test.go b/pkg/port/port_test.go new file mode 100644 index 00000000000..88434b33c6a --- /dev/null +++ b/pkg/port/port_test.go @@ -0,0 +1,414 @@ +package port + +import ( + "errors" + "strings" + "testing" + + "github.com/golang/mock/gomock" + "github.com/google/go-cmp/cmp" + + "github.com/redhat-developer/odo/pkg/api" + "github.com/redhat-developer/odo/pkg/exec" + "github.com/redhat-developer/odo/pkg/remotecmd" +) + +const ( + podName = "my-pod" + containerName = "my-container" +) + +var cmd = []string{ + remotecmd.ShellExecutable, "-c", + "cat /proc/net/tcp /proc/net/udp /proc/net/tcp6 /proc/net/udp6", +} + +const aggregatedContentFromProcNetFiles = ` +sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode +0: 0100007F:4E21 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 2798686 1 0000000000000000 100 0 0 10 0 +1: 690A0A0A:192B 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 24399 1 0000000000000000 100 0 0 10 0 +2: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 28227 1 0000000000000000 100 0 0 10 0 +6: 00000000:14EB 00000000:0000 0A 00000000:00000000 00:00000000 00000000 193 0 18937 1 0000000000000000 100 0 0 10 5 +10: 690A0A0A:A0B6 85B2FA8E:01BB 06 00000000:00000000 03:00000C93 00000000 0 0 0 3 0000000000000000 +11: 690A0A0A:DC86 6E882E34:01BB 01 00000000:00000000 02:00000418 00000000 1000 0 5992580 2 0000000000000000 29 4 30 10 -1 +invalid_state: 690A0A0A:DC86 6E882E34:01BB ZZZ 00000000:00000000 02:00000418 00000000 1000 0 5992580 2 0000000000000000 29 4 30 10 -1 +invalid_local_port: 690A0A0A:WXYZ 6E882E34:01BB 0A 00000000:00000000 02:00000418 00000000 1000 0 5992580 2 0000000000000000 29 4 30 10 -1 + +sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops +299: 690A0A0A:87BE EE4BFA8E:01BB 01 00000000:00000000 00:00000000 00000000 1000 0 5879134 2 0000000000000000 0 +3670: FB0000E0:14E9 00000000:0000 07 00000000:00000000 00:00000000 00000000 1000 0 5422041 2 0000000000000000 0 +invalid_local_addr: 000ZZ000:14E9 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 5422041 2 0000000000000000 0 + +sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode +0: 00000000000000000000000001000000:0277 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 30021 1 0000000000000000 100 0 0 10 0 +1: 0000000000000000FFFF00000100007F:F76E 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 383053 1 0000000000000000 100 0 0 10 0 +3: 00000000000000000000000000000000:1388 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 29332 1 0000000000000000 100 0 0 10 0 +too_long_local_addr 00000000000000000000000001000000123456789010:0277 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 30021 1 0000000000000000 100 0 0 10 0 + +sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops +623: 00000000000000000000000000000000:8902 00000000000000000000000000000000:0000 07 00000000:00000000 00:00000000 00000000 1000 0 5976457 2 0000000000000000 0 +32077: 00000000000000000000000000000000:83E0 00000000000000000000000000000000:0000 07 00000000:00000000 00:00000000 00000000 1000 0 5915479 2 0000000000000000 0 +invalid_local_addr_port 0000000000000000000000000100000:0277:123 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 30021 1 0000000000000000 100 0 0 10 0 + +` + +func TestDetectRemotePortsBoundOnLoopback(t *testing.T) { + inputPorts := []api.ForwardedPort{ + {ContainerPort: 20001}, + {ContainerPort: 6443}, + {ContainerPort: 22}, + {ContainerPort: 5355}, + {ContainerPort: 631}, + {ContainerPort: 63342}, + {ContainerPort: 5000}, + {ContainerPort: 8080}, + {ContainerPort: 5858}, + } + type args struct { + execClientCustomizer func(client *exec.MockClient) + podName string + containerName string + ports []api.ForwardedPort + } + tests := []struct { + name string + args args + want []api.ForwardedPort + wantErr bool + }{ + { + name: "error while executing command", + args: args{ + execClientCustomizer: func(client *exec.MockClient) { + client.EXPECT().ExecuteCommand(gomock.Eq(cmd), gomock.Eq(podName), gomock.Eq(containerName), gomock.Eq(false), gomock.Nil(), gomock.Nil()). + Return(nil, nil, errors.New("some-err")) + }, + podName: podName, + containerName: containerName, + ports: inputPorts, + }, + wantErr: true, + }, + { + name: "no active connections", + args: args{ + execClientCustomizer: func(client *exec.MockClient) { + client.EXPECT().ExecuteCommand(gomock.Eq(cmd), gomock.Eq(podName), gomock.Eq(containerName), gomock.Eq(false), gomock.Nil(), gomock.Nil()). + Return(strings.Split(` +sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode +sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops +sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode +sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops +`, "\n"), nil, nil) + }, + podName: podName, + containerName: containerName, + ports: inputPorts, + }, + wantErr: false, + want: nil, + }, + { + name: "no input ports", + args: args{ + execClientCustomizer: func(client *exec.MockClient) { + client.EXPECT().ExecuteCommand(gomock.Eq(cmd), gomock.Eq(podName), gomock.Eq(containerName), gomock.Eq(false), gomock.Nil(), gomock.Nil()). + Times(0) + }, + podName: podName, + containerName: containerName, + ports: nil, + }, + wantErr: false, + want: nil, + }, + { + name: "with different connections", + args: args{ + execClientCustomizer: func(client *exec.MockClient) { + client.EXPECT().ExecuteCommand(gomock.Eq(cmd), gomock.Eq(podName), gomock.Eq(containerName), gomock.Eq(false), gomock.Nil(), gomock.Nil()). + Return(strings.Split(aggregatedContentFromProcNetFiles, "\n"), nil, nil) + }, + podName: podName, + containerName: containerName, + ports: inputPorts, + }, + wantErr: false, + want: []api.ForwardedPort{ + {ContainerPort: 20001}, + {ContainerPort: 631}, + {ContainerPort: 63342}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + execClient := exec.NewMockClient(ctrl) + tt.args.execClientCustomizer(execClient) + + got, err := DetectRemotePortsBoundOnLoopback(execClient, tt.args.podName, tt.args.containerName, tt.args.ports) + if (err != nil) != tt.wantErr { + t.Errorf("detectRemotePortsBoundOnLoopback() error = %v, wantErr %v", err, tt.wantErr) + return + } + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("detectRemotePortsBoundOnLoopback() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestGetListeningConnections(t *testing.T) { + type args struct { + execClientCustomizer func(client *exec.MockClient) + podName string + containerName string + } + tests := []struct { + name string + args args + want []Connection + wantErr bool + }{ + { + name: "error while executing command", + args: args{ + execClientCustomizer: func(client *exec.MockClient) { + client.EXPECT().ExecuteCommand(gomock.Eq(cmd), gomock.Eq(podName), gomock.Eq(containerName), gomock.Eq(false), gomock.Nil(), gomock.Nil()). + Return(nil, nil, errors.New("some-err")) + }, + podName: podName, + containerName: containerName, + }, + wantErr: true, + }, + { + name: "no active connections", + args: args{ + execClientCustomizer: func(client *exec.MockClient) { + client.EXPECT().ExecuteCommand(gomock.Eq(cmd), gomock.Eq(podName), gomock.Eq(containerName), gomock.Eq(false), gomock.Nil(), gomock.Nil()). + Return(strings.Split(` +sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode +sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops +sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode +sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops +`, "\n"), nil, nil) + }, + podName: podName, + containerName: containerName, + }, + wantErr: false, + want: nil, + }, + { + name: "with different connections", + args: args{ + execClientCustomizer: func(client *exec.MockClient) { + client.EXPECT().ExecuteCommand(gomock.Eq(cmd), gomock.Eq(podName), gomock.Eq(containerName), gomock.Eq(false), gomock.Nil(), gomock.Nil()). + Return(strings.Split(aggregatedContentFromProcNetFiles, "\n"), nil, nil) + }, + podName: podName, + containerName: containerName, + }, + wantErr: false, + want: []Connection{ + {LocalAddress: "127.0.0.1", LocalPort: 20001, RemoteAddress: "0.0.0.0", RemotePort: 0, State: "LISTEN"}, + {LocalAddress: "10.10.10.105", LocalPort: 6443, RemoteAddress: "0.0.0.0", RemotePort: 0, State: "LISTEN"}, + {LocalAddress: "0.0.0.0", LocalPort: 22, RemoteAddress: "0.0.0.0", RemotePort: 0, State: "LISTEN"}, + {LocalAddress: "0.0.0.0", LocalPort: 5355, RemoteAddress: "0.0.0.0", RemotePort: 0, State: "LISTEN"}, + { + LocalAddress: "0000:0000:0000:0000:0000:0000:0000:0001", + LocalPort: 631, + RemoteAddress: "0000:0000:0000:0000:0000:0000:0000:0000", + RemotePort: 0, + State: "LISTEN", + }, + { + LocalAddress: "0000:0000:0000:0000:0000:FFFF:7F00:0001", + LocalPort: 63342, + RemoteAddress: "0000:0000:0000:0000:0000:0000:0000:0000", + RemotePort: 0, + State: "LISTEN", + }, + { + LocalAddress: "0000:0000:0000:0000:0000:0000:0000:0000", + LocalPort: 5000, + RemoteAddress: "0000:0000:0000:0000:0000:0000:0000:0000", + RemotePort: 0, + State: "LISTEN", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + execClient := exec.NewMockClient(ctrl) + tt.args.execClientCustomizer(execClient) + + got, err := GetListeningConnections(execClient, tt.args.podName, tt.args.containerName) + if (err != nil) != tt.wantErr { + t.Errorf("getListeningConnections() error = %v, wantErr %v", err, tt.wantErr) + return + } + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("getListeningConnections() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestGetConnections(t *testing.T) { + type args struct { + execClientCustomizer func(client *exec.MockClient) + podName string + containerName string + statePredicate func(state int) bool + } + tests := []struct { + name string + args args + want []Connection + wantErr bool + }{ + { + name: "error while executing command", + args: args{ + execClientCustomizer: func(client *exec.MockClient) { + client.EXPECT().ExecuteCommand(gomock.Eq(cmd), gomock.Eq(podName), gomock.Eq(containerName), gomock.Eq(false), gomock.Nil(), gomock.Nil()). + Return(nil, nil, errors.New("some-err")) + }, + podName: podName, + containerName: containerName, + }, + wantErr: true, + }, + { + name: "no active connections", + args: args{ + execClientCustomizer: func(client *exec.MockClient) { + client.EXPECT().ExecuteCommand(gomock.Eq(cmd), gomock.Eq(podName), gomock.Eq(containerName), gomock.Eq(false), gomock.Nil(), gomock.Nil()). + Return(strings.Split(` +sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode +sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops +sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode +sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops +`, "\n"), nil, nil) + }, + podName: podName, + containerName: containerName, + }, + wantErr: false, + want: nil, + }, + { + name: "non-matching filter on state", + args: args{ + execClientCustomizer: func(client *exec.MockClient) { + client.EXPECT().ExecuteCommand(gomock.Eq(cmd), gomock.Eq(podName), gomock.Eq(containerName), gomock.Eq(false), gomock.Nil(), gomock.Nil()). + Return(strings.Split(aggregatedContentFromProcNetFiles, "\n"), nil, nil) + }, + podName: podName, + containerName: containerName, + statePredicate: func(state int) bool { + return stateToString(state) == "some unknown state" + }, + }, + wantErr: false, + want: nil, + }, + { + name: "filter on state: ESTABLISHED", + args: args{ + execClientCustomizer: func(client *exec.MockClient) { + client.EXPECT().ExecuteCommand(gomock.Eq(cmd), gomock.Eq(podName), gomock.Eq(containerName), gomock.Eq(false), gomock.Nil(), gomock.Nil()). + Return(strings.Split(aggregatedContentFromProcNetFiles, "\n"), nil, nil) + }, + podName: podName, + containerName: containerName, + statePredicate: func(state int) bool { + return stateToString(state) == "ESTABLISHED" + }, + }, + wantErr: false, + want: []Connection{ + {LocalAddress: "10.10.10.105", LocalPort: 56454, RemoteAddress: "52.46.136.110", RemotePort: 443, State: "ESTABLISHED"}, + {LocalAddress: "10.10.10.105", LocalPort: 34750, RemoteAddress: "142.250.75.238", RemotePort: 443, State: "ESTABLISHED"}, + }, + }, + { + name: "all connections", + args: args{ + execClientCustomizer: func(client *exec.MockClient) { + client.EXPECT().ExecuteCommand(gomock.Eq(cmd), gomock.Eq(podName), gomock.Eq(containerName), gomock.Eq(false), gomock.Nil(), gomock.Nil()). + Return(strings.Split(aggregatedContentFromProcNetFiles, "\n"), nil, nil) + }, + podName: podName, + containerName: containerName, + }, + wantErr: false, + want: []Connection{ + {LocalAddress: "127.0.0.1", LocalPort: 20001, RemoteAddress: "0.0.0.0", RemotePort: 0, State: "LISTEN"}, + {LocalAddress: "10.10.10.105", LocalPort: 6443, RemoteAddress: "0.0.0.0", RemotePort: 0, State: "LISTEN"}, + {LocalAddress: "0.0.0.0", LocalPort: 22, RemoteAddress: "0.0.0.0", RemotePort: 0, State: "LISTEN"}, + {LocalAddress: "0.0.0.0", LocalPort: 5355, RemoteAddress: "0.0.0.0", RemotePort: 0, State: "LISTEN"}, + {LocalAddress: "10.10.10.105", LocalPort: 41142, RemoteAddress: "142.250.178.133", RemotePort: 443, State: "TIME_WAIT"}, + {LocalAddress: "10.10.10.105", LocalPort: 56454, RemoteAddress: "52.46.136.110", RemotePort: 443, State: "ESTABLISHED"}, + {LocalAddress: "10.10.10.105", LocalPort: 34750, RemoteAddress: "142.250.75.238", RemotePort: 443, State: "ESTABLISHED"}, + {LocalAddress: "224.0.0.251", LocalPort: 5353, RemoteAddress: "0.0.0.0", RemotePort: 0, State: "CLOSE"}, + { + LocalAddress: "0000:0000:0000:0000:0000:0000:0000:0001", + LocalPort: 631, + RemoteAddress: "0000:0000:0000:0000:0000:0000:0000:0000", + RemotePort: 0, + State: "LISTEN", + }, + { + LocalAddress: "0000:0000:0000:0000:0000:FFFF:7F00:0001", + LocalPort: 63342, + RemoteAddress: "0000:0000:0000:0000:0000:0000:0000:0000", + RemotePort: 0, + State: "LISTEN", + }, + { + LocalAddress: "0000:0000:0000:0000:0000:0000:0000:0000", + LocalPort: 5000, + RemoteAddress: "0000:0000:0000:0000:0000:0000:0000:0000", + RemotePort: 0, + State: "LISTEN", + }, + { + LocalAddress: "0000:0000:0000:0000:0000:0000:0000:0000", + LocalPort: 35074, + RemoteAddress: "0000:0000:0000:0000:0000:0000:0000:0000", + RemotePort: 0, + State: "CLOSE", + }, + { + LocalAddress: "0000:0000:0000:0000:0000:0000:0000:0000", + LocalPort: 33760, + RemoteAddress: "0000:0000:0000:0000:0000:0000:0000:0000", + RemotePort: 0, + State: "CLOSE", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + execClient := exec.NewMockClient(ctrl) + tt.args.execClientCustomizer(execClient) + + got, err := GetConnections(execClient, tt.args.podName, tt.args.containerName, tt.args.statePredicate) + if (err != nil) != tt.wantErr { + t.Errorf("getListeningConnections() error = %v, wantErr %v", err, tt.wantErr) + return + } + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("getListeningConnections() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/pkg/watch/watch.go b/pkg/watch/watch.go index e6eeed50eb4..148579e5fb7 100644 --- a/pkg/watch/watch.go +++ b/pkg/watch/watch.go @@ -90,6 +90,9 @@ type WatchParameters struct { Variables map[string]string // RandomPorts is true to forward containers ports on local random ports RandomPorts bool + // IgnoreLocalhost indicates whether to proceed with port-forwarding regardless of any container ports being bound to the container loopback interface. + // Applicable to Podman only. + IgnoreLocalhost bool // WatchFiles indicates to watch for file changes and sync changes to the container WatchFiles bool // WatchCluster indicates to watch Cluster-related objects (Deployment, Pod, etc) diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-endpoint-on-loopback.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-endpoint-on-loopback.yaml new file mode 100644 index 00000000000..f23e52d9f60 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-endpoint-on-loopback.yaml @@ -0,0 +1,76 @@ +commands: +- exec: + commandLine: npm install + component: runtime + group: + isDefault: true + kind: build + workingDir: ${PROJECT_SOURCE} + id: install +- exec: + commandLine: npm start + component: runtime + group: + isDefault: true + kind: run + workingDir: ${PROJECT_SOURCE} + id: run +- exec: + commandLine: npm run debug + component: runtime + group: + isDefault: true + kind: debug + workingDir: ${PROJECT_SOURCE} + id: debug +- exec: + commandLine: npm test + component: runtime + group: + isDefault: true + kind: test + workingDir: ${PROJECT_SOURCE} + id: test +components: +- container: + args: + - -f + - /dev/null + command: + - tail + endpoints: + - name: app + protocol: tcp + targetPort: 3000 + - exposure: none + name: admin + protocol: tcp + targetPort: 3001 + - exposure: none + name: debug + targetPort: 5858 + env: + - name: DEBUG_PORT + value: "5858" + image: registry.access.redhat.com/ubi8/nodejs-16:latest + memoryLimit: 1024Mi + mountSources: true + name: runtime +metadata: + description: Stack with Node.js 16 + displayName: Node.js Runtime + icon: https://nodejs.org/static/images/logos/nodejs-new-pantone-black.svg + language: JavaScript + name: nodejs-with-endpoint-on-loopback + projectType: Node.js + tags: + - Node.js + - Express + - ubi8 + version: 2.1.1 +schemaVersion: 2.2.0 +starterProjects: +- git: + remotes: + origin: https://github.com/odo-devfiles/nodejs-ex.git + name: nodejs-starter diff --git a/tests/examples/source/devfiles/nodejs/project-with-endpoint-on-loopback/package-lock.json b/tests/examples/source/devfiles/nodejs/project-with-endpoint-on-loopback/package-lock.json new file mode 100644 index 00000000000..d604a89c95f --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/project-with-endpoint-on-loopback/package-lock.json @@ -0,0 +1,592 @@ +{ + "name": "nodejs-with-endpoint-on-loopback", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nodejs-with-endpoint-on-loopback", + "version": "1.0.0", + "license": "EPL-2.0", + "dependencies": { + "express": "^4.18.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/tests/examples/source/devfiles/nodejs/project-with-endpoint-on-loopback/package.json b/tests/examples/source/devfiles/nodejs/project-with-endpoint-on-loopback/package.json new file mode 100644 index 00000000000..10c4220dec9 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/project-with-endpoint-on-loopback/package.json @@ -0,0 +1,13 @@ +{ + "name": "nodejs-with-endpoint-on-loopback", + "version": "1.0.0", + "description": "Simple Node.js application with one server listening on all interfaces and another on the loopback interface", + "license": "EPL-2.0", + "scripts": { + "start": "node server.js", + "debug": "node --inspect=${DEBUG_PORT} server.js" + }, + "dependencies": { + "express": "^4.18.2" + } +} diff --git a/tests/examples/source/devfiles/nodejs/project-with-endpoint-on-loopback/server.js b/tests/examples/source/devfiles/nodejs/project-with-endpoint-on-loopback/server.js new file mode 100644 index 00000000000..bb7fc29d393 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/project-with-endpoint-on-loopback/server.js @@ -0,0 +1,24 @@ +const express = require('express'); +const http = require('http'); + +/* + * App endpoints are bound to 0.0.0.0:3000 + */ +const app = express(); +app.get('/', (req, res) => { + console.log('GET /'); + res.send('Hello from Node.js Application!'); +}); +app.get('*', (req, res) => { res.status(404).send("Not Found"); }); +http.createServer(app).listen(3000, '0.0.0.0', () => { console.log(`App started on 0.0.0.0:3000`); }); + +/* + * Admin endpoints are bound to 127.0.0.1:3001 + */ +const adminApp = express(); +adminApp.get('/', (req, res) => { + console.log('[admin] GET /'); + res.send('Hello from Node.js Admin Application!'); +}); +adminApp.get('*', (req, res) => { res.status(404).send("Admin endpoint not Found"); }); +http.createServer(adminApp).listen(3001, '127.0.0.1', () => { console.log(`Admin App started on 127.0.0.1:3001`); }); diff --git a/tests/integration/cmd_describe_component_test.go b/tests/integration/cmd_describe_component_test.go index 440044798b0..2615884511a 100644 --- a/tests/integration/cmd_describe_component_test.go +++ b/tests/integration/cmd_describe_component_test.go @@ -250,6 +250,10 @@ var _ = Describe("odo describe component command tests", func() { opts := helper.DevSessionOpts{RunOnPodman: podman} if debug { opts.CmdlineArgs = []string{"--debug"} + if podman { + // TODO(rm3l): use forward-localhost when it is implemented + opts.CmdlineArgs = append(opts.CmdlineArgs, "--ignore-localhost") + } } var err error devSession, _, _, ports, err = helper.StartDevMode(opts) diff --git a/tests/integration/cmd_dev_debug_test.go b/tests/integration/cmd_dev_debug_test.go index c34a28d4046..449ce4feec8 100644 --- a/tests/integration/cmd_dev_debug_test.go +++ b/tests/integration/cmd_dev_debug_test.go @@ -45,10 +45,15 @@ var _ = Describe("odo dev debug command tests", func() { var ports map[string]string BeforeEach(func() { var err error - devSession, _, _, ports, err = helper.StartDevMode(helper.DevSessionOpts{ + opts := helper.DevSessionOpts{ CmdlineArgs: []string{"--debug"}, RunOnPodman: podman, - }) + } + if podman { + // TODO(rm3l): use forward-localhost when it is implemented + opts.CmdlineArgs = append(opts.CmdlineArgs, "--ignore-localhost") + } + devSession, _, _, ports, err = helper.StartDevMode(opts) Expect(err).ToNot(HaveOccurred()) }) @@ -62,7 +67,7 @@ var _ = Describe("odo dev debug command tests", func() { helper.HttpWaitForWithStatus("http://"+ports["3000"], "Hello from Node.js Starter Application!", 12, 5, 200) }) if podman { - //TODO(rm3l): Remove this once https://github.com/redhat-developer/odo/issues/6510 is fixed + //TODO(rm3l): Remove this once https://github.com/redhat-developer/odo/issues/6510 is fixed and --forward-localhost is implemented Skip("temporarily skipped on Podman because of https://github.com/redhat-developer/odo/issues/6510") } By("expecting a ws connection when tried to connect on default debug port locally", func() { diff --git a/tests/integration/cmd_dev_test.go b/tests/integration/cmd_dev_test.go index 2a8c1eee95b..f6916cc117f 100644 --- a/tests/integration/cmd_dev_test.go +++ b/tests/integration/cmd_dev_test.go @@ -16,6 +16,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/onsi/gomega/types" corev1 "k8s.io/api/core/v1" "k8s.io/utils/pointer" @@ -3342,4 +3343,156 @@ CMD ["npm", "start"] component.ExpectIsNotDeployed() }) }) + + When("running applications listening on the container loopback interface", func() { + + BeforeEach(func() { + helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project-with-endpoint-on-loopback"), commonVar.Context) + helper.CopyExampleDevFile(filepath.Join("source", "devfiles", "nodejs", "devfile-with-endpoint-on-loopback.yaml"), + filepath.Join(commonVar.Context, "devfile.yaml"), + helper.DevfileMetadataNameSetter(cmpName)) + }) + + haveHttpResponse := func(status int, body string) types.GomegaMatcher { + return WithTransform(func(urlWithoutProto string) (*http.Response, error) { + return http.Get("http://" + urlWithoutProto) + }, SatisfyAll(HaveHTTPStatus(status), HaveHTTPBody(body))) + } + + for _, plt := range []string{"", "cluster"} { + plt := plt + + It("should error out if using --ignore-localhost on any platform other than Podman", func() { + args := []string{"dev", "--ignore-localhost", "--random-ports"} + var env []string + if plt != "" { + args = append(args, "--platform", plt) + env = append(env, "ODO_EXPERIMENTAL_MODE=true") + } + stderr := helper.Cmd("odo", args...).AddEnv(env...).ShouldFail().Err() + Expect(stderr).Should(ContainSubstring("--ignore-localhost cannot be used when running in cluster mode")) + }) + } + + When("running on default cluster platform", func() { + var devSession helper.DevSession + var stdout, stderr string + var ports map[string]string + + BeforeEach(func() { + var bOut, bErr []byte + var err error + devSession, bOut, bErr, ports, err = helper.StartDevMode(helper.DevSessionOpts{}) + Expect(err).ShouldNot(HaveOccurred()) + stdout = string(bOut) + stderr = string(bErr) + }) + + AfterEach(func() { + devSession.Stop() + devSession.WaitEnd() + }) + + It("should port-forward successfully", func() { + By("not displaying warning message for loopback port", func() { + Expect(stderr).ShouldNot(ContainSubstring("Detected that the following port(s) can be reached only via the container loopback interface")) + }) + By("forwarding both loopback and non-loopback ports", func() { + Expect(ports).Should(HaveLen(2)) + Expect(ports).Should(SatisfyAll(HaveKey("3000"), HaveKey("3001"))) + }) + By("displaying both loopback and non-loopback ports as forwarded", func() { + Expect(stdout).Should(SatisfyAll( + ContainSubstring("Forwarding from %s -> 3000", ports["3000"]), + ContainSubstring("Forwarding from %s -> 3001", ports["3001"]))) + }) + By("reaching both loopback and non-loopback ports via port-forwarding", func() { + for port, body := range map[int]string{ + 3000: "Hello from Node.js Application!", + 3001: "Hello from Node.js Admin Application!", + } { + Eventually(func(g Gomega) { + g.Expect(ports[strconv.Itoa(port)]).Should(haveHttpResponse(http.StatusOK, body)) + }).WithTimeout(60 * time.Second).WithPolling(3 * time.Second).Should(Succeed()) + } + }) + }) + }) + + Context("running on Podman", Label(helper.LabelPodman), func() { + + It("should error out if not ignoring localhost", func() { + stderr := helper.Cmd("odo", "dev", "--random-ports", "--platform", "podman").AddEnv("ODO_EXPERIMENTAL_MODE=true").ShouldFail().Err() + Expect(stderr).Should(ContainSubstring("Detected that the following port(s) can be reached only via the container loopback interface: admin (3001)")) + }) + + When("ignoring localhost", func() { + + var devSession helper.DevSession + var stderr string + var ports map[string]string + + BeforeEach(func() { + var bErr []byte + var err error + devSession, _, bErr, ports, err = helper.StartDevMode(helper.DevSessionOpts{ + RunOnPodman: true, + CmdlineArgs: []string{"--ignore-localhost"}, + }) + Expect(err).ShouldNot(HaveOccurred()) + stderr = string(bErr) + }) + + AfterEach(func() { + devSession.Stop() + devSession.WaitEnd() + }) + + It("should port-forward successfully", func() { + By("displaying warning message for loopback port", func() { + Expect(stderr).Should(ContainSubstring("Detected that the following port(s) can be reached only via the container loopback interface: admin (3001)")) + }) + By("reaching the local port for the non-loopback interface", func() { + Eventually(func(g Gomega) { + g.Expect(ports["3000"]).Should(haveHttpResponse(http.StatusOK, "Hello from Node.js Application!")) + }).WithTimeout(60 * time.Second).WithPolling(3 * time.Second).Should(Succeed()) + }) + By("not succeeding to reach the local port for the loopback interface", func() { + // By design, Podman will not forward to container apps listening on localhost. + // See https://github.com/redhat-developer/odo/issues/6510 and https://github.com/containers/podman/issues/17353 + Consistently(func() error { + _, err := http.Get("http://" + ports["3001"]) + return err + }).Should(HaveOccurred()) + }) + }) + + When("making changes in the project source code during the dev session", func() { + BeforeEach(func() { + helper.ReplaceString(filepath.Join(commonVar.Context, "server.js"), "Hello from", "Hiya from the updated") + var err error + _, _, ports, err = devSession.WaitSync() + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("should port-forward successfully", func() { + By("reaching the local port for the non-loopback interface", func() { + Eventually(func(g Gomega) { + g.Expect(ports["3000"]).Should(haveHttpResponse(http.StatusOK, "Hiya from the updated Node.js Application!")) + }).WithTimeout(60 * time.Second).WithPolling(3 * time.Second).Should(Succeed()) + }) + By("not succeeding to reach the local port for the loopback interface", func() { + // By design, Podman will not forward to container apps listening on localhost. + // See https://github.com/redhat-developer/odo/issues/6510 and https://github.com/containers/podman/issues/17353 + Consistently(func() error { + _, err := http.Get("http://" + ports["3001"]) + return err + }).Should(HaveOccurred()) + }) + }) + }) + }) + }) + + }) })