Skip to content

feat(agent): add devcontainer autostart support #17076

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Mar 27, 2025
Merged
Prev Previous commit
Next Next commit
refactor
  • Loading branch information
mafredri committed Mar 25, 2025
commit d85e9a2e47211b6fcd93bff9be587f16892ecbaa
48 changes: 8 additions & 40 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -1116,38 +1116,19 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
}

var (
// Clone the scripts so we can remove the devcontainer scripts.
scripts = slices.Clone(manifest.Scripts)
scripts = manifest.Scripts
scriptRunnerOpts []agentscripts.InitOption
)
if a.experimentalDevcontainersEnabled {
var dcScripts []codersdk.WorkspaceAgentScript
scripts, dcScripts = agentcontainers.ExtractDevcontainerScripts(a.logger, expandPathToAbs, manifest.Devcontainers, scripts)
// The post-start scripts are used to autostart Dev Containers
// after the start scripts have completed. This is necessary
// because the Dev Container may depend on the workspace being
// initialized (git clone, etc).
postStartScripts []codersdk.WorkspaceAgentScript
)
if a.experimentalDevcontainersEnabled {
for _, dc := range manifest.Devcontainers {
// TODO(mafredri): Verify `@devcontainers/cli` presence.
// TODO(mafredri): Verify workspace folder exists.
// TODO(mafredri): If set, verify config path exists.
dc = expandDevcontainerPaths(a.logger, dc)

for i, s := range scripts {
// The devcontainer scripts match the devcontainer ID for
// identification.
if s.ID == dc.ID {
scripts = slices.Delete(scripts, i, i+1)
postStartScripts = append(postStartScripts, agentcontainers.DevcontainerStartupScript(dc, s))
break
}
}
}
scriptRunnerOpts = append(scriptRunnerOpts, agentscripts.WithPostStartScripts(dcScripts...))
}

err = a.scriptRunner.Init(
manifest.Scripts,
aAPI.ScriptCompleted,
agentscripts.WithPostStartScripts(postStartScripts...),
)
err = a.scriptRunner.Init(scripts, aAPI.ScriptCompleted, scriptRunnerOpts...)
if err != nil {
return xerrors.Errorf("init script runner: %w", err)
}
Expand Down Expand Up @@ -1911,19 +1892,6 @@ func expandPathToAbs(path string) (string, error) {
return path, nil
}

func expandDevcontainerPaths(logger slog.Logger, dc codersdk.WorkspaceAgentDevcontainer) codersdk.WorkspaceAgentDevcontainer {
var err error
if dc.WorkspaceFolder, err = expandPathToAbs(dc.WorkspaceFolder); err != nil {
logger.Warn(context.Background(), "expand devcontainer workspace folder failed", slog.Error(err))
}
if dc.ConfigPath != "" {
if dc.ConfigPath, err = expandPathToAbs(dc.ConfigPath); err != nil {
logger.Warn(context.Background(), "expand devcontainer config path failed", slog.Error(err))
}
}
return dc
}

// EnvAgentSubsystem is the environment variable used to denote the
// specialized environment in which the agent is running
// (e.g. envbox, envbuilder).
Expand Down
38 changes: 38 additions & 0 deletions agent/agentcontainers/devcontainer.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package agentcontainers

import (
"context"
"fmt"

"cdr.dev/slog"

"github.com/coder/coder/v2/codersdk"
)

Expand All @@ -15,3 +18,38 @@ func DevcontainerStartupScript(dc codersdk.WorkspaceAgentDevcontainer, script co
script.Script = cmd
return script
}

func ExtractDevcontainerScripts(
logger slog.Logger,
expandPath func(string) (string, error),
devcontainers []codersdk.WorkspaceAgentDevcontainer,
scripts []codersdk.WorkspaceAgentScript,
) (other []codersdk.WorkspaceAgentScript, devcontainerScripts []codersdk.WorkspaceAgentScript) {
for _, dc := range devcontainers {
dc = expandDevcontainerPaths(logger, expandPath, dc)
for _, script := range scripts {
// The devcontainer scripts match the devcontainer ID for
// identification.
if script.ID == dc.ID {
devcontainerScripts = append(devcontainerScripts, DevcontainerStartupScript(dc, script))
} else {
other = append(other, script)
}
}
}

return other, devcontainerScripts
}

func expandDevcontainerPaths(logger slog.Logger, expandPath func(string) (string, error), dc codersdk.WorkspaceAgentDevcontainer) codersdk.WorkspaceAgentDevcontainer {
var err error
if dc.WorkspaceFolder, err = expandPath(dc.WorkspaceFolder); err != nil {
logger.Warn(context.Background(), "expand devcontainer workspace folder failed", slog.Error(err))
}
if dc.ConfigPath != "" {
if dc.ConfigPath, err = expandPath(dc.ConfigPath); err != nil {
logger.Warn(context.Background(), "expand devcontainer config path failed", slog.Error(err))
}
}
return dc
}
Loading