@@ -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 sets additional 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 ) {
0 commit comments