@@ -16,37 +16,67 @@ import (
16
16
17
17
// DevcontainerCLI is an interface for the devcontainer CLI.
18
18
type DevcontainerCLI interface {
19
- Up (ctx context.Context ,workspaceFolder ,configPath string ,opts ... DevcontainerCLIUpOptions ) (id string ,err error )
19
+ Up (ctx context.Context ,workspaceFolder ,configPath string ,opts ... DevcontainerCLIOptions ) (id string ,err error )
20
+ Exec (ctx context.Context ,workspaceFolder ,configPath string ,cmd string ,cmdArgs []string ,opts ... DevcontainerCLIOptions )error
20
21
}
21
22
22
- // DevcontainerCLIUpOptions are options for the devcontainer CLI up
23
- // command.
24
- type DevcontainerCLIUpOptions func (* devcontainerCLIUpConfig )
23
+ // DevcontainerCLIOptions are options for the devcontainer CLI commands.
24
+ type DevcontainerCLIOptions func (* devcontainerCLIUpConfig )
25
25
26
26
// WithRemoveExistingContainer is an option to remove the existing
27
- // container.
28
- func WithRemoveExistingContainer ()DevcontainerCLIUpOptions {
27
+ // container. Can only be used with the Up command.
28
+ func WithRemoveExistingContainer ()DevcontainerCLIOptions {
29
29
return func (o * devcontainerCLIUpConfig ) {
30
- o .removeExistingContainer = true
30
+ if o .command != "up" {
31
+ panic ("developer error: WithRemoveExistingContainer can only be used with the Up command" )
32
+ }
33
+ o .args = append (o .args ,"--remove-existing-container" )
31
34
}
32
35
}
33
36
34
- // WithOutput sets stdout and stderr writers for Up command logs.
35
- func WithOutput (stdout ,stderr io.Writer )DevcontainerCLIUpOptions {
37
+ // WithOutput setsadditional stdout and stderr writers for logs.
38
+ func WithOutput (stdout ,stderr io.Writer )DevcontainerCLIOptions {
36
39
return func (o * devcontainerCLIUpConfig ) {
37
40
o .stdout = stdout
38
41
o .stderr = stderr
39
42
}
40
43
}
41
44
45
+ // WithContainerID sets the container ID to target a specific container.
46
+ // Can only be used with the Exec command.
47
+ func WithContainerID (id string )DevcontainerCLIOptions {
48
+ return func (o * devcontainerCLIUpConfig ) {
49
+ if o .command != "exec" {
50
+ panic ("developer error: WithContainerID can only be used with the Exec command" )
51
+ }
52
+ o .args = append (o .args ,"--container-id" ,id )
53
+ }
54
+ }
55
+
56
+ // WithRemoteEnv sets environment variables for the Exec command.
57
+ // Can only be used with the Exec command.
58
+ func WithRemoteEnv (env ... string )DevcontainerCLIOptions {
59
+ return func (o * devcontainerCLIUpConfig ) {
60
+ if o .command != "exec" {
61
+ panic ("developer error: WithRemoteEnv can only be used with the Exec command" )
62
+ }
63
+ for _ ,e := range env {
64
+ o .args = append (o .args ,"--remote-env" ,e )
65
+ }
66
+ }
67
+ }
68
+
42
69
type devcontainerCLIUpConfig struct {
70
+ command string // The devcontainer CLI command to run, e.g. "up", "exec".
43
71
removeExistingContainer bool
72
+ args []string // Additional arguments for the command.
44
73
stdout io.Writer
45
74
stderr io.Writer
46
75
}
47
76
48
- func applyDevcontainerCLIUpOptions ( opts []DevcontainerCLIUpOptions )devcontainerCLIUpConfig {
77
+ func applyDevcontainerCLIOptions ( command string , opts []DevcontainerCLIOptions )devcontainerCLIUpConfig {
49
78
conf := devcontainerCLIUpConfig {
79
+ command :command ,
50
80
removeExistingContainer :false ,
51
81
}
52
82
for _ ,opt := range opts {
@@ -71,8 +101,8 @@ func NewDevcontainerCLI(logger slog.Logger, execer agentexec.Execer) Devcontaine
71
101
}
72
102
}
73
103
74
- func (d * devcontainerCLI )Up (ctx context.Context ,workspaceFolder ,configPath string ,opts ... DevcontainerCLIUpOptions ) (string ,error ) {
75
- conf := applyDevcontainerCLIUpOptions ( opts )
104
+ func (d * devcontainerCLI )Up (ctx context.Context ,workspaceFolder ,configPath string ,opts ... DevcontainerCLIOptions ) (string ,error ) {
105
+ conf := applyDevcontainerCLIOptions ( "up" , opts )
76
106
logger := d .logger .With (slog .F ("workspace_folder" ,workspaceFolder ),slog .F ("config_path" ,configPath ),slog .F ("recreate" ,conf .removeExistingContainer ))
77
107
78
108
args := []string {
@@ -83,9 +113,7 @@ func (d *devcontainerCLI) Up(ctx context.Context, workspaceFolder, configPath st
83
113
if configPath != "" {
84
114
args = append (args ,"--config" ,configPath )
85
115
}
86
- if conf .removeExistingContainer {
87
- args = append (args ,"--remove-existing-container" )
88
- }
116
+ args = append (args ,conf .args ... )
89
117
cmd := d .execer .CommandContext (ctx ,"devcontainer" ,args ... )
90
118
91
119
// Capture stdout for parsing and stream logs for both default and provided writers.
@@ -117,6 +145,40 @@ func (d *devcontainerCLI) Up(ctx context.Context, workspaceFolder, configPath st
117
145
return result .ContainerID ,nil
118
146
}
119
147
148
+ func (d * devcontainerCLI )Exec (ctx context.Context ,workspaceFolder ,configPath string ,cmd string ,cmdArgs []string ,opts ... DevcontainerCLIOptions )error {
149
+ conf := applyDevcontainerCLIOptions ("exec" ,opts )
150
+ logger := d .logger .With (slog .F ("workspace_folder" ,workspaceFolder ),slog .F ("config_path" ,configPath ))
151
+
152
+ args := []string {"exec" }
153
+ if workspaceFolder != "" {
154
+ args = append (args ,"--workspace-folder" ,workspaceFolder )
155
+ }
156
+ if configPath != "" {
157
+ args = append (args ,"--config" ,configPath )
158
+ }
159
+ args = append (args ,conf .args ... )
160
+ args = append (args ,cmd )
161
+ args = append (args ,cmdArgs ... )
162
+ c := d .execer .CommandContext (ctx ,"devcontainer" ,args ... )
163
+
164
+ stdoutWriters := []io.Writer {& devcontainerCLILogWriter {ctx :ctx ,logger :logger .With (slog .F ("stdout" ,true ))}}
165
+ if conf .stdout != nil {
166
+ stdoutWriters = append (stdoutWriters ,conf .stdout )
167
+ }
168
+ c .Stdout = io .MultiWriter (stdoutWriters ... )
169
+ stderrWriters := []io.Writer {& devcontainerCLILogWriter {ctx :ctx ,logger :logger .With (slog .F ("stderr" ,true ))}}
170
+ if conf .stderr != nil {
171
+ stderrWriters = append (stderrWriters ,conf .stderr )
172
+ }
173
+ c .Stderr = io .MultiWriter (stderrWriters ... )
174
+
175
+ if err := c .Run ();err != nil {
176
+ return xerrors .Errorf ("devcontainer exec failed: %w" ,err )
177
+ }
178
+
179
+ return nil
180
+ }
181
+
120
182
// parseDevcontainerCLILastLine parses the last line of the devcontainer CLI output
121
183
// which is a JSON object.
122
184
func parseDevcontainerCLILastLine (ctx context.Context ,logger slog.Logger ,p []byte ) (result devcontainerCLIResult ,err error ) {