Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
cli: replace open vscode container with devcontainer sub agent
  • Loading branch information
mafredri committed Jul 7, 2025
commit 6c3d31d2c0e45fce6fb2827095cdf94610ece216
119 changes: 73 additions & 46 deletions cli/open.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import (
"runtime"
"slices"
"strings"
"time"

"github.com/google/uuid"
"github.com/skratchdot/open-golang/open"
"golang.org/x/xerrors"

Expand Down Expand Up @@ -42,7 +44,6 @@ func (r *RootCmd) openVSCode() *serpent.Command {
generateToken bool
testOpenError bool
appearanceConfig codersdk.AppearanceConfig
containerName string
)

client := new(codersdk.Client)
Expand Down Expand Up @@ -79,6 +80,61 @@ func (r *RootCmd) openVSCode() *serpent.Command {
workspaceName := workspace.Name + "." + workspaceAgent.Name
insideThisWorkspace := insideAWorkspace && inWorkspaceName == workspaceName

var parentWorkspaceAgent codersdk.WorkspaceAgent
var devcontainer codersdk.WorkspaceAgentDevcontainer
if workspaceAgent.ParentID.Valid {
// This is likely a devcontainer agent, so we need to find the
// parent workspace agent as well as the devcontainer.
for _, otherAgent := range otherWorkspaceAgents {
if otherAgent.ID == workspaceAgent.ParentID.UUID {
parentWorkspaceAgent = otherAgent
break
}
}
if parentWorkspaceAgent.ID == uuid.Nil {
return xerrors.Errorf("parent workspace agent %s not found", workspaceAgent.ParentID.UUID)
}

printedWaiting := false
for {
resp, err := client.WorkspaceAgentListContainers(ctx, parentWorkspaceAgent.ID, nil)
if err != nil {
return xerrors.Errorf("list parent workspace agent containers: %w", err)
}

for _, dc := range resp.Devcontainers {
if dc.Agent.ID == workspaceAgent.ID {
devcontainer = dc
break
}
}
if devcontainer.ID == uuid.Nil {
cliui.Warnf(inv.Stderr, "Devcontainer for agent %q not found, opening as a regular workspace", workspaceAgent.Name)
parentWorkspaceAgent = codersdk.WorkspaceAgent{} // Reset to empty, so we don't use it later.
break
}

// Precondition, the devcontainer must be running to enter
// it. Once running, devcontainer.Container will be set.
if devcontainer.Status == codersdk.WorkspaceAgentDevcontainerStatusRunning {
break
}
if devcontainer.Status != codersdk.WorkspaceAgentDevcontainerStatusStarting {
return xerrors.Errorf("devcontainer %q is in unexpected status %q, expected %q or %q",
devcontainer.Name, devcontainer.Status,
codersdk.WorkspaceAgentDevcontainerStatusRunning,
codersdk.WorkspaceAgentDevcontainerStatusStarting,
)
}

if !printedWaiting {
_, _ = fmt.Fprintf(inv.Stderr, "Waiting for devcontainer %q status to change from %q to %q...\n", devcontainer.Name, devcontainer.Status, codersdk.WorkspaceAgentDevcontainerStatusRunning)
printedWaiting = true
}
time.Sleep(5 * time.Second) // Wait a bit before retrying.
}
}

if !insideThisWorkspace {
// Wait for the agent to connect, we don't care about readiness
// otherwise (e.g. wait).
Expand All @@ -99,6 +155,9 @@ func (r *RootCmd) openVSCode() *serpent.Command {
// the created state, so we need to wait for that to happen.
// However, if no directory is set, the expanded directory will
// not be set either.
//
// Note that this is irrelevant for devcontainer sub agents, as
// they always have a directory set.
if workspaceAgent.Directory != "" {
workspace, workspaceAgent, err = waitForAgentCond(ctx, client, workspace, workspaceAgent, func(_ codersdk.WorkspaceAgent) bool {
return workspaceAgent.LifecycleState != codersdk.WorkspaceAgentLifecycleCreated
Expand All @@ -114,41 +173,6 @@ func (r *RootCmd) openVSCode() *serpent.Command {
directory = inv.Args[1]
}

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)
}

var foundContainer bool

for _, container := range containers.Containers {
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")
}

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)
Expand All @@ -174,14 +198,16 @@ func (r *RootCmd) openVSCode() *serpent.Command {
u *url.URL
qp url.Values
)
if containerName != "" {
if devcontainer.ID != uuid.Nil {
u, qp = buildVSCodeWorkspaceDevContainerLink(
token,
client.URL.String(),
workspace,
workspaceAgent,
containerName,
parentWorkspaceAgent,
devcontainer.Container.FriendlyName,
directory,
devcontainer.WorkspaceFolder,
devcontainer.ConfigPath,
)
} else {
u, qp = buildVSCodeWorkspaceLink(
Expand Down Expand Up @@ -247,13 +273,6 @@ 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),
Hidden: true, // Hidden until this features is at least in beta.
},
{
Flag: "test.open-error",
Description: "Don't run the open command.",
Expand Down Expand Up @@ -430,8 +449,14 @@ func buildVSCodeWorkspaceDevContainerLink(
workspaceAgent codersdk.WorkspaceAgent,
containerName string,
containerFolder string,
localWorkspaceFolder string,
localConfigFile string,
) (*url.URL, url.Values) {
containerFolder = filepath.ToSlash(containerFolder)
localWorkspaceFolder = filepath.ToSlash(localWorkspaceFolder)
if localConfigFile != "" {
localConfigFile = filepath.ToSlash(localConfigFile)
}

qp := url.Values{}
qp.Add("url", clientURL)
Expand All @@ -440,6 +465,8 @@ func buildVSCodeWorkspaceDevContainerLink(
qp.Add("agent", workspaceAgent.Name)
qp.Add("devContainerName", containerName)
qp.Add("devContainerFolder", containerFolder)
qp.Add("localWorkspaceFolder", localWorkspaceFolder)
qp.Add("localConfigFile", localConfigFile)

if token != "" {
qp.Add("token", token)
Expand Down
Loading
Loading