Skip to content

Commit 788b2f9

Browse files
committed
wip: feat(agent): add devcontainer autostart support
1 parent d570ce7 commit 788b2f9

File tree

3 files changed

+74
-7
lines changed

3 files changed

+74
-7
lines changed

agent/agent.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -1115,7 +1115,12 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
11151115
}
11161116
}
11171117

1118-
err = a.scriptRunner.Init(manifest.Scripts, aAPI.ScriptCompleted)
1118+
var postStartScripts []codersdk.WorkspaceAgentScript
1119+
for _, dc := range manifest.Devcontainers {
1120+
postStartScripts = append(postStartScripts, agentcontainers.DevcontainerStartupScript(dc))
1121+
}
1122+
1123+
err = a.scriptRunner.Init(manifest.Scripts, aAPI.ScriptCompleted, agentscripts.WithPostStartScripts(postStartScripts...))
11191124
if err != nil {
11201125
return xerrors.Errorf("init script runner: %w", err)
11211126
}
@@ -1124,6 +1129,7 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
11241129
// here we use the graceful context because the script runner is not directly tied
11251130
// to the agent API.
11261131
err := a.scriptRunner.Execute(a.gracefulCtx, agentscripts.ExecuteStartScripts)
1132+
err = errors.Join(err, a.scriptRunner.Execute(a.gracefulCtx, agentscripts.ExecutePostStartScripts))
11271133
// Measure the time immediately after the script has finished
11281134
dur := time.Since(start).Seconds()
11291135
if err != nil {

agent/agentcontainers/devcontainer.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package agentcontainers
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/google/uuid"
7+
8+
"github.com/coder/coder/v2/codersdk"
9+
)
10+
11+
func DevcontainerStartupScript(dc codersdk.WorkspaceAgentDevcontainer) codersdk.WorkspaceAgentScript {
12+
script := fmt.Sprintf("devcontainer up --workspace-folder %q", dc.WorkspaceFolder)
13+
if dc.ConfigPath != "" {
14+
script = fmt.Sprintf("%s --config %q", script, dc.ConfigPath)
15+
}
16+
return codersdk.WorkspaceAgentScript{
17+
ID: uuid.New(),
18+
LogSourceID: uuid.Nil, // TODO(mafredri): Add a devcontainer log source?
19+
LogPath: "",
20+
Script: script,
21+
Cron: "",
22+
Timeout: 0,
23+
DisplayName: fmt.Sprintf("Dev Container (%s)", dc.WorkspaceFolder),
24+
}
25+
}

agent/agentscripts/agentscripts.go

+42-6
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,21 @@ func New(opts Options) *Runner {
8080

8181
type ScriptCompletedFunc func(context.Context, *proto.WorkspaceAgentScriptCompletedRequest) (*proto.WorkspaceAgentScriptCompletedResponse, error)
8282

83+
type runnerScript struct {
84+
runOnPostStart bool
85+
codersdk.WorkspaceAgentScript
86+
}
87+
88+
func toRunnerScript(scripts ...codersdk.WorkspaceAgentScript) []runnerScript {
89+
var rs []runnerScript
90+
for _, s := range scripts {
91+
rs = append(rs, runnerScript{
92+
WorkspaceAgentScript: s,
93+
})
94+
}
95+
return rs
96+
}
97+
8398
type Runner struct {
8499
Options
85100

@@ -90,7 +105,7 @@ type Runner struct {
90105
closeMutex sync.Mutex
91106
cron *cron.Cron
92107
initialized atomic.Bool
93-
scripts []codersdk.WorkspaceAgentScript
108+
scripts []runnerScript
94109
dataDir string
95110
scriptCompleted ScriptCompletedFunc
96111

@@ -119,30 +134,49 @@ func (r *Runner) RegisterMetrics(reg prometheus.Registerer) {
119134
reg.MustRegister(r.scriptsExecuted)
120135
}
121136

137+
// InitOption describes an option for the runner initialization.
138+
type InitOption func(*Runner)
139+
140+
// WithPostStartScripts adds scripts that should be run after the workspace
141+
// start scripts but before the workspace is marked as started.
142+
func WithPostStartScripts(scripts ...codersdk.WorkspaceAgentScript) InitOption {
143+
return func(r *Runner) {
144+
for _, s := range scripts {
145+
r.scripts = append(r.scripts, runnerScript{
146+
runOnPostStart: true,
147+
WorkspaceAgentScript: s,
148+
})
149+
}
150+
}
151+
}
152+
122153
// Init initializes the runner with the provided scripts.
123154
// It also schedules any scripts that have a schedule.
124155
// This function must be called before Execute.
125-
func (r *Runner) Init(scripts []codersdk.WorkspaceAgentScript, scriptCompleted ScriptCompletedFunc) error {
156+
func (r *Runner) Init(scripts []codersdk.WorkspaceAgentScript, scriptCompleted ScriptCompletedFunc, opts ...InitOption) error {
126157
if r.initialized.Load() {
127158
return xerrors.New("init: already initialized")
128159
}
129160
r.initialized.Store(true)
130-
r.scripts = scripts
161+
r.scripts = toRunnerScript(scripts...)
131162
r.scriptCompleted = scriptCompleted
163+
for _, opt := range opts {
164+
opt(r)
165+
}
132166
r.Logger.Info(r.cronCtx, "initializing agent scripts", slog.F("script_count", len(scripts)), slog.F("log_dir", r.LogDir))
133167

134168
err := r.Filesystem.MkdirAll(r.ScriptBinDir(), 0o700)
135169
if err != nil {
136170
return xerrors.Errorf("create script bin dir: %w", err)
137171
}
138172

139-
for _, script := range scripts {
173+
for _, script := range r.scripts {
140174
if script.Cron == "" {
141175
continue
142176
}
143177
script := script
144178
_, err := r.cron.AddFunc(script.Cron, func() {
145-
err := r.trackRun(r.cronCtx, script, ExecuteCronScripts)
179+
err := r.trackRun(r.cronCtx, script.WorkspaceAgentScript, ExecuteCronScripts)
146180
if err != nil {
147181
r.Logger.Warn(context.Background(), "run agent script on schedule", slog.Error(err))
148182
}
@@ -186,6 +220,7 @@ type ExecuteOption int
186220
const (
187221
ExecuteAllScripts ExecuteOption = iota
188222
ExecuteStartScripts
223+
ExecutePostStartScripts
189224
ExecuteStopScripts
190225
ExecuteCronScripts
191226
)
@@ -196,6 +231,7 @@ func (r *Runner) Execute(ctx context.Context, option ExecuteOption) error {
196231
for _, script := range r.scripts {
197232
runScript := (option == ExecuteStartScripts && script.RunOnStart) ||
198233
(option == ExecuteStopScripts && script.RunOnStop) ||
234+
(option == ExecutePostStartScripts && script.runOnPostStart) ||
199235
(option == ExecuteCronScripts && script.Cron != "") ||
200236
option == ExecuteAllScripts
201237

@@ -205,7 +241,7 @@ func (r *Runner) Execute(ctx context.Context, option ExecuteOption) error {
205241

206242
script := script
207243
eg.Go(func() error {
208-
err := r.trackRun(ctx, script, option)
244+
err := r.trackRun(ctx, script.WorkspaceAgentScript, option)
209245
if err != nil {
210246
return xerrors.Errorf("run agent script %q: %w", script.LogSourceID, err)
211247
}

0 commit comments

Comments
 (0)