Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions e2e/tests/up-docker-compose/up_docker_compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"fmt"
"io"
"net"
"net/http"
"os"
"os/exec"
Expand Down Expand Up @@ -160,6 +161,77 @@ var _ = ginkgo.Describe(
))
}, ginkgo.SpecTimeout(framework.TimeoutLong()))

ginkgo.It("ssh forward ports support remote service names", func(ctx context.Context) {
_, workspace, err := tc.setupAndStartWorkspace(
ctx,
"tests/up-docker-compose/testdata/docker-compose-forward-ports",
"--debug",
)
framework.ExpectNoError(err)

ids, err := findComposeContainer(
ctx,
tc.dockerHelper,
tc.composeHelper,
workspace.UID,
"app",
)
framework.ExpectNoError(err)
gomega.Expect(ids).To(gomega.HaveLen(1), "1 compose container to be created")

listener, err := net.Listen("tcp", "127.0.0.1:0")
framework.ExpectNoError(err)
localPort := listener.Addr().(*net.TCPAddr).Port
framework.ExpectNoError(listener.Close())

done := make(chan error)
sshContext, sshCancel := context.WithCancel(context.Background())
go func() {
// #nosec G204 -- test command with controlled arguments
cmd := exec.CommandContext(
sshContext,
filepath.Join(tc.f.DevsyBinDir, tc.f.DevsyBinName),
"ssh",
"--forward-ports",
fmt.Sprintf("%d:nginx:8080", localPort),
workspace.ID,
)

if err := cmd.Start(); err != nil {
done <- err
return
}

if err := cmd.Wait(); err != nil {
done <- err
return
}

done <- nil
}()

gomega.Eventually(func(g gomega.Gomega) {
response, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d", localPort))
g.Expect(err).NotTo(gomega.HaveOccurred())
defer func() { _ = response.Body.Close() }()

body, err := io.ReadAll(response.Body)
g.Expect(err).NotTo(gomega.HaveOccurred())
g.Expect(body).To(gomega.ContainSubstring("Thank you for using nginx."))
}).
WithPolling(1 * time.Second).
WithTimeout(20 * time.Second).
Should(gomega.Succeed())

sshCancel()
err = <-done

gomega.Expect(err).To(gomega.Or(
gomega.MatchError("signal: killed"),
gomega.MatchError(context.Canceled),
))
}, ginkgo.SpecTimeout(framework.TimeoutLong()))

