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