diff --git a/e2e/e2e_suite_test.go b/e2e/e2e_suite_test.go index 265250c75..004caa19c 100644 --- a/e2e/e2e_suite_test.go +++ b/e2e/e2e_suite_test.go @@ -5,6 +5,8 @@ import ( "testing" "time" + "github.com/loft-sh/devpod/e2e/framework" + "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" @@ -24,5 +26,11 @@ import ( func TestRunE2ETests(t *testing.T) { rand.Seed(time.Now().UTC().UnixNano()) gomega.RegisterFailHandler(ginkgo.Fail) + go func() { + err := framework.StartAgentServer() + if err != nil { + t.Error(err) + } + }() ginkgo.RunSpecs(t, "DevPod e2e suite") } diff --git a/e2e/framework/agent_server.go b/e2e/framework/agent_server.go new file mode 100644 index 000000000..c1e4b7df9 --- /dev/null +++ b/e2e/framework/agent_server.go @@ -0,0 +1,33 @@ +package framework + +import ( + "net" + "net/http" + "os" + "path/filepath" +) + +const agentServerPort = "9191" + +func StartAgentServer() error { + wd, err := os.Getwd() + if err != nil { + return err + } + + listener, err := net.Listen("tcp", ":"+agentServerPort) + if err != nil { + return err + } + + err = os.Setenv("DEVPOD_AGENT_URL", "http://localhost:"+agentServerPort) + if err != nil { + return err + } + + if err := http.Serve(listener, http.FileServer(http.Dir(filepath.Join(wd, "bin")))); err != nil { + return err + } + + return nil +} diff --git a/e2e/tests/up/testdata/docker-compose-forward-ports/.devcontainer.json b/e2e/tests/up/testdata/docker-compose-forward-ports/.devcontainer.json new file mode 100755 index 000000000..1f1ed8e5f --- /dev/null +++ b/e2e/tests/up/testdata/docker-compose-forward-ports/.devcontainer.json @@ -0,0 +1,12 @@ +{ + "name": "Go", + "dockerComposeFile": "./docker-compose.yaml", + "service": "app", + "runServices": ["nginx"], + "workspaceFolder": "/workspaces", + "forwardPorts": [ + 4000, + "3000", + "nginx:8989" + ] +} \ No newline at end of file diff --git a/e2e/tests/up/testdata/docker-compose-forward-ports/docker-compose.yaml b/e2e/tests/up/testdata/docker-compose-forward-ports/docker-compose.yaml new file mode 100644 index 000000000..78993f0cd --- /dev/null +++ b/e2e/tests/up/testdata/docker-compose-forward-ports/docker-compose.yaml @@ -0,0 +1,12 @@ +version: '3' + +services: + app: + image: mcr.microsoft.com/devcontainers/go:0-1.19-bullseye + command: sleep infinity + volumes: + - .:/workspaces:cached + nginx: + image: nginx + ports: + - "8989:80" \ No newline at end of file diff --git a/e2e/tests/up/up.go b/e2e/tests/up/up.go index 309ebac79..c4b6cb5df 100644 --- a/e2e/tests/up/up.go +++ b/e2e/tests/up/up.go @@ -4,7 +4,9 @@ import ( "context" "fmt" "io" + "net/http" "os" + "os/exec" "path/filepath" "time" @@ -45,6 +47,9 @@ var _ = DevPodDescribe("devpod up test suite", func() { err = f.DevPodProviderUse(context.Background(), "docker") framework.ExpectNoError(err) + projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) + ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) + // Wait for devpod workspace to come online (deadline: 30s) err = f.DevPodUp(ctx, tempDir) framework.ExpectNoError(err) @@ -60,13 +65,12 @@ var _ = DevPodDescribe("devpod up test suite", func() { err = f.DevPodProviderUse(context.Background(), "docker") framework.ExpectNoError(err) - err = f.DevPodUp(ctx, tempDir) - framework.ExpectNoError(err) - - // Check for docker-compose container running projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) + err = f.DevPodUp(ctx, tempDir) + framework.ExpectNoError(err) + ids, err := dockerHelper.FindContainer([]string{ fmt.Sprintf("%s=%s", config.DockerIDLabel, projectName), }) @@ -112,13 +116,12 @@ var _ = DevPodDescribe("devpod up test suite", func() { err = f.DevPodProviderUse(context.Background(), "docker") framework.ExpectNoError(err) - err = f.DevPodUp(ctx, tempDir, "--debug") - framework.ExpectNoError(err) - - // Check for docker-compose container running projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) + err = f.DevPodUp(ctx, tempDir, "--debug") + framework.ExpectNoError(err) + ids, err := dockerHelper.FindContainer([]string{ fmt.Sprintf("%s=%s", config.DockerIDLabel, projectName), }) @@ -161,13 +164,12 @@ var _ = DevPodDescribe("devpod up test suite", func() { err = f.DevPodProviderUse(context.Background(), "docker") framework.ExpectNoError(err) - err = f.DevPodUp(ctx, tempDir) - framework.ExpectNoError(err) - - // Check for docker-compose container running projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) + err = f.DevPodUp(ctx, tempDir) + framework.ExpectNoError(err) + ids, err := dockerHelper.FindContainer([]string{ fmt.Sprintf("%s=%s", compose.ProjectLabel, projectName), fmt.Sprintf("%s=%s", compose.ServiceLabel, "app"), @@ -198,13 +200,12 @@ var _ = DevPodDescribe("devpod up test suite", func() { err = f.DevPodProviderUse(context.Background(), "docker") framework.ExpectNoError(err) - err = f.DevPodUp(ctx, tempDir) - framework.ExpectNoError(err) - - // Check for docker-compose container running projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) + err = f.DevPodUp(ctx, tempDir) + framework.ExpectNoError(err) + ids, err := dockerHelper.FindContainer([]string{ fmt.Sprintf("%s=%s", compose.ProjectLabel, projectName), fmt.Sprintf("%s=%s", compose.ServiceLabel, "app"), @@ -235,13 +236,12 @@ var _ = DevPodDescribe("devpod up test suite", func() { err = f.DevPodProviderUse(context.Background(), "docker") framework.ExpectNoError(err) - err = f.DevPodUp(ctx, tempDir) - framework.ExpectNoError(err) - - // Check for docker-compose container running projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) + err = f.DevPodUp(ctx, tempDir) + framework.ExpectNoError(err) + appIDs, err := dockerHelper.FindContainer([]string{ fmt.Sprintf("%s=%s", compose.ProjectLabel, projectName), fmt.Sprintf("%s=%s", compose.ServiceLabel, "app"), @@ -267,13 +267,12 @@ var _ = DevPodDescribe("devpod up test suite", func() { err = f.DevPodProviderUse(context.Background(), "docker") framework.ExpectNoError(err) - err = f.DevPodUp(ctx, tempDir) - framework.ExpectNoError(err) - - // Check for docker-compose container running projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) + err = f.DevPodUp(ctx, tempDir) + framework.ExpectNoError(err) + ids, err := dockerHelper.FindContainer([]string{ fmt.Sprintf("%s=%s", compose.ProjectLabel, projectName), fmt.Sprintf("%s=%s", compose.ServiceLabel, "app"), @@ -304,13 +303,12 @@ var _ = DevPodDescribe("devpod up test suite", func() { err = f.DevPodProviderUse(context.Background(), "docker") framework.ExpectNoError(err) - err = f.DevPodUp(ctx, tempDir) - framework.ExpectNoError(err) - - // Check for docker-compose container running projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) + err = f.DevPodUp(ctx, tempDir) + framework.ExpectNoError(err) + ids, err := dockerHelper.FindContainer([]string{ fmt.Sprintf("%s=%s", compose.ProjectLabel, projectName), fmt.Sprintf("%s=%s", compose.ServiceLabel, "app"), @@ -332,13 +330,12 @@ var _ = DevPodDescribe("devpod up test suite", func() { err = f.DevPodProviderUse(context.Background(), "docker") framework.ExpectNoError(err) - err = f.DevPodUp(ctx, tempDir) - framework.ExpectNoError(err) - - // Check for docker-compose container running projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) + err = f.DevPodUp(ctx, tempDir) + framework.ExpectNoError(err) + ids, err := dockerHelper.FindContainer([]string{ fmt.Sprintf("%s=%s", compose.ProjectLabel, projectName), fmt.Sprintf("%s=%s", compose.ServiceLabel, "app"), @@ -360,13 +357,12 @@ var _ = DevPodDescribe("devpod up test suite", func() { err = f.DevPodProviderUse(context.Background(), "docker") framework.ExpectNoError(err) - err = f.DevPodUp(ctx, tempDir) - framework.ExpectNoError(err) - - // Check for docker-compose container running projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) + err = f.DevPodUp(ctx, tempDir) + framework.ExpectNoError(err) + ids, err := dockerHelper.FindContainer([]string{ fmt.Sprintf("%s=%s", compose.ProjectLabel, projectName), fmt.Sprintf("%s=%s", compose.ServiceLabel, "app"), @@ -392,13 +388,12 @@ var _ = DevPodDescribe("devpod up test suite", func() { err = f.DevPodProviderUse(context.Background(), "docker") framework.ExpectNoError(err) - err = f.DevPodUp(ctx, tempDir) - framework.ExpectNoError(err) - - // Check for docker-compose container running projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) + err = f.DevPodUp(ctx, tempDir) + framework.ExpectNoError(err) + ids, err := dockerHelper.FindContainer([]string{ fmt.Sprintf("%s=%s", compose.ProjectLabel, projectName), fmt.Sprintf("%s=%s", compose.ServiceLabel, "app"), @@ -425,13 +420,12 @@ var _ = DevPodDescribe("devpod up test suite", func() { err = f.DevPodProviderUse(context.Background(), "docker") framework.ExpectNoError(err) - err = f.DevPodUp(ctx, tempDir) - framework.ExpectNoError(err) - - // Check for docker-compose container running projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) + err = f.DevPodUp(ctx, tempDir) + framework.ExpectNoError(err) + ids, err := dockerHelper.FindContainer([]string{ fmt.Sprintf("%s=%s", compose.ProjectLabel, projectName), fmt.Sprintf("%s=%s", compose.ServiceLabel, "app"), @@ -458,13 +452,12 @@ var _ = DevPodDescribe("devpod up test suite", func() { err = f.DevPodProviderUse(context.Background(), "docker") framework.ExpectNoError(err) - err = f.DevPodUp(ctx, tempDir) - framework.ExpectNoError(err) - - // Check for docker-compose container running projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) + err = f.DevPodUp(ctx, tempDir) + framework.ExpectNoError(err) + ids, err := dockerHelper.FindContainer([]string{ fmt.Sprintf("%s=%s", compose.ProjectLabel, projectName), fmt.Sprintf("%s=%s", compose.ServiceLabel, "app"), @@ -491,13 +484,12 @@ var _ = DevPodDescribe("devpod up test suite", func() { err = f.DevPodProviderUse(context.Background(), "docker") framework.ExpectNoError(err) - err = f.DevPodUp(ctx, tempDir) - framework.ExpectNoError(err) - - // Check for docker-compose container running projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) + err = f.DevPodUp(ctx, tempDir) + framework.ExpectNoError(err) + ids, err := dockerHelper.FindContainer([]string{ fmt.Sprintf("%s=%s", compose.ProjectLabel, projectName), fmt.Sprintf("%s=%s", compose.ServiceLabel, "app"), @@ -522,13 +514,12 @@ var _ = DevPodDescribe("devpod up test suite", func() { err = f.DevPodProviderUse(context.Background(), "docker") framework.ExpectNoError(err) - err = f.DevPodUp(ctx, tempDir) - framework.ExpectNoError(err) - - // Check for docker-compose container running projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) + err = f.DevPodUp(ctx, tempDir) + framework.ExpectNoError(err) + ids, err := dockerHelper.FindContainer([]string{ fmt.Sprintf("%s=%s", compose.ProjectLabel, projectName), fmt.Sprintf("%s=%s", compose.ServiceLabel, "app"), @@ -550,13 +541,12 @@ var _ = DevPodDescribe("devpod up test suite", func() { err = f.DevPodProviderUse(context.Background(), "docker") framework.ExpectNoError(err) - err = f.DevPodUp(ctx, tempDir) - framework.ExpectNoError(err) - - // Check for docker-compose container running projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) + err = f.DevPodUp(ctx, tempDir) + framework.ExpectNoError(err) + ids, err := dockerHelper.FindContainer([]string{ fmt.Sprintf("%s=%s", compose.ProjectLabel, projectName), fmt.Sprintf("%s=%s", compose.ServiceLabel, "app"), @@ -603,13 +593,13 @@ var _ = DevPodDescribe("devpod up test suite", func() { err = f.DevPodProviderUse(context.Background(), "docker") framework.ExpectNoError(err) - err = f.DevPodUp(ctx, tempDir, "--debug") - framework.ExpectNoError(err) - // Check for docker-compose container running projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) + err = f.DevPodUp(ctx, tempDir, "--debug") + framework.ExpectNoError(err) + ids, err := dockerHelper.FindContainer([]string{ fmt.Sprintf("%s=%s", compose.ProjectLabel, projectName), fmt.Sprintf("%s=%s", compose.ServiceLabel, "app"), @@ -647,6 +637,73 @@ var _ = DevPodDescribe("devpod up test suite", func() { framework.ExpectNoError(err) }, ginkgo.SpecTimeout(60*time.Second)) + ginkgo.It("should start a new workspace with host:port forwardPorts", func(ctx context.Context) { + tempDir, err := framework.CopyToTempDir("tests/up/testdata/docker-compose-forward-ports") + framework.ExpectNoError(err) + ginkgo.DeferCleanup(framework.CleanupTempDir, initialDir, tempDir) + + f := framework.NewDefaultFramework(initialDir + "/bin") + _ = f.DevPodProviderAdd([]string{"docker"}) + err = f.DevPodProviderUse(context.Background(), "docker") + framework.ExpectNoError(err) + + // Check for docker-compose container running + projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) + ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) + + err = f.DevPodUp(ctx, tempDir, "--debug") + framework.ExpectNoError(err) + + ids, err := dockerHelper.FindContainer([]string{ + fmt.Sprintf("%s=%s", compose.ProjectLabel, projectName), + fmt.Sprintf("%s=%s", compose.ServiceLabel, "app"), + }) + framework.ExpectNoError(err) + gomega.Expect(ids).To(gomega.HaveLen(1), "1 compose container to be created") + + done := make(chan error) + + sshContext, sshCancel := context.WithCancel(context.Background()) + go func() { + cmd := exec.CommandContext(sshContext, "ssh", projectName+".devpod", "sleep", "10") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + 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) { + // Postgres send EOF if the connection succeeds + response, err := http.Get("http://localhost:8989") + g.Expect(err).NotTo(gomega.HaveOccurred()) + + 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(60*time.Second)) + ginkgo.Context("with lifecycle commands", func() { ginkgo.It("should start a new workspace and execute array based lifecycle commands", func(ctx context.Context) { tempDir, err := framework.CopyToTempDir("tests/up/testdata/docker-compose-lifecycle-array") @@ -658,13 +715,12 @@ var _ = DevPodDescribe("devpod up test suite", func() { err = f.DevPodProviderUse(context.Background(), "docker") framework.ExpectNoError(err) - err = f.DevPodUp(ctx, tempDir) - framework.ExpectNoError(err) - - // Check for docker-compose container running projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) + err = f.DevPodUp(ctx, tempDir) + framework.ExpectNoError(err) + ids, err := dockerHelper.FindContainer([]string{ fmt.Sprintf("%s=%s", compose.ProjectLabel, projectName), fmt.Sprintf("%s=%s", compose.ServiceLabel, "app"), @@ -758,14 +814,14 @@ var _ = DevPodDescribe("devpod up test suite", func() { err = f.DevPodProviderUse(context.Background(), "docker") framework.ExpectNoError(err) + projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) + ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) + ginkgo.By("Starting DevPod") err = f.DevPodUp(ctx, tempDir) framework.ExpectNoError(err) ginkgo.By("Should start a docker-compose container") - projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) - ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) - ids, err := dockerHelper.FindContainer([]string{ fmt.Sprintf("%s=%s", compose.ProjectLabel, projectName), fmt.Sprintf("%s=%s", compose.ServiceLabel, "app"), @@ -810,14 +866,14 @@ var _ = DevPodDescribe("devpod up test suite", func() { err = f.DevPodProviderUse(context.Background(), "docker") framework.ExpectNoError(err) + projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) + ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) + ginkgo.By("Starting DevPod") err = f.DevPodUp(ctx, tempDir) framework.ExpectNoError(err) ginkgo.By("Should start a docker-compose container") - projectName := composeHelper.ToProjectName(filepath.Base(tempDir)) - ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), projectName) - ids, err := dockerHelper.FindContainer([]string{ fmt.Sprintf("%s=%s", compose.ProjectLabel, projectName), fmt.Sprintf("%s=%s", compose.ServiceLabel, "app"), diff --git a/pkg/devcontainer/config/config.go b/pkg/devcontainer/config/config.go index 5e3f916db..fdb49f360 100644 --- a/pkg/devcontainer/config/config.go +++ b/pkg/devcontainer/config/config.go @@ -2,6 +2,7 @@ package config import ( "encoding/json" + "fmt" "strconv" "strings" @@ -50,7 +51,7 @@ type DevContainerConfigBase struct { OverrideFeatureInstallOrder []string `json:"overrideFeatureInstallOrder,omitempty"` // Ports that are forwarded from the container to the local machine. Can be an integer port number, or a string of the format "host:port_number". - ForwardPorts []json.Number `json:"forwardPorts,omitempty"` + ForwardPorts []*ForwardPort `json:"forwardPorts,omitempty"` // Set default properties that are applied when a specific port number is forwarded. PortsAttributes map[string]PortAttribute `json:"portAttributes,omitempty"` @@ -354,3 +355,55 @@ func (m *Mount) UnmarshalJSON(data []byte) error { } return types.ErrUnsupportedType } + +type ForwardPort struct { + data []byte + Host string + Port int64 +} + +func (p *ForwardPort) MarshalJSON() ([]byte, error) { + return p.data, nil +} + +func (p *ForwardPort) UnmarshalJSON(data []byte) error { + p.data = data + + var jsonObj interface{} + err := json.Unmarshal(data, &jsonObj) + if err != nil { + return err + } + + switch obj := jsonObj.(type) { + case float64: + p.Host = "localhost" + p.Port = int64(obj) + return nil + case string: + tokens := strings.Split(obj, ":") + if len(tokens) == 1 { + port, err := strconv.ParseInt(tokens[0], 10, 64) + if err == nil { + p.Host = "localhost" + p.Port = port + return nil + } + } + + if len(tokens) == 2 { + port, err := strconv.ParseInt(tokens[1], 10, 64) + if err == nil { + p.Host = tokens[0] + p.Port = port + return nil + } + } + } + + return fmt.Errorf("invalid forwardPorts port") +} + +func (p *ForwardPort) String() string { + return fmt.Sprintf("%s:%d", p.Host, p.Port) +} diff --git a/pkg/devcontainer/config/merge.go b/pkg/devcontainer/config/merge.go index 9216c09be..54a21899b 100644 --- a/pkg/devcontainer/config/merge.go +++ b/pkg/devcontainer/config/merge.go @@ -1,9 +1,6 @@ package config import ( - "encoding/json" - "strconv" - "github.com/loft-sh/devpod/pkg/types" ) @@ -103,16 +100,12 @@ func mergeHostRequirements(entries []*ImageMetadata) *HostRequirements { return nil } -func mergeForwardPorts(entries []*ImageMetadata) []json.Number { +func mergeForwardPorts(entries []*ImageMetadata) []*ForwardPort { portMap := map[string]bool{} - retPorts := []json.Number{} + var retPorts []*ForwardPort for _, entry := range entries { for _, port := range entry.ForwardPorts { portString := port.String() - _, err := strconv.Atoi(portString) - if err == nil { - portString = "localhost:" + portString - } if portMap[portString] { continue } diff --git a/pkg/tunnel/services.go b/pkg/tunnel/services.go index e185d0cf6..c85135ae9 100644 --- a/pkg/tunnel/services.go +++ b/pkg/tunnel/services.go @@ -154,21 +154,14 @@ func forwardDevContainerPorts(ctx context.Context, workspaceClient client.Worksp // forward ports for _, port := range result.MergedConfig.ForwardPorts { - // convert port - i, err := port.Int64() - if err != nil { - log.Debugf("Error parsing forwardPort %s: %v", port.String(), err) - continue - } - // try to forward - go func(i int64, port json.Number) { + go func(port *config2.ForwardPort) { log.Debugf("Forward port %s", port.String()) - err = devssh.PortForward(ctx, containerClient, "localhost:"+port.String(), "localhost:"+port.String(), log) + err = devssh.PortForward(ctx, containerClient, fmt.Sprintf("localhost:%d", port.Port), port.String(), log) if err != nil { - log.Debugf("Error port forwarding %d: %v", int(i), err) + log.Debugf("Error port forwarding %s: %v", port.String(), err) } - }(i, port) + }(port) forwardedPorts = append(forwardedPorts, port.String()) }