Skip to content

Commit c29d517

Browse files
feat(cli): support opening devcontainers in vscode
1 parent fcac4ab commit c29d517

File tree

2 files changed

+461
-18
lines changed

2 files changed

+461
-18
lines changed

cli/open.go

+122-18
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,46 @@ 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+
foundContainer = true
128+
129+
if directory == "" {
130+
localFolder, ok := container.Labels["devcontainer.local_folder"]
131+
if !ok {
132+
return xerrors.New("container missing `devcontainer.local_folder` label")
133+
}
134+
135+
directory, ok = container.Volumes[localFolder]
136+
if !ok {
137+
return xerrors.New("container missing volume for `devcontainer.local_folder`")
138+
}
139+
}
140+
141+
break
142+
}
143+
}
144+
145+
if !foundContainer {
146+
return xerrors.New("no container found")
147+
}
148+
}
149+
150+
directory, err = resolveAgentAbsPath(workspaceAgent.ExpandedDirectory, directory, workspaceAgent.OperatingSystem, insideThisWorkspace)
151+
if err != nil {
152+
return xerrors.Errorf("resolve agent path: %w", err)
134153
}
135154

155+
var token string
136156
// We always set the token if we believe we can open without
137157
// printing the URI, otherwise the token must be explicitly
138158
// requested as it will be printed in plain text.
@@ -145,10 +165,31 @@ func (r *RootCmd) openVSCode() *serpent.Command {
145165
if err != nil {
146166
return xerrors.Errorf("create API key: %w", err)
147167
}
148-
qp.Add("token", apiKey.Key)
168+
token = apiKey.Key
149169
}
150170

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

153194
openingPath := workspaceName
154195
if directory != "" {
@@ -204,6 +245,12 @@ func (r *RootCmd) openVSCode() *serpent.Command {
204245
),
205246
Value: serpent.BoolOf(&generateToken),
206247
},
248+
{
249+
Flag: "container",
250+
FlagShorthand: "c",
251+
Description: "Container name to connect to in the workspace.",
252+
Value: serpent.StringOf(&containerName),
253+
},
207254
{
208255
Flag: "test.open-error",
209256
Description: "Don't run the open command.",
@@ -344,6 +391,63 @@ func (r *RootCmd) openApp() *serpent.Command {
344391
return cmd
345392
}
346393

394+
func buildVSCodeWorkspaceLink(
395+
token string,
396+
clientURL string,
397+
workspace codersdk.Workspace,
398+
workspaceAgent codersdk.WorkspaceAgent,
399+
directory string,
400+
) (*url.URL, url.Values) {
401+
qp := url.Values{}
402+
qp.Add("url", clientURL)
403+
qp.Add("owner", workspace.OwnerName)
404+
qp.Add("workspace", workspace.Name)
405+
qp.Add("agent", workspaceAgent.Name)
406+
407+
if directory != "" {
408+
qp.Add("folder", directory)
409+
}
410+
411+
if token != "" {
412+
qp.Add("token", token)
413+
}
414+
415+
return &url.URL{
416+
Scheme: "vscode",
417+
Host: "coder.coder-remote",
418+
Path: "/open",
419+
RawQuery: qp.Encode(),
420+
}, qp
421+
}
422+
423+
func buildVSCodeWorkspaceDevContainerLink(
424+
token string,
425+
clientURL string,
426+
workspace codersdk.Workspace,
427+
workspaceAgent codersdk.WorkspaceAgent,
428+
containerName string,
429+
containerFolder string,
430+
) (*url.URL, url.Values) {
431+
qp := url.Values{}
432+
qp.Add("url", clientURL)
433+
qp.Add("owner", workspace.OwnerName)
434+
qp.Add("workspace", workspace.Name)
435+
qp.Add("agent", workspaceAgent.Name)
436+
qp.Add("devContainerName", containerName)
437+
qp.Add("devContainerFolder", containerFolder)
438+
439+
if token != "" {
440+
qp.Add("token", token)
441+
}
442+
443+
return &url.URL{
444+
Scheme: "vscode",
445+
Host: "coder.coder-remote",
446+
Path: "/openDevContainer",
447+
RawQuery: qp.Encode(),
448+
}, qp
449+
}
450+
347451
// waitForAgentCond uses the watch workspace API to update the agent information
348452
// until the condition is met.
349453
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)