ginkgo.It("features", func(ctx context.Context) {
tempDir, workspace, err := tc.setupAndStartWorkspace(
ctx,
Expand Down
25 changes: 12 additions & 13 deletions pkg/port/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package port

import (
"fmt"
"net"
"strconv"
"strings"
)

const defaultHost = "localhost"

type Address struct {
Protocol string
Address string
Expand Down Expand Up @@ -39,26 +40,21 @@ func ParsePortSpec(port string) (Mapping, error) {
}, nil
}

func toAddress(ip, port string) (Address, error) {
// check if port is integer
func toAddress(host, port string) (Address, error) {
_, err := strconv.Atoi(port)
if err == nil {
if ip == "" {
ip = "localhost"
}

if ip != "localhost" && net.ParseIP(ip) == nil {
return Address{}, fmt.Errorf("not an ip address %s", ip)
if host == "" {
host = defaultHost
}

return Address{
Protocol: "tcp",
Address: ip + ":" + port,
Address: host + ":" + port,
}, nil
}

if ip != "" {
return Address{}, fmt.Errorf("unexpected ip address for unix socket: %s", ip)
if host != "" {
return Address{}, fmt.Errorf("unexpected host for unix socket: %s", host)
}

return Address{
Expand All @@ -78,7 +74,10 @@ func splitParts(rawport string) (string, string, string, string, error) {
case 2:
return "", parts[0], "", containerport, nil
case 3:
if parts[1] == "localhost" || net.ParseIP(parts[1]) != nil {
// a:b:c — if middle token is non-numeric, it's a host/IP for the
// container side: hostPort:containerHost:containerPort.
// Otherwise it's hostHost:hostPort:containerPort.
if _, err := strconv.Atoi(parts[1]); err != nil {
return "", parts[0], parts[1], containerport, nil
}

Expand Down
236 changes: 236 additions & 0 deletions pkg/port/parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
package port

import (
"testing"
)

const (
protoTCP = "tcp"
protoUnix = "unix"

addrLocalhost8080 = "localhost:8080"
addrLocalhost3000 = "localhost:3000"
testUnixSocket = "/var/run/app.sock"
)

func TestParsePortSpec_BasicPorts(t *testing.T) {
tests := []struct {
name string
input string
want Mapping
}{
{
name: "bare port",
input: "8080",
want: Mapping{
Host: Address{Protocol: protoTCP, Address: addrLocalhost8080},
Container: Address{Protocol: protoTCP, Address: addrLocalhost8080},
},
},
{
name: "host port to container port",
input: "8080:3000",
want: Mapping{
Host: Address{Protocol: protoTCP, Address: addrLocalhost8080},
Container: Address{Protocol: protoTCP, Address: addrLocalhost3000},
},
},
{
name: "unix socket",
input: testUnixSocket,
want: Mapping{
Host: Address{Protocol: protoUnix, Address: testUnixSocket},
Container: Address{Protocol: protoUnix, Address: testUnixSocket},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParsePortSpec(tt.input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.want {
t.Errorf("got %+v, want %+v", got, tt.want)
}
})
}
}

func TestParsePortSpec_Hostnames(t *testing.T) {
tests := []struct {
name string
input string
want Mapping
}{
{
name: "IP host three-part",
input: "127.0.0.1:8080:3000",
want: Mapping{
Host: Address{Protocol: protoTCP, Address: "127.0.0.1:8080"},
Container: Address{Protocol: protoTCP, Address: addrLocalhost3000},
},
},
{
name: "localhost explicit three-part",
input: "localhost:8080:3000",
want: Mapping{
Host: Address{Protocol: protoTCP, Address: addrLocalhost8080},
Container: Address{Protocol: protoTCP, Address: addrLocalhost3000},
},
},
{
name: "hostname host",
input: "database.internal:5432:5432",
want: Mapping{
Host: Address{Protocol: protoTCP, Address: "database.internal:5432"},
Container: Address{Protocol: protoTCP, Address: "localhost:5432"},
},
},
{
name: "container hostname in three-part spec",
input: "8080:redis:6379",
want: Mapping{
Host: Address{Protocol: protoTCP, Address: addrLocalhost8080},
Container: Address{Protocol: protoTCP, Address: "redis:6379"},
},
},
{
name: "four-part full spec with hostnames",
input: "myhost:8080:mycontainer:3000",
want: Mapping{
Host: Address{Protocol: protoTCP, Address: "myhost:8080"},
Container: Address{Protocol: protoTCP, Address: "mycontainer:3000"},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParsePortSpec(tt.input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.want {
t.Errorf("got %+v, want %+v", got, tt.want)
}
})
}
}

func TestParsePortSpec_Errors(t *testing.T) {
_, err := ParsePortSpec("a:b:c:d:e")
if err == nil {
t.Fatal("expected error for too many parts")
}
}

func TestToAddress_TCP(t *testing.T) {
tests := []struct {
name string
host string
port string
want Address
}{
{
"empty host defaults to localhost",
"",
"8080",
Address{Protocol: protoTCP, Address: addrLocalhost8080},
},
{defaultHost, defaultHost, "3000", Address{Protocol: protoTCP, Address: addrLocalhost3000}},
{
"IP address", "192.168.1.1", "443",
Address{Protocol: protoTCP, Address: "192.168.1.1:443"},
},
{
"hostname",
"database.internal",
"5432",
Address{Protocol: protoTCP, Address: "database.internal:5432"},
},
{"short hostname", "db", "5432", Address{Protocol: protoTCP, Address: "db:5432"}},
{"IPv6 address", "::1", "8080", Address{Protocol: protoTCP, Address: "::1:8080"}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := toAddress(tt.host, tt.port)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.want {
t.Errorf("got %+v, want %+v", got, tt.want)
}
})
}
}

func TestToAddress_Unix(t *testing.T) {
got, err := toAddress("", testUnixSocket)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
want := Address{Protocol: protoUnix, Address: testUnixSocket}
if got != want {
t.Errorf("got %+v, want %+v", got, want)
}
}

func TestToAddress_HostWithUnixErrors(t *testing.T) {
_, err := toAddress("myhost", testUnixSocket)
if err == nil {
t.Fatal("expected error for host with unix socket")
}
}

func TestSplitParts(t *testing.T) {
type result struct{ hostIP, hostPort, contIP, contPort string }

tests := []struct {
name string
input string
want result
}{
{"single port", "8080", result{"", "8080", "", "8080"}},
{"two parts", "8080:3000", result{"", "8080", "", "3000"}},
{"three parts numeric middle", "myhost:8080:3000", result{"myhost", "8080", "", "3000"}},
{"three parts hostname middle", "8080:redis:6379", result{"", "8080", "redis", "6379"}},
{
"three parts IP middle",
"8080:192.168.1.1:3000",
result{"", "8080", "192.168.1.1", "3000"},
},
{
"three parts localhost middle",
"8080:localhost:3000",
result{"", "8080", defaultHost, "3000"},
},
{
"four parts",
"myhost:8080:mycontainer:3000",
result{"myhost", "8080", "mycontainer", "3000"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hostIP, hostPort, contIP, contPort, err := splitParts(tt.input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
got := result{hostIP, hostPort, contIP, contPort}
if got != tt.want {
t.Errorf("got %+v, want %+v", got, tt.want)
}
})
}
}

func TestSplitParts_Errors(t *testing.T) {
_, _, _, _, err := splitParts("a:b:c:d:e")
if err == nil {
t.Fatal("expected error for five parts")
}
}
Loading