mirror of https://github.com/coder/coder.git
feat: Support for comma-separation and ranges in port-forward (#4166)
Fixes #3766
This commit is contained in:
parent
4919975f13
commit
092a22f242
|
@ -18,6 +18,7 @@ import (
|
||||||
|
|
||||||
"cdr.dev/slog"
|
"cdr.dev/slog"
|
||||||
"github.com/coder/coder/agent"
|
"github.com/coder/coder/agent"
|
||||||
|
"github.com/coder/coder/cli/cliflag"
|
||||||
"github.com/coder/coder/cli/cliui"
|
"github.com/coder/coder/cli/cliui"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
)
|
)
|
||||||
|
@ -45,6 +46,10 @@ func portForward() *cobra.Command {
|
||||||
Description: "Port forward multiple TCP ports and a UDP port",
|
Description: "Port forward multiple TCP ports and a UDP port",
|
||||||
Command: "coder port-forward <workspace> --tcp 8080:8080 --tcp 9000:3000 --udp 5353:53",
|
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 {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
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")
|
cliflag.StringArrayVarP(cmd.Flags(), &tcpForwards, "tcp", "p", "CODER_PORT_FORWARD_TCP", nil, "Forward TCP port(s) 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(), &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
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,32 +247,40 @@ type portForwardSpec struct {
|
||||||
func parsePortForwards(tcpSpecs, udpSpecs []string) ([]portForwardSpec, error) {
|
func parsePortForwards(tcpSpecs, udpSpecs []string) ([]portForwardSpec, error) {
|
||||||
specs := []portForwardSpec{}
|
specs := []portForwardSpec{}
|
||||||
|
|
||||||
for _, spec := range tcpSpecs {
|
for _, specEntry := range tcpSpecs {
|
||||||
local, remote, err := parsePortPort(spec)
|
for _, spec := range strings.Split(specEntry, ",") {
|
||||||
if err != nil {
|
ports, err := parseSrcDestPorts(spec)
|
||||||
return nil, xerrors.Errorf("failed to parse TCP port-forward specification %q: %w", spec, err)
|
if err != nil {
|
||||||
}
|
return nil, xerrors.Errorf("failed to parse TCP port-forward specification %q: %w", spec, err)
|
||||||
|
}
|
||||||
|
|
||||||
specs = append(specs, portForwardSpec{
|
for _, port := range ports {
|
||||||
listenNetwork: "tcp",
|
specs = append(specs, portForwardSpec{
|
||||||
listenAddress: fmt.Sprintf("127.0.0.1:%v", local),
|
listenNetwork: "tcp",
|
||||||
dialNetwork: "tcp",
|
listenAddress: fmt.Sprintf("127.0.0.1:%v", port.local),
|
||||||
dialAddress: fmt.Sprintf("127.0.0.1:%v", remote),
|
dialNetwork: "tcp",
|
||||||
})
|
dialAddress: fmt.Sprintf("127.0.0.1:%v", port.remote),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, spec := range udpSpecs {
|
for _, specEntry := range udpSpecs {
|
||||||
local, remote, err := parsePortPort(spec)
|
for _, spec := range strings.Split(specEntry, ",") {
|
||||||
if err != nil {
|
ports, err := parseSrcDestPorts(spec)
|
||||||
return nil, xerrors.Errorf("failed to parse UDP port-forward specification %q: %w", spec, err)
|
if err != nil {
|
||||||
}
|
return nil, xerrors.Errorf("failed to parse UDP port-forward specification %q: %w", spec, err)
|
||||||
|
}
|
||||||
|
|
||||||
specs = append(specs, portForwardSpec{
|
for _, port := range ports {
|
||||||
listenNetwork: "udp",
|
specs = append(specs, portForwardSpec{
|
||||||
listenAddress: fmt.Sprintf("127.0.0.1:%v", local),
|
listenNetwork: "udp",
|
||||||
dialNetwork: "udp",
|
listenAddress: fmt.Sprintf("127.0.0.1:%v", port.local),
|
||||||
dialAddress: fmt.Sprintf("127.0.0.1:%v", remote),
|
dialNetwork: "udp",
|
||||||
})
|
dialAddress: fmt.Sprintf("127.0.0.1:%v", port.remote),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicate entries.
|
// Check for duplicate entries.
|
||||||
|
@ -295,24 +308,72 @@ func parsePort(in string) (uint16, error) {
|
||||||
return uint16(port), nil
|
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, ":")
|
parts := strings.Split(in, ":")
|
||||||
if len(parts) > 2 {
|
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 {
|
if len(parts) == 1 {
|
||||||
// Duplicate the single part
|
// Duplicate the single part
|
||||||
parts = append(parts, parts[0])
|
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])
|
return []parsedSrcDestPort{{local: local, remote: remote}}, nil
|
||||||
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 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,14 +12,34 @@ There are three ways to forward ports in Coder:
|
||||||
|
|
||||||
The `coder port-forward` command is generally more performant.
|
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
|
```console
|
||||||
coder port-forward myworkspace --tcp 8000:8080
|
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`.
|
For more examples, see `coder port-forward --help`.
|
||||||
|
|
||||||
## Dashboard
|
## Dashboard
|
||||||
|
|
Loading…
Reference in New Issue