From 3735e7ea4a4369b0c6fa443f72027671ff295d49 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 1 Apr 2025 15:18:29 +0100 Subject: [PATCH 1/9] feat(cli): support opening devcontainers in vscode --- cli/open.go | 140 ++++++++++++++++--- cli/open_test.go | 339 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 461 insertions(+), 18 deletions(-) diff --git a/cli/open.go b/cli/open.go index d0946854ddb25..496178420b0a2 100644 --- a/cli/open.go +++ b/cli/open.go @@ -42,6 +42,7 @@ func (r *RootCmd) openVSCode() *serpent.Command { generateToken bool testOpenError bool appearanceConfig codersdk.AppearanceConfig + containerName string ) client := new(codersdk.Client) @@ -112,27 +113,46 @@ func (r *RootCmd) openVSCode() *serpent.Command { if len(inv.Args) > 1 { directory = inv.Args[1] } - directory, err = resolveAgentAbsPath(workspaceAgent.ExpandedDirectory, directory, workspaceAgent.OperatingSystem, insideThisWorkspace) - if err != nil { - return xerrors.Errorf("resolve agent path: %w", err) - } - u := &url.URL{ - Scheme: "vscode", - Host: "coder.coder-remote", - Path: "/open", - } + if containerName != "" { + containers, err := client.WorkspaceAgentListContainers(ctx, workspaceAgent.ID, map[string]string{"devcontainer.local_folder": ""}) + if err != nil { + return xerrors.Errorf("list workspace agent containers: %w", err) + } - qp := url.Values{} + var foundContainer bool - qp.Add("url", client.URL.String()) - qp.Add("owner", workspace.OwnerName) - qp.Add("workspace", workspace.Name) - qp.Add("agent", workspaceAgent.Name) - if directory != "" { - qp.Add("folder", directory) + for _, container := range containers.Containers { + if container.FriendlyName == containerName { + foundContainer = true + + if directory == "" { + localFolder, ok := container.Labels["devcontainer.local_folder"] + if !ok { + return xerrors.New("container missing `devcontainer.local_folder` label") + } + + directory, ok = container.Volumes[localFolder] + if !ok { + return xerrors.New("container missing volume for `devcontainer.local_folder`") + } + } + + break + } + } + + if !foundContainer { + return xerrors.New("no container found") + } + } + + directory, err = resolveAgentAbsPath(workspaceAgent.ExpandedDirectory, directory, workspaceAgent.OperatingSystem, insideThisWorkspace) + if err != nil { + return xerrors.Errorf("resolve agent path: %w", err) } + var token string // We always set the token if we believe we can open without // printing the URI, otherwise the token must be explicitly // requested as it will be printed in plain text. @@ -145,10 +165,31 @@ func (r *RootCmd) openVSCode() *serpent.Command { if err != nil { return xerrors.Errorf("create API key: %w", err) } - qp.Add("token", apiKey.Key) + token = apiKey.Key } - u.RawQuery = qp.Encode() + var ( + u *url.URL + qp url.Values + ) + if containerName != "" { + u, qp = buildVSCodeWorkspaceDevContainerLink( + token, + client.URL.String(), + workspace, + workspaceAgent, + containerName, + directory, + ) + } else { + u, qp = buildVSCodeWorkspaceLink( + token, + client.URL.String(), + workspace, + workspaceAgent, + directory, + ) + } openingPath := workspaceName if directory != "" { @@ -204,6 +245,12 @@ func (r *RootCmd) openVSCode() *serpent.Command { ), Value: serpent.BoolOf(&generateToken), }, + { + Flag: "container", + FlagShorthand: "c", + Description: "Container name to connect to in the workspace.", + Value: serpent.StringOf(&containerName), + }, { Flag: "test.open-error", Description: "Don't run the open command.", @@ -344,6 +391,63 @@ func (r *RootCmd) openApp() *serpent.Command { return cmd } +func buildVSCodeWorkspaceLink( + token string, + clientURL string, + workspace codersdk.Workspace, + workspaceAgent codersdk.WorkspaceAgent, + directory string, +) (*url.URL, url.Values) { + qp := url.Values{} + qp.Add("url", clientURL) + qp.Add("owner", workspace.OwnerName) + qp.Add("workspace", workspace.Name) + qp.Add("agent", workspaceAgent.Name) + + if directory != "" { + qp.Add("folder", directory) + } + + if token != "" { + qp.Add("token", token) + } + + return &url.URL{ + Scheme: "vscode", + Host: "coder.coder-remote", + Path: "/open", + RawQuery: qp.Encode(), + }, qp +} + +func buildVSCodeWorkspaceDevContainerLink( + token string, + clientURL string, + workspace codersdk.Workspace, + workspaceAgent codersdk.WorkspaceAgent, + containerName string, + containerFolder string, +) (*url.URL, url.Values) { + qp := url.Values{} + qp.Add("url", clientURL) + qp.Add("owner", workspace.OwnerName) + qp.Add("workspace", workspace.Name) + qp.Add("agent", workspaceAgent.Name) + qp.Add("devContainerName", containerName) + qp.Add("devContainerFolder", containerFolder) + + if token != "" { + qp.Add("token", token) + } + + return &url.URL{ + Scheme: "vscode", + Host: "coder.coder-remote", + Path: "/openDevContainer", + RawQuery: qp.Encode(), + }, qp +} + // waitForAgentCond uses the watch workspace API to update the agent information // until the condition is met. func waitForAgentCond(ctx context.Context, client *codersdk.Client, workspace codersdk.Workspace, workspaceAgent codersdk.WorkspaceAgent, cond func(codersdk.WorkspaceAgent) bool) (codersdk.Workspace, codersdk.WorkspaceAgent, error) { diff --git a/cli/open_test.go b/cli/open_test.go index e36d20a59aaf4..f30caba92d7b7 100644 --- a/cli/open_test.go +++ b/cli/open_test.go @@ -8,12 +8,17 @@ import ( "strings" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "github.com/coder/coder/v2/agent" + "github.com/coder/coder/v2/agent/agentcontainers/acmock" "github.com/coder/coder/v2/agent/agenttest" "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/pty/ptytest" @@ -285,6 +290,340 @@ func TestOpenVSCode_NoAgentDirectory(t *testing.T) { } } +func TestOpenVSCodeDevContainer(t *testing.T) { + t.Parallel() + + agentName := "agent1" + agentDir, err := filepath.Abs(filepath.FromSlash("/tmp")) + require.NoError(t, err) + + containerName := testutil.GetRandomName(t) + containerFolder := "/workspace/coder" + + ctrl := gomock.NewController(t) + mcl := acmock.NewMockLister(ctrl) + mcl.EXPECT().List(gomock.Any()).Return( + codersdk.WorkspaceAgentListContainersResponse{ + Containers: []codersdk.WorkspaceAgentContainer{ + { + ID: uuid.NewString(), + CreatedAt: dbtime.Now(), + FriendlyName: containerName, + Image: "busybox:latest", + Labels: map[string]string{ + "devcontainer.local_folder": "/home/coder/coder", + }, + Running: true, + Status: "running", + Volumes: map[string]string{ + "/home/coder/coder": containerFolder, + }, + }, + }, + }, nil, + ) + + client, workspace, agentToken := setupWorkspaceForAgent(t, func(agents []*proto.Agent) []*proto.Agent { + agents[0].Directory = agentDir + agents[0].Name = agentName + agents[0].OperatingSystem = runtime.GOOS + return agents + }) + + _ = agenttest.New(t, client.URL, agentToken, func(o *agent.Options) { + o.ContainerLister = mcl + }) + _ = coderdtest.NewWorkspaceAgentWaiter(t, client, workspace.ID).Wait() + + insideWorkspaceEnv := map[string]string{ + "CODER": "true", + "CODER_WORKSPACE_NAME": workspace.Name, + "CODER_WORKSPACE_AGENT_NAME": agentName, + } + + wd, err := os.Getwd() + require.NoError(t, err) + + tests := []struct { + name string + env map[string]string + args []string + wantDir string + wantError bool + wantToken bool + }{ + { + name: "nonexistent container", + args: []string{"--test.open-error", workspace.Name, "--container", containerName + "bad"}, + wantError: true, + }, + { + name: "ok", + args: []string{"--test.open-error", workspace.Name, "--container", containerName}, + wantDir: containerFolder, + wantError: false, + }, + { + name: "ok with absolute path", + args: []string{"--test.open-error", workspace.Name, "--container", containerName, containerFolder}, + wantDir: containerFolder, + wantError: false, + }, + { + name: "ok with relative path", + args: []string{"--test.open-error", workspace.Name, "--container", containerName, "my/relative/path"}, + wantDir: filepath.Join(agentDir, filepath.FromSlash("my/relative/path")), + wantError: false, + }, + { + name: "ok with token", + args: []string{"--test.open-error", workspace.Name, "--container", containerName, "--generate-token"}, + wantDir: containerFolder, + wantError: false, + wantToken: true, + }, + // Inside workspace, does not require --test.open-error + { + name: "ok inside workspace", + env: insideWorkspaceEnv, + args: []string{workspace.Name, "--container", containerName}, + wantDir: containerFolder, + }, + { + name: "ok inside workspace relative path", + env: insideWorkspaceEnv, + args: []string{workspace.Name, "--container", containerName, "foo"}, + wantDir: filepath.Join(wd, "foo"), + }, + { + name: "ok inside workspace token", + env: insideWorkspaceEnv, + args: []string{workspace.Name, "--container", containerName, "--generate-token"}, + wantDir: containerFolder, + wantToken: true, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + inv, root := clitest.New(t, append([]string{"open", "vscode"}, tt.args...)...) + clitest.SetupConfig(t, client, root) + + pty := ptytest.New(t) + inv.Stdin = pty.Input() + inv.Stdout = pty.Output() + + ctx := testutil.Context(t, testutil.WaitLong) + inv = inv.WithContext(ctx) + + for k, v := range tt.env { + inv.Environ.Set(k, v) + } + + w := clitest.StartWithWaiter(t, inv) + + if tt.wantError { + w.RequireError() + return + } + + me, err := client.User(ctx, codersdk.Me) + require.NoError(t, err) + + line := pty.ReadLine(ctx) + u, err := url.ParseRequestURI(line) + require.NoError(t, err, "line: %q", line) + + qp := u.Query() + assert.Equal(t, client.URL.String(), qp.Get("url")) + assert.Equal(t, me.Username, qp.Get("owner")) + assert.Equal(t, workspace.Name, qp.Get("workspace")) + assert.Equal(t, agentName, qp.Get("agent")) + assert.Equal(t, containerName, qp.Get("devContainerName")) + + if tt.wantDir != "" { + assert.Equal(t, tt.wantDir, qp.Get("devContainerFolder")) + } else { + assert.Equal(t, containerFolder, qp.Get("devContainerFolder")) + } + + if tt.wantToken { + assert.NotEmpty(t, qp.Get("token")) + } else { + assert.Empty(t, qp.Get("token")) + } + + w.RequireSuccess() + }) + } +} + +func TestOpenVSCodeDevContainer_NoAgentDirectory(t *testing.T) { + t.Parallel() + + agentName := "agent1" + + containerName := testutil.GetRandomName(t) + containerFolder := "/workspace/coder" + + ctrl := gomock.NewController(t) + mcl := acmock.NewMockLister(ctrl) + mcl.EXPECT().List(gomock.Any()).Return( + codersdk.WorkspaceAgentListContainersResponse{ + Containers: []codersdk.WorkspaceAgentContainer{ + { + ID: uuid.NewString(), + CreatedAt: dbtime.Now(), + FriendlyName: containerName, + Image: "busybox:latest", + Labels: map[string]string{ + "devcontainer.local_folder": "/home/coder/coder", + }, + Running: true, + Status: "running", + Volumes: map[string]string{ + "/home/coder/coder": containerFolder, + }, + }, + }, + }, nil, + ) + + client, workspace, agentToken := setupWorkspaceForAgent(t, func(agents []*proto.Agent) []*proto.Agent { + agents[0].Name = agentName + agents[0].OperatingSystem = runtime.GOOS + return agents + }) + + _ = agenttest.New(t, client.URL, agentToken, func(o *agent.Options) { + o.ContainerLister = mcl + }) + _ = coderdtest.NewWorkspaceAgentWaiter(t, client, workspace.ID).Wait() + + insideWorkspaceEnv := map[string]string{ + "CODER": "true", + "CODER_WORKSPACE_NAME": workspace.Name, + "CODER_WORKSPACE_AGENT_NAME": agentName, + } + + wd, err := os.Getwd() + require.NoError(t, err) + + absPath := "/home/coder" + if runtime.GOOS == "windows" { + absPath = "C:\\home\\coder" + } + + tests := []struct { + name string + env map[string]string + args []string + wantDir string + wantError bool + wantToken bool + }{ + { + name: "ok", + args: []string{"--test.open-error", workspace.Name, "--container", containerName}, + }, + { + name: "no agent dir error relative path", + args: []string{"--test.open-error", workspace.Name, "--container", containerName, "my/relative/path"}, + wantDir: filepath.FromSlash("my/relative/path"), + wantError: true, + }, + { + name: "ok with absolute path", + args: []string{"--test.open-error", workspace.Name, "--container", containerName, absPath}, + wantDir: absPath, + }, + { + name: "ok with token", + args: []string{"--test.open-error", workspace.Name, "--container", containerName, "--generate-token"}, + wantToken: true, + }, + // Inside workspace, does not require --test.open-error + { + name: "ok inside workspace", + env: insideWorkspaceEnv, + args: []string{workspace.Name, "--container", containerName}, + }, + { + name: "ok inside workspace relative path", + env: insideWorkspaceEnv, + args: []string{workspace.Name, "--container", containerName, "foo"}, + wantDir: filepath.Join(wd, "foo"), + }, + { + name: "ok inside workspace token", + env: insideWorkspaceEnv, + args: []string{workspace.Name, "--container", containerName, "--generate-token"}, + wantToken: true, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + inv, root := clitest.New(t, append([]string{"open", "vscode"}, tt.args...)...) + clitest.SetupConfig(t, client, root) + + pty := ptytest.New(t) + inv.Stdin = pty.Input() + inv.Stdout = pty.Output() + + ctx := testutil.Context(t, testutil.WaitLong) + inv = inv.WithContext(ctx) + + for k, v := range tt.env { + inv.Environ.Set(k, v) + } + + w := clitest.StartWithWaiter(t, inv) + + if tt.wantError { + w.RequireError() + return + } + + me, err := client.User(ctx, codersdk.Me) + require.NoError(t, err) + + line := pty.ReadLine(ctx) + u, err := url.ParseRequestURI(line) + require.NoError(t, err, "line: %q", line) + + qp := u.Query() + assert.Equal(t, client.URL.String(), qp.Get("url")) + assert.Equal(t, me.Username, qp.Get("owner")) + assert.Equal(t, workspace.Name, qp.Get("workspace")) + assert.Equal(t, agentName, qp.Get("agent")) + assert.Equal(t, containerName, qp.Get("devContainerName")) + + if tt.wantDir != "" { + assert.Equal(t, tt.wantDir, qp.Get("devContainerFolder")) + } else { + assert.Equal(t, containerFolder, qp.Get("devContainerFolder")) + } + + if tt.wantToken { + assert.NotEmpty(t, qp.Get("token")) + } else { + assert.Empty(t, qp.Get("token")) + } + + w.RequireSuccess() + }) + } +} + func TestOpenApp(t *testing.T) { t.Parallel() From 3632b1bb43712d1a3147ebe238c6c261df916586 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 1 Apr 2025 15:23:38 +0100 Subject: [PATCH 2/9] chore: run 'make gen' --- cli/testdata/coder_open_vscode_--help.golden | 3 +++ docs/reference/cli/open_vscode.md | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/cli/testdata/coder_open_vscode_--help.golden b/cli/testdata/coder_open_vscode_--help.golden index e6e10ef8e31a1..d436402e4d800 100644 --- a/cli/testdata/coder_open_vscode_--help.golden +++ b/cli/testdata/coder_open_vscode_--help.golden @@ -6,6 +6,9 @@ USAGE: Open a workspace in VS Code Desktop OPTIONS: + -c, --container string + Container name to connect to in the workspace. + --generate-token bool, $CODER_OPEN_VSCODE_GENERATE_TOKEN Generate an auth token and include it in the vscode:// URI. This is for automagical configuration of VS Code Desktop and not needed if diff --git a/docs/reference/cli/open_vscode.md b/docs/reference/cli/open_vscode.md index 2b1e80dfbe5b7..cd57989e96512 100644 --- a/docs/reference/cli/open_vscode.md +++ b/docs/reference/cli/open_vscode.md @@ -19,3 +19,11 @@ coder open vscode [flags] [] | Environment | $CODER_OPEN_VSCODE_GENERATE_TOKEN | Generate an auth token and include it in the vscode:// URI. This is for automagical configuration of VS Code Desktop and not needed if already configured. This flag does not need to be specified when running this command on a local machine unless automatic open fails. + +### -c, --container + +| | | +|------|---------------------| +| Type | string | + +Container name to connect to in the workspace. From ad6ddcaa1055372013a31dd2f71232018beccec9 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 1 Apr 2025 15:43:07 +0100 Subject: [PATCH 3/9] chore: ensure we use unix path --- cli/open.go | 6 ++++++ cli/open_test.go | 9 ++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/cli/open.go b/cli/open.go index 496178420b0a2..40fa4518896c2 100644 --- a/cli/open.go +++ b/cli/open.go @@ -428,6 +428,8 @@ func buildVSCodeWorkspaceDevContainerLink( containerName string, containerFolder string, ) (*url.URL, url.Values) { + containerFolder = windowsToUnixPath(containerFolder) + qp := url.Values{} qp.Add("url", clientURL) qp.Add("owner", workspace.OwnerName) @@ -520,6 +522,10 @@ func unixToWindowsPath(p string) string { return strings.ReplaceAll(p, "/", "\\") } +func windowsToUnixPath(p string) string { + return strings.ReplaceAll(p, "\\", "/") +} + // resolveAgentAbsPath resolves the absolute path to a file or directory in the // workspace. If the path is relative, it will be resolved relative to the // workspace's expanded directory. If the path is absolute, it will be returned diff --git a/cli/open_test.go b/cli/open_test.go index f30caba92d7b7..48886fe5358e2 100644 --- a/cli/open_test.go +++ b/cli/open_test.go @@ -185,11 +185,6 @@ func TestOpenVSCode_NoAgentDirectory(t *testing.T) { wd, err := os.Getwd() require.NoError(t, err) - absPath := "/home/coder" - if runtime.GOOS == "windows" { - absPath = "C:\\home\\coder" - } - tests := []struct { name string args []string @@ -210,8 +205,8 @@ func TestOpenVSCode_NoAgentDirectory(t *testing.T) { }, { name: "ok with absolute path", - args: []string{"--test.open-error", workspace.Name, absPath}, - wantDir: absPath, + args: []string{"--test.open-error", workspace.Name, "/home/coder"}, + wantDir: "/home/coder", }, { name: "ok with token", From a1e95e80cbd7b9000c15ba98615804bb966c089e Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 1 Apr 2025 18:26:54 +0100 Subject: [PATCH 4/9] chore: fix oopsie on test --- cli/open_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cli/open_test.go b/cli/open_test.go index 48886fe5358e2..6cc7c569f979d 100644 --- a/cli/open_test.go +++ b/cli/open_test.go @@ -185,6 +185,11 @@ func TestOpenVSCode_NoAgentDirectory(t *testing.T) { wd, err := os.Getwd() require.NoError(t, err) + absPath := "/home/coder" + if runtime.GOOS == "windows" { + absPath = "C:\\home\\coder" + } + tests := []struct { name string args []string @@ -205,7 +210,7 @@ func TestOpenVSCode_NoAgentDirectory(t *testing.T) { }, { name: "ok with absolute path", - args: []string{"--test.open-error", workspace.Name, "/home/coder"}, + args: []string{"--test.open-error", workspace.Name, absPath}, wantDir: "/home/coder", }, { @@ -508,11 +513,6 @@ func TestOpenVSCodeDevContainer_NoAgentDirectory(t *testing.T) { wd, err := os.Getwd() require.NoError(t, err) - absPath := "/home/coder" - if runtime.GOOS == "windows" { - absPath = "C:\\home\\coder" - } - tests := []struct { name string env map[string]string @@ -533,8 +533,8 @@ func TestOpenVSCodeDevContainer_NoAgentDirectory(t *testing.T) { }, { name: "ok with absolute path", - args: []string{"--test.open-error", workspace.Name, "--container", containerName, absPath}, - wantDir: absPath, + args: []string{"--test.open-error", workspace.Name, "--container", containerName, "/home/coder"}, + wantDir: "/home/coder", }, { name: "ok with token", From f499bb192bb5842adc0a824d67cd4471811d8daf Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 1 Apr 2025 18:35:13 +0100 Subject: [PATCH 5/9] chore: muppet --- cli/open_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/open_test.go b/cli/open_test.go index 6cc7c569f979d..f3c9accb417e8 100644 --- a/cli/open_test.go +++ b/cli/open_test.go @@ -211,7 +211,7 @@ func TestOpenVSCode_NoAgentDirectory(t *testing.T) { { name: "ok with absolute path", args: []string{"--test.open-error", workspace.Name, absPath}, - wantDir: "/home/coder", + wantDir: absPath, }, { name: "ok with token", From 8abf8830c505a86acc8dad7ec5d401d6386665fb Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 1 Apr 2025 18:51:29 +0100 Subject: [PATCH 6/9] chore: filepath from slash --- cli/open.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/open.go b/cli/open.go index 40fa4518896c2..b6d769fd482af 100644 --- a/cli/open.go +++ b/cli/open.go @@ -136,6 +136,8 @@ func (r *RootCmd) openVSCode() *serpent.Command { if !ok { return xerrors.New("container missing volume for `devcontainer.local_folder`") } + + directory = filepath.FromSlash(directory) } break From 9cc49f825700844fa88779f46fb1237e34121872 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 1 Apr 2025 18:59:31 +0100 Subject: [PATCH 7/9] chore: run test on linux only --- cli/open.go | 2 -- cli/open_test.go | 8 ++++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cli/open.go b/cli/open.go index b6d769fd482af..40fa4518896c2 100644 --- a/cli/open.go +++ b/cli/open.go @@ -136,8 +136,6 @@ func (r *RootCmd) openVSCode() *serpent.Command { if !ok { return xerrors.New("container missing volume for `devcontainer.local_folder`") } - - directory = filepath.FromSlash(directory) } break diff --git a/cli/open_test.go b/cli/open_test.go index f3c9accb417e8..f0183022782d9 100644 --- a/cli/open_test.go +++ b/cli/open_test.go @@ -293,6 +293,10 @@ func TestOpenVSCode_NoAgentDirectory(t *testing.T) { func TestOpenVSCodeDevContainer(t *testing.T) { t.Parallel() + if runtime.GOOS != "linux" { + t.Skip("DevContainers are only supported for agents on Linux") + } + agentName := "agent1" agentDir, err := filepath.Abs(filepath.FromSlash("/tmp")) require.NoError(t, err) @@ -465,6 +469,10 @@ func TestOpenVSCodeDevContainer(t *testing.T) { func TestOpenVSCodeDevContainer_NoAgentDirectory(t *testing.T) { t.Parallel() + if runtime.GOOS != "linux" { + t.Skip("DevContainers are only supported for agents on Linux") + } + agentName := "agent1" containerName := testutil.GetRandomName(t) From 93bf7b7fb38cdac67ed753d625ace157161ea199 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 3 Apr 2025 09:40:38 +0100 Subject: [PATCH 8/9] chore: suggestions - Invert an if-condition to reduce how nested the code was. - Hide `-c` flag for now --- cli/open.go | 31 +++++++++++--------- cli/testdata/coder_open_vscode_--help.golden | 3 -- docs/reference/cli/open_vscode.md | 8 ----- 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/cli/open.go b/cli/open.go index 40fa4518896c2..e75d42ef38f64 100644 --- a/cli/open.go +++ b/cli/open.go @@ -123,23 +123,25 @@ func (r *RootCmd) openVSCode() *serpent.Command { var foundContainer bool for _, container := range containers.Containers { - if container.FriendlyName == containerName { - foundContainer = true - - if directory == "" { - localFolder, ok := container.Labels["devcontainer.local_folder"] - if !ok { - return xerrors.New("container missing `devcontainer.local_folder` label") - } - - directory, ok = container.Volumes[localFolder] - if !ok { - return xerrors.New("container missing volume for `devcontainer.local_folder`") - } + if container.FriendlyName != containerName { + continue + } + + foundContainer = true + + if directory == "" { + localFolder, ok := container.Labels["devcontainer.local_folder"] + if !ok { + return xerrors.New("container missing `devcontainer.local_folder` label") } - break + directory, ok = container.Volumes[localFolder] + if !ok { + return xerrors.New("container missing volume for `devcontainer.local_folder`") + } } + + break } if !foundContainer { @@ -250,6 +252,7 @@ func (r *RootCmd) openVSCode() *serpent.Command { FlagShorthand: "c", Description: "Container name to connect to in the workspace.", Value: serpent.StringOf(&containerName), + Hidden: true, // Hidden until this features is at least in beta. }, { Flag: "test.open-error", diff --git a/cli/testdata/coder_open_vscode_--help.golden b/cli/testdata/coder_open_vscode_--help.golden index d436402e4d800..e6e10ef8e31a1 100644 --- a/cli/testdata/coder_open_vscode_--help.golden +++ b/cli/testdata/coder_open_vscode_--help.golden @@ -6,9 +6,6 @@ USAGE: Open a workspace in VS Code Desktop OPTIONS: - -c, --container string - Container name to connect to in the workspace. - --generate-token bool, $CODER_OPEN_VSCODE_GENERATE_TOKEN Generate an auth token and include it in the vscode:// URI. This is for automagical configuration of VS Code Desktop and not needed if diff --git a/docs/reference/cli/open_vscode.md b/docs/reference/cli/open_vscode.md index cd57989e96512..2b1e80dfbe5b7 100644 --- a/docs/reference/cli/open_vscode.md +++ b/docs/reference/cli/open_vscode.md @@ -19,11 +19,3 @@ coder open vscode [flags] [] | Environment | $CODER_OPEN_VSCODE_GENERATE_TOKEN | Generate an auth token and include it in the vscode:// URI. This is for automagical configuration of VS Code Desktop and not needed if already configured. This flag does not need to be specified when running this command on a local machine unless automatic open fails. - -### -c, --container - -| | | -|------|---------------------| -| Type | string | - -Container name to connect to in the workspace. From e63db0c75f9478ba0b54a3ba381fb1aab80e3ee8 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 3 Apr 2025 10:08:17 +0100 Subject: [PATCH 9/9] chore: replace windowsToUnixPath with filepath.ToSlash --- cli/open.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cli/open.go b/cli/open.go index e75d42ef38f64..ff950b552a853 100644 --- a/cli/open.go +++ b/cli/open.go @@ -431,7 +431,7 @@ func buildVSCodeWorkspaceDevContainerLink( containerName string, containerFolder string, ) (*url.URL, url.Values) { - containerFolder = windowsToUnixPath(containerFolder) + containerFolder = filepath.ToSlash(containerFolder) qp := url.Values{} qp.Add("url", clientURL) @@ -525,10 +525,6 @@ func unixToWindowsPath(p string) string { return strings.ReplaceAll(p, "/", "\\") } -func windowsToUnixPath(p string) string { - return strings.ReplaceAll(p, "\\", "/") -} - // resolveAgentAbsPath resolves the absolute path to a file or directory in the // workspace. If the path is relative, it will be resolved relative to the // workspace's expanded directory. If the path is absolute, it will be returned pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy