@@ -17,38 +17,83 @@ import (
17
17
// DevcontainerCLI is an interface for the devcontainer CLI.
18
18
type DevcontainerCLI interface {
19
19
Up (ctx context.Context ,workspaceFolder ,configPath string ,opts ... DevcontainerCLIUpOptions ) (id string ,err error )
20
+ Exec (ctx context.Context ,workspaceFolder ,configPath string ,cmd string ,cmdArgs []string ,opts ... DevcontainerCLIExecOptions )error
20
21
}
21
22
22
- // DevcontainerCLIUpOptions are options for the devcontainer CLIup
23
+ // DevcontainerCLIUpOptions are options for the devcontainer CLIUp
23
24
// command.
24
25
type DevcontainerCLIUpOptions func (* devcontainerCLIUpConfig )
25
26
27
+ type devcontainerCLIUpConfig struct {
28
+ args []string // Additional arguments for the Up command.
29
+ stdout io.Writer
30
+ stderr io.Writer
31
+ }
32
+
26
33
// WithRemoveExistingContainer is an option to remove the existing
27
34
// container.
28
35
func WithRemoveExistingContainer ()DevcontainerCLIUpOptions {
29
36
return func (o * devcontainerCLIUpConfig ) {
30
- o .removeExistingContainer = true
37
+ o .args = append ( o . args , "--remove-existing-container" )
31
38
}
32
39
}
33
40
34
- // WithOutput sets stdout and stderr writers for Up command logs.
35
- func WithOutput (stdout ,stderr io.Writer )DevcontainerCLIUpOptions {
41
+ // WithUpOutput sets additional stdout and stderr writers for logs
42
+ // during Up operations.
43
+ func WithUpOutput (stdout ,stderr io.Writer )DevcontainerCLIUpOptions {
36
44
return func (o * devcontainerCLIUpConfig ) {
37
45
o .stdout = stdout
38
46
o .stderr = stderr
39
47
}
40
48
}
41
49
42
- type devcontainerCLIUpConfig struct {
43
- removeExistingContainer bool
44
- stdout io.Writer
45
- stderr io.Writer
50
+ // DevcontainerCLIExecOptions are options for the devcontainer CLI Exec
51
+ // command.
52
+ type DevcontainerCLIExecOptions func (* devcontainerCLIExecConfig )
53
+
54
+ type devcontainerCLIExecConfig struct {
55
+ args []string // Additional arguments for the Exec command.
56
+ stdout io.Writer
57
+ stderr io.Writer
58
+ }
59
+
60
+ // WithExecOutput sets additional stdout and stderr writers for logs
61
+ // during Exec operations.
62
+ func WithExecOutput (stdout ,stderr io.Writer )DevcontainerCLIExecOptions {
63
+ return func (o * devcontainerCLIExecConfig ) {
64
+ o .stdout = stdout
65
+ o .stderr = stderr
66
+ }
67
+ }
68
+
69
+ // WithContainerID sets the container ID to target a specific container.
70
+ func WithContainerID (id string )DevcontainerCLIExecOptions {
71
+ return func (o * devcontainerCLIExecConfig ) {
72
+ o .args = append (o .args ,"--container-id" ,id )
73
+ }
74
+ }
75
+
76
+ // WithRemoteEnv sets environment variables for the Exec command.
77
+ func WithRemoteEnv (env ... string )DevcontainerCLIExecOptions {
78
+ return func (o * devcontainerCLIExecConfig ) {
79
+ for _ ,e := range env {
80
+ o .args = append (o .args ,"--remote-env" ,e )
81
+ }
82
+ }
46
83
}
47
84
48
85
func applyDevcontainerCLIUpOptions (opts []DevcontainerCLIUpOptions )devcontainerCLIUpConfig {
49
- conf := devcontainerCLIUpConfig {
50
- removeExistingContainer :false ,
86
+ conf := devcontainerCLIUpConfig {}
87
+ for _ ,opt := range opts {
88
+ if opt != nil {
89
+ opt (& conf )
90
+ }
51
91
}
92
+ return conf
93
+ }
94
+
95
+ func applyDevcontainerCLIExecOptions (opts []DevcontainerCLIExecOptions )devcontainerCLIExecConfig {
96
+ conf := devcontainerCLIExecConfig {}
52
97
for _ ,opt := range opts {
53
98
if opt != nil {
54
99
opt (& conf )
@@ -73,7 +118,7 @@ func NewDevcontainerCLI(logger slog.Logger, execer agentexec.Execer) Devcontaine
73
118
74
119
func (d * devcontainerCLI )Up (ctx context.Context ,workspaceFolder ,configPath string ,opts ... DevcontainerCLIUpOptions ) (string ,error ) {
75
120
conf := applyDevcontainerCLIUpOptions (opts )
76
- logger := d .logger .With (slog .F ("workspace_folder" ,workspaceFolder ),slog .F ("config_path" ,configPath ), slog . F ( "recreate" , conf . removeExistingContainer ) )
121
+ logger := d .logger .With (slog .F ("workspace_folder" ,workspaceFolder ),slog .F ("config_path" ,configPath ))
77
122
78
123
args := []string {
79
124
"up" ,
@@ -83,9 +128,7 @@ func (d *devcontainerCLI) Up(ctx context.Context, workspaceFolder, configPath st
83
128
if configPath != "" {
84
129
args = append (args ,"--config" ,configPath )
85
130
}
86
- if conf .removeExistingContainer {
87
- args = append (args ,"--remove-existing-container" )
88
- }
131
+ args = append (args ,conf .args ... )
89
132
cmd := d .execer .CommandContext (ctx ,"devcontainer" ,args ... )
90
133
91
134
// 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
117
160
return result .ContainerID ,nil
118
161
}
119
162
163
+ func (d * devcontainerCLI )Exec (ctx context.Context ,workspaceFolder ,configPath string ,cmd string ,cmdArgs []string ,opts ... DevcontainerCLIExecOptions )error {
164
+ conf := applyDevcontainerCLIExecOptions (opts )
165
+ logger := d .logger .With (slog .F ("workspace_folder" ,workspaceFolder ),slog .F ("config_path" ,configPath ))
166
+
167
+ args := []string {"exec" }
168
+ if workspaceFolder != "" {
169
+ args = append (args ,"--workspace-folder" ,workspaceFolder )
170
+ }
171
+ if configPath != "" {
172
+ args = append (args ,"--config" ,configPath )
173
+ }
174
+ args = append (args ,conf .args ... )
175
+ args = append (args ,cmd )
176
+ args = append (args ,cmdArgs ... )
177
+ c := d .execer .CommandContext (ctx ,"devcontainer" ,args ... )
178
+
179
+ stdoutWriters := []io.Writer {& devcontainerCLILogWriter {ctx :ctx ,logger :logger .With (slog .F ("stdout" ,true ))}}
180
+ if conf .stdout != nil {
181
+ stdoutWriters = append (stdoutWriters ,conf .stdout )
182
+ }
183
+ c .Stdout = io .MultiWriter (stdoutWriters ... )
184
+ stderrWriters := []io.Writer {& devcontainerCLILogWriter {ctx :ctx ,logger :logger .With (slog .F ("stderr" ,true ))}}
185
+ if conf .stderr != nil {
186
+ stderrWriters = append (stderrWriters ,conf .stderr )
187
+ }
188
+ c .Stderr = io .MultiWriter (stderrWriters ... )
189
+
190
+ if err := c .Run ();err != nil {
191
+ return xerrors .Errorf ("devcontainer exec failed: %w" ,err )
192
+ }
193
+
194
+ return nil
195
+ }
196
+
120
197
// parseDevcontainerCLILastLine parses the last line of the devcontainer CLI output
121
198
// which is a JSON object.
122
199
func parseDevcontainerCLILastLine (ctx context.Context ,logger slog.Logger ,p []byte ) (result devcontainerCLIResult ,err error ) {