@@ -11,7 +11,9 @@ import (
1111"runtime"
1212"slices"
1313"strings"
14+ "time"
1415
16+ "github.com/google/uuid"
1517"github.com/skratchdot/open-golang/open"
1618"golang.org/x/xerrors"
1719
@@ -42,7 +44,6 @@ func (r *RootCmd) openVSCode() *serpent.Command {
4244generateToken bool
4345testOpenError bool
4446appearanceConfig codersdk.AppearanceConfig
45- containerName string
4647)
4748
4849client := new (codersdk.Client )
@@ -71,14 +72,78 @@ func (r *RootCmd) openVSCode() *serpent.Command {
7172// need to wait for the agent to start.
7273workspaceQuery := inv .Args [0 ]
7374autostart := true
74- workspace ,workspaceAgent ,err := getWorkspaceAndAgent (ctx ,inv ,client ,autostart ,workspaceQuery )
75+ workspace ,workspaceAgent ,otherWorkspaceAgents , err := getWorkspaceAndAgent (ctx ,inv ,client ,autostart ,workspaceQuery )
7576if err != nil {
7677return xerrors .Errorf ("get workspace and agent: %w" ,err )
7778}
7879
7980workspaceName := workspace .Name + "." + workspaceAgent .Name
8081insideThisWorkspace := insideAWorkspace && inWorkspaceName == workspaceName
8182
83+ // To properly work with devcontainers, VS Code has to connect to
84+ // parent workspace agent. It will then proceed to enter the
85+ // container given the correct parameters. There is inherently no
86+ // dependency on the devcontainer agent in this scenario, but
87+ // relying on it simplifies the logic and ensures the devcontainer
88+ // is ready. To eliminate the dependency we would need to know that
89+ // a sub-agent that hasn't been created yet may be a devcontainer,
90+ // and thus will be created at a later time as well as expose the
91+ // container folder on the API response.
92+ var parentWorkspaceAgent codersdk.WorkspaceAgent
93+ var devcontainer codersdk.WorkspaceAgentDevcontainer
94+ if workspaceAgent .ParentID .Valid {
95+ // This is likely a devcontainer agent, so we need to find the
96+ // parent workspace agent as well as the devcontainer.
97+ for _ ,otherAgent := range otherWorkspaceAgents {
98+ if otherAgent .ID == workspaceAgent .ParentID .UUID {
99+ parentWorkspaceAgent = otherAgent
100+ break
101+ }
102+ }
103+ if parentWorkspaceAgent .ID == uuid .Nil {
104+ return xerrors .Errorf ("parent workspace agent %s not found" ,workspaceAgent .ParentID .UUID )
105+ }
106+
107+ printedWaiting := false
108+ for {
109+ resp ,err := client .WorkspaceAgentListContainers (ctx ,parentWorkspaceAgent .ID ,nil )
110+ if err != nil {
111+ return xerrors .Errorf ("list parent workspace agent containers: %w" ,err )
112+ }
113+
114+ for _ ,dc := range resp .Devcontainers {
115+ if dc .Agent .ID == workspaceAgent .ID {
116+ devcontainer = dc
117+ break
118+ }
119+ }
120+ if devcontainer .ID == uuid .Nil {
121+ cliui .Warnf (inv .Stderr ,"Devcontainer %q not found, opening as a regular workspace..." ,workspaceAgent .Name )
122+ parentWorkspaceAgent = codersdk.WorkspaceAgent {}// Reset to empty, so we don't use it later.
123+ break
124+ }
125+
126+ // Precondition, the devcontainer must be running to enter
127+ // it. Once running, devcontainer.Container will be set.
128+ if devcontainer .Status == codersdk .WorkspaceAgentDevcontainerStatusRunning {
129+ break
130+ }
131+ if devcontainer .Status != codersdk .WorkspaceAgentDevcontainerStatusStarting {
132+ return xerrors .Errorf ("devcontainer %q is in unexpected status %q, expected %q or %q" ,
133+ devcontainer .Name ,devcontainer .Status ,
134+ codersdk .WorkspaceAgentDevcontainerStatusRunning ,
135+ codersdk .WorkspaceAgentDevcontainerStatusStarting ,
136+ )
137+ }
138+
139+ if ! printedWaiting {
140+ _ ,_ = fmt .Fprintf (inv .Stderr ,"Waiting for devcontainer %q status to change from %q to %q...\n " ,devcontainer .Name ,devcontainer .Status ,codersdk .WorkspaceAgentDevcontainerStatusRunning )
141+ printedWaiting = true
142+ }
143+ time .Sleep (5 * time .Second )// Wait a bit before retrying.
144+ }
145+ }
146+
82147if ! insideThisWorkspace {
83148// Wait for the agent to connect, we don't care about readiness
84149// otherwise (e.g. wait).
@@ -99,6 +164,9 @@ func (r *RootCmd) openVSCode() *serpent.Command {
99164// the created state, so we need to wait for that to happen.
100165// However, if no directory is set, the expanded directory will
101166// not be set either.
167+ //
168+ // Note that this is irrelevant for devcontainer sub agents, as
169+ // they always have a directory set.
102170if workspaceAgent .Directory != "" {
103171workspace ,workspaceAgent ,err = waitForAgentCond (ctx ,client ,workspace ,workspaceAgent ,func (_ codersdk.WorkspaceAgent )bool {
104172return workspaceAgent .LifecycleState != codersdk .WorkspaceAgentLifecycleCreated
@@ -114,41 +182,6 @@ func (r *RootCmd) openVSCode() *serpent.Command {
114182directory = inv .Args [1 ]
115183}
116184
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- }
122-
123- var foundContainer bool
124-
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-
152185directory ,err = resolveAgentAbsPath (workspaceAgent .ExpandedDirectory ,directory ,workspaceAgent .OperatingSystem ,insideThisWorkspace )
153186if err != nil {
154187return xerrors .Errorf ("resolve agent path: %w" ,err )
@@ -174,14 +207,16 @@ func (r *RootCmd) openVSCode() *serpent.Command {
174207u * url.URL
175208qp url.Values
176209)
177- if containerName != "" {
210+ if devcontainer . ID != uuid . Nil {
178211u ,qp = buildVSCodeWorkspaceDevContainerLink (
179212token ,
180213client .URL .String (),
181214workspace ,
182- workspaceAgent ,
183- containerName ,
215+ parentWorkspaceAgent ,
216+ devcontainer . Container . FriendlyName ,
184217directory ,
218+ devcontainer .WorkspaceFolder ,
219+ devcontainer .ConfigPath ,
185220)
186221}else {
187222u ,qp = buildVSCodeWorkspaceLink (
@@ -247,13 +282,6 @@ func (r *RootCmd) openVSCode() *serpent.Command {
247282),
248283Value :serpent .BoolOf (& generateToken ),
249284},
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- },
257285{
258286Flag :"test.open-error" ,
259287Description :"Don't run the open command." ,
@@ -288,7 +316,7 @@ func (r *RootCmd) openApp() *serpent.Command {
288316}
289317
290318workspaceName := inv .Args [0 ]
291- ws ,agt ,err := getWorkspaceAndAgent (ctx ,inv ,client ,false ,workspaceName )
319+ ws ,agt ,_ , err := getWorkspaceAndAgent (ctx ,inv ,client ,false ,workspaceName )
292320if err != nil {
293321var sdkErr * codersdk.Error
294322if errors .As (err ,& sdkErr )&& sdkErr .StatusCode ()== http .StatusNotFound {
@@ -430,8 +458,14 @@ func buildVSCodeWorkspaceDevContainerLink(
430458workspaceAgent codersdk.WorkspaceAgent ,
431459containerName string ,
432460containerFolder string ,
461+ localWorkspaceFolder string ,
462+ localConfigFile string ,
433463) (* url.URL , url.Values ) {
434464containerFolder = filepath .ToSlash (containerFolder )
465+ localWorkspaceFolder = filepath .ToSlash (localWorkspaceFolder )
466+ if localConfigFile != "" {
467+ localConfigFile = filepath .ToSlash (localConfigFile )
468+ }
435469
436470qp := url.Values {}
437471qp .Add ("url" ,clientURL )
@@ -440,6 +474,8 @@ func buildVSCodeWorkspaceDevContainerLink(
440474qp .Add ("agent" ,workspaceAgent .Name )
441475qp .Add ("devContainerName" ,containerName )
442476qp .Add ("devContainerFolder" ,containerFolder )
477+ qp .Add ("localWorkspaceFolder" ,localWorkspaceFolder )
478+ qp .Add ("localConfigFile" ,localConfigFile )
443479
444480if token != "" {
445481qp .Add ("token" ,token )
@@ -469,7 +505,7 @@ func waitForAgentCond(ctx context.Context, client *codersdk.Client, workspace co
469505}
470506
471507for workspace = range wc {
472- workspaceAgent ,err = getWorkspaceAgent (workspace ,workspaceAgent .Name )
508+ workspaceAgent ,_ , err = getWorkspaceAgent (workspace ,workspaceAgent .Name )
473509if err != nil {
474510return workspace ,workspaceAgent ,xerrors .Errorf ("get workspace agent: %w" ,err )
475511}