feat(agent/agentcontainers): add Exec method to devcontainers CLI (#18244)

This change adds Exec to the devcontainer CLI.

Updates coder/internal#621
This commit is contained in:
Mathias Fredriksson
2025-06-06 14:38:58 +03:00
committed by GitHub
parent a12429e9f8
commit 709f374fe0
5 changed files with 324 additions and 57 deletions
+91 -14
View File
@@ -17,38 +17,83 @@ import (
// DevcontainerCLI is an interface for the devcontainer CLI.
type DevcontainerCLI interface {
Up(ctx context.Context, workspaceFolder, configPath string, opts ...DevcontainerCLIUpOptions) (id string, err error)
Exec(ctx context.Context, workspaceFolder, configPath string, cmd string, cmdArgs []string, opts ...DevcontainerCLIExecOptions) error
}
// DevcontainerCLIUpOptions are options for the devcontainer CLI up
// DevcontainerCLIUpOptions are options for the devcontainer CLI Up
// command.
type DevcontainerCLIUpOptions func(*devcontainerCLIUpConfig)
type devcontainerCLIUpConfig struct {
args []string // Additional arguments for the Up command.
stdout io.Writer
stderr io.Writer
}
// WithRemoveExistingContainer is an option to remove the existing
// container.
func WithRemoveExistingContainer() DevcontainerCLIUpOptions {
return func(o *devcontainerCLIUpConfig) {
o.removeExistingContainer = true
o.args = append(o.args, "--remove-existing-container")
}
}
// WithOutput sets stdout and stderr writers for Up command logs.
func WithOutput(stdout, stderr io.Writer) DevcontainerCLIUpOptions {
// WithUpOutput sets additional stdout and stderr writers for logs
// during Up operations.
func WithUpOutput(stdout, stderr io.Writer) DevcontainerCLIUpOptions {
return func(o *devcontainerCLIUpConfig) {
o.stdout = stdout
o.stderr = stderr
}
}
type devcontainerCLIUpConfig struct {
removeExistingContainer bool
stdout io.Writer
stderr io.Writer
// DevcontainerCLIExecOptions are options for the devcontainer CLI Exec
// command.
type DevcontainerCLIExecOptions func(*devcontainerCLIExecConfig)
type devcontainerCLIExecConfig struct {
args []string // Additional arguments for the Exec command.
stdout io.Writer
stderr io.Writer
}
// WithExecOutput sets additional stdout and stderr writers for logs
// during Exec operations.
func WithExecOutput(stdout, stderr io.Writer) DevcontainerCLIExecOptions {
return func(o *devcontainerCLIExecConfig) {
o.stdout = stdout
o.stderr = stderr
}
}
// WithContainerID sets the container ID to target a specific container.
func WithContainerID(id string) DevcontainerCLIExecOptions {
return func(o *devcontainerCLIExecConfig) {
o.args = append(o.args, "--container-id", id)
}
}
// WithRemoteEnv sets environment variables for the Exec command.
func WithRemoteEnv(env ...string) DevcontainerCLIExecOptions {
return func(o *devcontainerCLIExecConfig) {
for _, e := range env {
o.args = append(o.args, "--remote-env", e)
}
}
}
func applyDevcontainerCLIUpOptions(opts []DevcontainerCLIUpOptions) devcontainerCLIUpConfig {
conf := devcontainerCLIUpConfig{
removeExistingContainer: false,
conf := devcontainerCLIUpConfig{}
for _, opt := range opts {
if opt != nil {
opt(&conf)
}
}
return conf
}
func applyDevcontainerCLIExecOptions(opts []DevcontainerCLIExecOptions) devcontainerCLIExecConfig {
conf := devcontainerCLIExecConfig{}
for _, opt := range opts {
if opt != nil {
opt(&conf)
@@ -73,7 +118,7 @@ func NewDevcontainerCLI(logger slog.Logger, execer agentexec.Execer) Devcontaine
func (d *devcontainerCLI) Up(ctx context.Context, workspaceFolder, configPath string, opts ...DevcontainerCLIUpOptions) (string, error) {
conf := applyDevcontainerCLIUpOptions(opts)
logger := d.logger.With(slog.F("workspace_folder", workspaceFolder), slog.F("config_path", configPath), slog.F("recreate", conf.removeExistingContainer))
logger := d.logger.With(slog.F("workspace_folder", workspaceFolder), slog.F("config_path", configPath))
args := []string{
"up",
@@ -83,9 +128,7 @@ func (d *devcontainerCLI) Up(ctx context.Context, workspaceFolder, configPath st
if configPath != "" {
args = append(args, "--config", configPath)
}
if conf.removeExistingContainer {
args = append(args, "--remove-existing-container")
}
args = append(args, conf.args...)
cmd := d.execer.CommandContext(ctx, "devcontainer", args...)
// Capture stdout for parsing and stream logs for both default and provided writers.
@@ -117,6 +160,40 @@ func (d *devcontainerCLI) Up(ctx context.Context, workspaceFolder, configPath st
return result.ContainerID, nil
}
func (d *devcontainerCLI) Exec(ctx context.Context, workspaceFolder, configPath string, cmd string, cmdArgs []string, opts ...DevcontainerCLIExecOptions) error {
conf := applyDevcontainerCLIExecOptions(opts)
logger := d.logger.With(slog.F("workspace_folder", workspaceFolder), slog.F("config_path", configPath))
args := []string{"exec"}
if workspaceFolder != "" {
args = append(args, "--workspace-folder", workspaceFolder)
}
if configPath != "" {
args = append(args, "--config", configPath)
}
args = append(args, conf.args...)
args = append(args, cmd)
args = append(args, cmdArgs...)
c := d.execer.CommandContext(ctx, "devcontainer", args...)
stdoutWriters := []io.Writer{&devcontainerCLILogWriter{ctx: ctx, logger: logger.With(slog.F("stdout", true))}}
if conf.stdout != nil {
stdoutWriters = append(stdoutWriters, conf.stdout)
}
c.Stdout = io.MultiWriter(stdoutWriters...)
stderrWriters := []io.Writer{&devcontainerCLILogWriter{ctx: ctx, logger: logger.With(slog.F("stderr", true))}}
if conf.stderr != nil {
stderrWriters = append(stderrWriters, conf.stderr)
}
c.Stderr = io.MultiWriter(stderrWriters...)
if err := c.Run(); err != nil {
return xerrors.Errorf("devcontainer exec failed: %w", err)
}
return nil
}
// parseDevcontainerCLILastLine parses the last line of the devcontainer CLI output
// which is a JSON object.
func parseDevcontainerCLILastLine(ctx context.Context, logger slog.Logger, p []byte) (result devcontainerCLIResult, err error) {