Skip to content

Commit aa3d71d

Browse files
feat(cli): support opening devcontainers in vscode (#17189)
Closes #16427 Adds the option `-c,--container` to `open vscode` that allows opening VSCode into a running devcontainer.
1 parent 99c6f23 commit aa3d71d

File tree

2 files changed

+469
-18
lines changed

2 files changed

+469
-18
lines changed

cli/open.go

Lines changed: 127 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func (r *RootCmd) openVSCode() *serpent.Command {
4242
generateToken bool
4343
testOpenError bool
4444
appearanceConfig codersdk.AppearanceConfig
45+
containerName string
4546
)
4647

4748
client := new(codersdk.Client)
@@ -112,27 +113,48 @@ func (r *RootCmd) openVSCode() *serpent.Command {
112113
if len(inv.Args) > 1 {
113114
directory = inv.Args[1]
114115
}
115-
directory, err = resolveAgentAbsPath(workspaceAgent.ExpandedDirectory, directory, workspaceAgent.OperatingSystem, insideThisWorkspace)
116-
if err != nil {
117-
return xerrors.Errorf("resolve agent path: %w", err)
118-
}
119116

120-
u := &url.URL{
121-
Scheme: "vscode",
122-
Host: "coder.coder-remote",
123-
Path: "/open",
124-
}
117+
if containerName != "" {
118+
containers, err := client.WorkspaceAgentListContainers(ctx, workspaceAgent.ID, map[string]string{"devcontainer.local_folder": ""})
119+
if err != nil {
120+
return xerrors.Errorf("list workspace agent containers: %w", err)
121+
}
125122

126-
qp := url.Values{}
123+
var foundContainer bool
127124

128-
qp.Add("url", client.URL.String())
129-
qp.Add("owner", workspace.OwnerName)
130-
qp.Add("workspace", workspace.Name)
131-
qp.Add("agent", workspaceAgent.Name)
132-
if directory != "" {
133-
qp.Add("folder", directory)
125+
for _, container := range containers.Containers {
126+
if container.FriendlyName != containerName {
127+
continue
128+
}
129+
130+
foundContainer = true
131+
132+
if directory == "" {
133+
localFolder, ok := container.Labels["devcontainer.local_folder"]
134+
if !ok {
135+
return xerrors.New("container missing `devcontainer.local_folder` label")
136+
}
137+
138+
directory, ok = container.Volumes[localFolder]
139+
if !ok {
140+
return xerrors.New("container missing volume for `devcontainer.local_folder`")
141+
}
142+
}
143+
144+
break
145+
}
146+
147+
if !foundContainer {
148+
return xerrors.New("no container found")
149+
}
150+
}
151+
152+
directory, err = resolveAgentAbsPath(workspaceAgent.ExpandedDirectory, directory, workspaceAgent.OperatingSystem, insideThisWorkspace)
153+
if err != nil {
154+
return xerrors.Errorf("resolve agent path: %w", err)
134155
}
135156

157+
var token string
136158
// We always set the token if we believe we can open without
137159
// printing the URI, otherwise the token must be explicitly
138160
// requested as it will be printed in plain text.
@@ -145,10 +167,31 @@ func (r *RootCmd) openVSCode() *serpent.Command {
145167
if err != nil {
146168
return xerrors.Errorf("create API key: %w", err)
147169
}
148-
qp.Add("token", apiKey.Key)
170+
token = apiKey.Key
149171
}
150172

151-
u.RawQuery = qp.Encode()
173+
var (
174+
u *url.URL
175+
qp url.Values
176+
)
177+
if containerName != "" {
178+
u, qp = buildVSCodeWorkspaceDevContainerLink(
179+
token,
180+
client.URL.String(),
181+
workspace,
182+
workspaceAgent,
183+
containerName,
184+
directory,
185+
)
186+
} else {
187+
u, qp = buildVSCodeWorkspaceLink(
188+
token,
189+
client.URL.String(),
190+
workspace,
191+
workspaceAgent,
192+
directory,
193+
)
194+
}
152195

153196
openingPath := workspaceName
154197
if directory != "" {
@@ -204,6 +247,13 @@ func (r *RootCmd) openVSCode() *serpent.Command {
204247
),
205248
Value: serpent.BoolOf(&generateToken),
206249
},
250+
{
251+
Flag: "container",
252+
FlagShorthand: "c",
253+
Description: "Container name to connect to in the workspace.",
254+
Value: serpent.StringOf(&containerName),
255+
Hidden: true, // Hidden until this features is at least in beta.
256+
},
207257
{
208258
Flag: "test.open-error",
209259
Description: "Don't run the open command.",
@@ -344,6 +394,65 @@ func (r *RootCmd) openApp() *serpent.Command {
344394
return cmd
345395
}
346396

397+
func buildVSCodeWorkspaceLink(
398+
token string,
399+
clientURL string,
400+
workspace codersdk.Workspace,
401+
workspaceAgent codersdk.WorkspaceAgent,
402+
directory string,
403+
) (*url.URL, url.Values) {
404+
qp := url.Values{}
405+
qp.Add("url", clientURL)
406+
qp.Add("owner", workspace.OwnerName)
407+
qp.Add("workspace", workspace.Name)
408+
qp.Add("agent", workspaceAgent.Name)
409+
410+
if directory != "" {
411+
qp.Add("folder", directory)
412+
}
413+
414+
if token != "" {
415+
qp.Add("token", token)
416+
}
417+
418+
return &url.URL{
419+
Scheme: "vscode",
420+
Host: "coder.coder-remote",
421+
Path: "/open",
422+
RawQuery: qp.Encode(),
423+
}, qp
424+
}
425+
426+
func buildVSCodeWorkspaceDevContainerLink(
427+
token string,
428+
clientURL string,
429+
workspace codersdk.Workspace,
430+
workspaceAgent codersdk.WorkspaceAgent,
431+
containerName string,
432+
containerFolder string,
433+
) (*url.URL, url.Values) {
434+
containerFolder = filepath.ToSlash(containerFolder)
435+
436+
qp := url.Values{}
437+
qp.Add("url", clientURL)
438+
qp.Add("owner", workspace.OwnerName)
439+
qp.Add("workspace", workspace.Name)
440+
qp.Add("agent", workspaceAgent.Name)
441+
qp.Add("devContainerName", containerName)
442+
qp.Add("devContainerFolder", containerFolder)
443+
444+
if token != "" {
445+
qp.Add("token", token)
446+
}
447+
448+
return &url.URL{
449+
Scheme: "vscode",
450+
Host: "coder.coder-remote",
451+
Path: "/openDevContainer",
452+
RawQuery: qp.Encode(),
453+
}, qp
454+
}
455+
347456
// waitForAgentCond uses the watch workspace API to update the agent information
348457
// until the condition is met.
349458
func waitForAgentCond(ctx context.Context, client *codersdk.Client, workspace codersdk.Workspace, workspaceAgent codersdk.WorkspaceAgent, cond func(codersdk.WorkspaceAgent) bool) (codersdk.Workspace, codersdk.WorkspaceAgent, error) {

0 commit comments

Comments
 (0)
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