@@ -17,38 +17,83 @@ import (
1717// DevcontainerCLI is an interface for the devcontainer CLI.
1818type DevcontainerCLI interface {
1919 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
2021}
2122
22- // DevcontainerCLIUpOptions are options for the devcontainer CLI up
23+ // DevcontainerCLIUpOptions are options for the devcontainer CLI Up
2324// command.
2425type DevcontainerCLIUpOptions func (* devcontainerCLIUpConfig )
2526
27+ type devcontainerCLIUpConfig struct {
28+ args []string // Additional arguments for the Up command.
29+ stdout io.Writer
30+ stderr io.Writer
31+ }
32+
2633// WithRemoveExistingContainer is an option to remove the existing
2734// container.
2835func WithRemoveExistingContainer () DevcontainerCLIUpOptions {
2936 return func (o * devcontainerCLIUpConfig ) {
30- o .removeExistingContainer = true
37+ o .args = append ( o . args , "--remove-existing-container" )
3138 }
3239}
3340
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 {
3644 return func (o * devcontainerCLIUpConfig ) {
3745 o .stdout = stdout
3846 o .stderr = stderr
3947 }
4048}
4149
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+ }
4683}
4784
4885func 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+ }
5191 }
92+ return conf
93+ }
94+
95+ func applyDevcontainerCLIExecOptions (opts []DevcontainerCLIExecOptions ) devcontainerCLIExecConfig {
96+ conf := devcontainerCLIExecConfig {}
5297 for _ , opt := range opts {
5398 if opt != nil {
5499 opt (& conf )
@@ -73,7 +118,7 @@ func NewDevcontainerCLI(logger slog.Logger, execer agentexec.Execer) Devcontaine
73118
74119func (d * devcontainerCLI ) Up (ctx context.Context , workspaceFolder , configPath string , opts ... DevcontainerCLIUpOptions ) (string , error ) {
75120 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 ))
77122
78123 args := []string {
79124 "up" ,
@@ -83,9 +128,7 @@ func (d *devcontainerCLI) Up(ctx context.Context, workspaceFolder, configPath st
83128 if configPath != "" {
84129 args = append (args , "--config" , configPath )
85130 }
86- if conf .removeExistingContainer {
87- args = append (args , "--remove-existing-container" )
88- }
131+ args = append (args , conf .args ... )
89132 cmd := d .execer .CommandContext (ctx , "devcontainer" , args ... )
90133
91134 // 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
117160 return result .ContainerID , nil
118161}
119162
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+
120197// parseDevcontainerCLILastLine parses the last line of the devcontainer CLI output
121198// which is a JSON object.
122199func parseDevcontainerCLILastLine (ctx context.Context , logger slog.Logger , p []byte ) (result devcontainerCLIResult , err error ) {
0 commit comments