mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(agent): add devcontainer autostart support (#17076)
This change adds support for devcontainer autostart in workspaces. The
preconditions for utilizing this feature are:
1. The `coder_devcontainer` resource must be defined in Terraform
2. By the time the startup scripts have completed,
- The `@devcontainers/cli` tool must be installed
- The given workspace folder must contain a devcontainer configuration
Example Terraform:
```tf
resource "coder_devcontainer" "coder" {
agent_id = coder_agent.main.id
workspace_folder = "/home/coder/coder"
config_path = ".devcontainer/devcontainer.json" # (optional)
}
```
Closes #16423
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -151,11 +153,160 @@ func TestCronClose(t *testing.T) {
|
||||
require.NoError(t, runner.Close(), "close runner")
|
||||
}
|
||||
|
||||
func TestExecuteOptions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
startScript := codersdk.WorkspaceAgentScript{
|
||||
ID: uuid.New(),
|
||||
LogSourceID: uuid.New(),
|
||||
Script: "echo start",
|
||||
RunOnStart: true,
|
||||
}
|
||||
stopScript := codersdk.WorkspaceAgentScript{
|
||||
ID: uuid.New(),
|
||||
LogSourceID: uuid.New(),
|
||||
Script: "echo stop",
|
||||
RunOnStop: true,
|
||||
}
|
||||
postStartScript := codersdk.WorkspaceAgentScript{
|
||||
ID: uuid.New(),
|
||||
LogSourceID: uuid.New(),
|
||||
Script: "echo poststart",
|
||||
}
|
||||
regularScript := codersdk.WorkspaceAgentScript{
|
||||
ID: uuid.New(),
|
||||
LogSourceID: uuid.New(),
|
||||
Script: "echo regular",
|
||||
}
|
||||
|
||||
scripts := []codersdk.WorkspaceAgentScript{
|
||||
startScript,
|
||||
stopScript,
|
||||
regularScript,
|
||||
}
|
||||
allScripts := append(slices.Clone(scripts), postStartScript)
|
||||
|
||||
scriptByID := func(t *testing.T, id uuid.UUID) codersdk.WorkspaceAgentScript {
|
||||
for _, script := range allScripts {
|
||||
if script.ID == id {
|
||||
return script
|
||||
}
|
||||
}
|
||||
t.Fatal("script not found")
|
||||
return codersdk.WorkspaceAgentScript{}
|
||||
}
|
||||
|
||||
wantOutput := map[uuid.UUID]string{
|
||||
startScript.ID: "start",
|
||||
stopScript.ID: "stop",
|
||||
postStartScript.ID: "poststart",
|
||||
regularScript.ID: "regular",
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
option agentscripts.ExecuteOption
|
||||
wantRun []uuid.UUID
|
||||
}{
|
||||
{
|
||||
name: "ExecuteAllScripts",
|
||||
option: agentscripts.ExecuteAllScripts,
|
||||
wantRun: []uuid.UUID{startScript.ID, stopScript.ID, regularScript.ID, postStartScript.ID},
|
||||
},
|
||||
{
|
||||
name: "ExecuteStartScripts",
|
||||
option: agentscripts.ExecuteStartScripts,
|
||||
wantRun: []uuid.UUID{startScript.ID},
|
||||
},
|
||||
{
|
||||
name: "ExecutePostStartScripts",
|
||||
option: agentscripts.ExecutePostStartScripts,
|
||||
wantRun: []uuid.UUID{postStartScript.ID},
|
||||
},
|
||||
{
|
||||
name: "ExecuteStopScripts",
|
||||
option: agentscripts.ExecuteStopScripts,
|
||||
wantRun: []uuid.UUID{stopScript.ID},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
executedScripts := make(map[uuid.UUID]bool)
|
||||
fLogger := &executeOptionTestLogger{
|
||||
tb: t,
|
||||
executedScripts: executedScripts,
|
||||
wantOutput: wantOutput,
|
||||
}
|
||||
|
||||
runner := setup(t, func(uuid.UUID) agentscripts.ScriptLogger {
|
||||
return fLogger
|
||||
})
|
||||
defer runner.Close()
|
||||
|
||||
aAPI := agenttest.NewFakeAgentAPI(t, testutil.Logger(t), nil, nil)
|
||||
err := runner.Init(
|
||||
scripts,
|
||||
aAPI.ScriptCompleted,
|
||||
agentscripts.WithPostStartScripts(postStartScript),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = runner.Execute(ctx, tc.option)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotRun := map[uuid.UUID]bool{}
|
||||
for _, id := range tc.wantRun {
|
||||
gotRun[id] = true
|
||||
require.True(t, executedScripts[id],
|
||||
"script %s should have run when using filter %s", scriptByID(t, id).Script, tc.name)
|
||||
}
|
||||
|
||||
for _, script := range allScripts {
|
||||
if _, ok := gotRun[script.ID]; ok {
|
||||
continue
|
||||
}
|
||||
require.False(t, executedScripts[script.ID],
|
||||
"script %s should not have run when using filter %s", script.Script, tc.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type executeOptionTestLogger struct {
|
||||
tb testing.TB
|
||||
executedScripts map[uuid.UUID]bool
|
||||
wantOutput map[uuid.UUID]string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (l *executeOptionTestLogger) Send(_ context.Context, logs ...agentsdk.Log) error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
for _, log := range logs {
|
||||
l.tb.Log(log.Output)
|
||||
for id, output := range l.wantOutput {
|
||||
if log.Output == output {
|
||||
l.executedScripts[id] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*executeOptionTestLogger) Flush(context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func setup(t *testing.T, getScriptLogger func(logSourceID uuid.UUID) agentscripts.ScriptLogger) *agentscripts.Runner {
|
||||
t.Helper()
|
||||
if getScriptLogger == nil {
|
||||
// noop
|
||||
getScriptLogger = func(uuid uuid.UUID) agentscripts.ScriptLogger {
|
||||
getScriptLogger = func(uuid.UUID) agentscripts.ScriptLogger {
|
||||
return noopScriptLogger{}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user