mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(cli): replace open vscode container with devcontainer subagent (#18765)
This change allows a devcontainer to be opened via the agent syntax, `coder open vscode <workspace>.<agent>` and removes the `--container` option to simplify the subcommand. Accessing the subagent will behave similarly to how the `--container` option behaved. Fixes coder/internal#748
This commit is contained in:
+85
-49
@@ -11,7 +11,9 @@ import (
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
@@ -42,7 +44,6 @@ func (r *RootCmd) openVSCode() *serpent.Command {
|
||||
generateToken bool
|
||||
testOpenError bool
|
||||
appearanceConfig codersdk.AppearanceConfig
|
||||
containerName string
|
||||
)
|
||||
|
||||
client := new(codersdk.Client)
|
||||
@@ -71,7 +72,7 @@ func (r *RootCmd) openVSCode() *serpent.Command {
|
||||
// need to wait for the agent to start.
|
||||
workspaceQuery := inv.Args[0]
|
||||
autostart := true
|
||||
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, autostart, workspaceQuery)
|
||||
workspace, workspaceAgent, otherWorkspaceAgents, err := getWorkspaceAndAgent(ctx, inv, client, autostart, workspaceQuery)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get workspace and agent: %w", err)
|
||||
}
|
||||
@@ -79,6 +80,70 @@ func (r *RootCmd) openVSCode() *serpent.Command {
|
||||
workspaceName := workspace.Name + "." + workspaceAgent.Name
|
||||
insideThisWorkspace := insideAWorkspace && inWorkspaceName == workspaceName
|
||||
|
||||
// To properly work with devcontainers, VS Code has to connect to
|
||||
// parent workspace agent. It will then proceed to enter the
|
||||
// container given the correct parameters. There is inherently no
|
||||
// dependency on the devcontainer agent in this scenario, but
|
||||
// relying on it simplifies the logic and ensures the devcontainer
|
||||
// is ready. To eliminate the dependency we would need to know that
|
||||
// a sub-agent that hasn't been created yet may be a devcontainer,
|
||||
// and thus will be created at a later time as well as expose the
|
||||
// container folder on the API response.
|
||||
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 %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).
|
||||
@@ -99,6 +164,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
|
||||
@@ -114,41 +182,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)
|
||||
@@ -174,14 +207,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(
|
||||
@@ -247,13 +282,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.",
|
||||
@@ -288,7 +316,7 @@ func (r *RootCmd) openApp() *serpent.Command {
|
||||
}
|
||||
|
||||
workspaceName := inv.Args[0]
|
||||
ws, agt, err := getWorkspaceAndAgent(ctx, inv, client, false, workspaceName)
|
||||
ws, agt, _, err := getWorkspaceAndAgent(ctx, inv, client, false, workspaceName)
|
||||
if err != nil {
|
||||
var sdkErr *codersdk.Error
|
||||
if errors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusNotFound {
|
||||
@@ -430,8 +458,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)
|
||||
@@ -440,6 +474,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)
|
||||
@@ -469,7 +505,7 @@ func waitForAgentCond(ctx context.Context, client *codersdk.Client, workspace co
|
||||
}
|
||||
|
||||
for workspace = range wc {
|
||||
workspaceAgent, err = getWorkspaceAgent(workspace, workspaceAgent.Name)
|
||||
workspaceAgent, _, err = getWorkspaceAgent(workspace, workspaceAgent.Name)
|
||||
if err != nil {
|
||||
return workspace, workspaceAgent, xerrors.Errorf("get workspace agent: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user