feat: Support for comma-separation and ranges in port-forward (#4166)

Fixes #3766
This commit is contained in:
Mathias Fredriksson 2022-10-03 11:58:43 +03:00 committed by GitHub
parent 4919975f13
commit 092a22f242
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 207 additions and 36 deletions

View File

@ -18,6 +18,7 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/agent"
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
)
@ -45,6 +46,10 @@ func portForward() *cobra.Command {
Description: "Port forward multiple TCP ports and a UDP port",
Command: "coder port-forward <workspace> --tcp 8080:8080 --tcp 9000:3000 --udp 5353:53",
},
example{
Description: "Port forward multiple ports (TCP or UDP) in condensed syntax",
Command: "coder port-forward <workspace> --tcp 8080,9000:3000,9090-9092,10000-10002:10010-10012",
},
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithCancel(cmd.Context())
@ -164,8 +169,8 @@ func portForward() *cobra.Command {
},
}
cmd.Flags().StringArrayVarP(&tcpForwards, "tcp", "p", []string{}, "Forward a TCP port from the workspace to the local machine")
cmd.Flags().StringArrayVar(&udpForwards, "udp", []string{}, "Forward a UDP port from the workspace to the local machine. The UDP connection has TCP-like semantics to support stateful UDP protocols")
cliflag.StringArrayVarP(cmd.Flags(), &tcpForwards, "tcp", "p", "CODER_PORT_FORWARD_TCP", nil, "Forward TCP port(s) from the workspace to the local machine")
cliflag.StringArrayVarP(cmd.Flags(), &udpForwards, "udp", "", "CODER_PORT_FORWARD_UDP", nil, "Forward UDP port(s) from the workspace to the local machine. The UDP connection has TCP-like semantics to support stateful UDP protocols")
return cmd
}
@ -242,32 +247,40 @@ type portForwardSpec struct {
func parsePortForwards(tcpSpecs, udpSpecs []string) ([]portForwardSpec, error) {
specs := []portForwardSpec{}
for _, spec := range tcpSpecs {
local, remote, err := parsePortPort(spec)
if err != nil {
return nil, xerrors.Errorf("failed to parse TCP port-forward specification %q: %w", spec, err)
}
for _, specEntry := range tcpSpecs {
for _, spec := range strings.Split(specEntry, ",") {
ports, err := parseSrcDestPorts(spec)
if err != nil {
return nil, xerrors.Errorf("failed to parse TCP port-forward specification %q: %w", spec, err)
}
specs = append(specs, portForwardSpec{
listenNetwork: "tcp",
listenAddress: fmt.Sprintf("127.0.0.1:%v", local),
dialNetwork: "tcp",
dialAddress: fmt.Sprintf("127.0.0.1:%v", remote),
})
for _, port := range ports {
specs = append(specs, portForwardSpec{
listenNetwork: "tcp",
listenAddress: fmt.Sprintf("127.0.0.1:%v", port.local),
dialNetwork: "tcp",
dialAddress: fmt.Sprintf("127.0.0.1:%v", port.remote),
})
}
}
}
for _, spec := range udpSpecs {
local, remote, err := parsePortPort(spec)
if err != nil {
return nil, xerrors.Errorf("failed to parse UDP port-forward specification %q: %w", spec, err)
}
for _, specEntry := range udpSpecs {
for _, spec := range strings.Split(specEntry, ",") {
ports, err := parseSrcDestPorts(spec)
if err != nil {
return nil, xerrors.Errorf("failed to parse UDP port-forward specification %q: %w", spec, err)
}
specs = append(specs, portForwardSpec{
listenNetwork: "udp",
listenAddress: fmt.Sprintf("127.0.0.1:%v", local),
dialNetwork: "udp",
dialAddress: fmt.Sprintf("127.0.0.1:%v", remote),
})
for _, port := range ports {
specs = append(specs, portForwardSpec{
listenNetwork: "udp",
listenAddress: fmt.Sprintf("127.0.0.1:%v", port.local),
dialNetwork: "udp",
dialAddress: fmt.Sprintf("127.0.0.1:%v", port.remote),
})
}
}
}
// Check for duplicate entries.
@ -295,24 +308,72 @@ func parsePort(in string) (uint16, error) {
return uint16(port), nil
}
func parsePortPort(in string) (local uint16, remote uint16, err error) {
type parsedSrcDestPort struct {
local, remote uint16
}
func parseSrcDestPorts(in string) ([]parsedSrcDestPort, error) {
parts := strings.Split(in, ":")
if len(parts) > 2 {
return 0, 0, xerrors.Errorf("invalid port specification %q", in)
return nil, xerrors.Errorf("invalid port specification %q", in)
}
if len(parts) == 1 {
// Duplicate the single part
parts = append(parts, parts[0])
}
if !strings.Contains(parts[0], "-") {
local, err := parsePort(parts[0])
if err != nil {
return nil, xerrors.Errorf("parse local port from %q: %w", in, err)
}
remote, err := parsePort(parts[1])
if err != nil {
return nil, xerrors.Errorf("parse remote port from %q: %w", in, err)
}
local, err = parsePort(parts[0])
if err != nil {
return 0, 0, xerrors.Errorf("parse local port from %q: %w", in, err)
}
remote, err = parsePort(parts[1])
if err != nil {
return 0, 0, xerrors.Errorf("parse remote port from %q: %w", in, err)
return []parsedSrcDestPort{{local: local, remote: remote}}, nil
}
return local, remote, nil
local, err := parsePortRange(parts[0])
if err != nil {
return nil, xerrors.Errorf("parse local port range from %q: %w", in, err)
}
remote, err := parsePortRange(parts[1])
if err != nil {
return nil, xerrors.Errorf("parse remote port range from %q: %w", in, err)
}
if len(local) != len(remote) {
return nil, xerrors.Errorf("port ranges must be the same length, got %d ports forwarded to %d ports", len(local), len(remote))
}
var out []parsedSrcDestPort
for i := range local {
out = append(out, parsedSrcDestPort{
local: local[i],
remote: remote[i],
})
}
return out, nil
}
func parsePortRange(in string) ([]uint16, error) {
parts := strings.Split(in, "-")
if len(parts) != 2 {
return nil, xerrors.Errorf("invalid port range specification %q", in)
}
start, err := parsePort(parts[0])
if err != nil {
return nil, xerrors.Errorf("parse range start port from %q: %w", in, err)
}
end, err := parsePort(parts[1])
if err != nil {
return nil, xerrors.Errorf("parse range end port from %q: %w", in, err)
}
if end < start {
return nil, xerrors.Errorf("range end port %v is less than start port %v", end, start)
}
var ports []uint16
for i := start; i <= end; i++ {
ports = append(ports, i)
}
return ports, nil
}

View File

@ -0,0 +1,90 @@
package cli
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func Test_parsePortForwards(t *testing.T) {
t.Parallel()
portForwardSpecToString := func(v []portForwardSpec) (out []string) {
for _, p := range v {
require.Equal(t, p.listenNetwork, p.dialNetwork)
out = append(out, fmt.Sprintf("%s:%s", strings.Replace(p.listenAddress, "127.0.0.1:", "", 1), strings.Replace(p.dialAddress, "127.0.0.1:", "", 1)))
}
return out
}
type args struct {
tcpSpecs []string
udpSpecs []string
}
tests := []struct {
name string
args args
want []string
wantErr bool
}{
{
name: "TCP mixed ports and ranges",
args: args{
tcpSpecs: []string{
"8000,8080:8081,9000-9002,9003-9004:9005-9006",
"10000",
},
},
want: []string{
"8000:8000",
"8080:8081",
"9000:9000",
"9001:9001",
"9002:9002",
"9003:9005",
"9004:9006",
"10000:10000",
},
},
{
name: "UDP with port range",
args: args{
udpSpecs: []string{"8000,8080-8081"},
},
want: []string{
"8000:8000",
"8080:8080",
"8081:8081",
},
},
{
name: "Bad port range",
args: args{
tcpSpecs: []string{"8000-7000"},
},
wantErr: true,
},
{
name: "Bad dest port range",
args: args{
tcpSpecs: []string{"8080-8081:9080-9082"},
},
wantErr: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := parsePortForwards(tt.args.tcpSpecs, tt.args.udpSpecs)
if (err != nil) != tt.wantErr {
t.Fatalf("parsePortForwards() error = %v, wantErr %v", err, tt.wantErr)
return
}
gotStrings := portForwardSpecToString(got)
require.Equal(t, tt.want, gotStrings)
})
}
}

View File

@ -12,14 +12,34 @@ There are three ways to forward ports in Coder:
The `coder port-forward` command is generally more performant.
## coder port-forward
## The `coder port-forward` command
Forward the remote TCP port `8080` to local port `8000` like so:
This command can be used to forward TCP or UDP ports from the remote
workspace so they can be accessed locally. Both the TCP and UDP command
line flags (`--tcp` and `--udp`) can be given once or multiple times.
The supported syntax variations for the `--tcp` and `--udp` flag are:
- Single port with optional remote port: `local_port[:remote_port]`
- Comma separation `local_port1,local_port2`
- Port ranges `start_port-end_port`
- Any combination of the above
### Examples
Forward the remote TCP port `8080` to local port `8000`:
```console
coder port-forward myworkspace --tcp 8000:8080
```
Forward the remote TCP port `3000` and all ports from `9990` to `9999`
to their respective local ports.
```console
coder port-forward myworkspace --tcp 3000,9990-9999
```
For more examples, see `coder port-forward --help`.
## Dashboard