Skip to content

Commit 66cf90c

Browse files
feat(agent/agentcontainers): allow auto start for discovered containers (#19040)
Closes coder/internal#711 When a `devcontainer.json` has been found and it has `.customizations.coder.autoStart = true`, we will now auto start this dev container.
1 parent 398e80f commit 66cf90c

File tree

3 files changed

+270
-4
lines changed

3 files changed

+270
-4
lines changed

agent/agentcontainers/api.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ func WithCommandEnv(ce CommandEnv) Option {
143143
strings.HasPrefix(s, "CODER_WORKSPACE_AGENT_URL=") ||
144144
strings.HasPrefix(s, "CODER_AGENT_TOKEN=") ||
145145
strings.HasPrefix(s, "CODER_AGENT_AUTH=") ||
146-
strings.HasPrefix(s, "CODER_AGENT_DEVCONTAINERS_ENABLE=")
146+
strings.HasPrefix(s, "CODER_AGENT_DEVCONTAINERS_ENABLE=") ||
147+
strings.HasPrefix(s, "CODER_AGENT_DEVCONTAINERS_PROJECT_DISCOVERY_ENABLE=")
147148
})
148149
return shell, dir, env, nil
149150
}
@@ -524,23 +525,41 @@ func (api *API) discoverDevcontainersInProject(projectPath string) error {
524525

525526
workspaceFolder := strings.TrimSuffix(path, relativeConfigPath)
526527

527-
logger.Debug(api.ctx, "discovered dev container project", slog.F("workspace_folder", workspaceFolder))
528+
logger := logger.With(slog.F("workspace_folder", workspaceFolder))
529+
logger.Debug(api.ctx, "discovered dev container project")
528530

529531
api.mu.Lock()
530532
if _, found := api.knownDevcontainers[workspaceFolder]; !found {
531-
logger.Debug(api.ctx, "adding dev container project", slog.F("workspace_folder", workspaceFolder))
533+
logger.Debug(api.ctx, "adding dev container project")
532534

533535
dc := codersdk.WorkspaceAgentDevcontainer{
534536
ID: uuid.New(),
535537
Name: "", // Updated later based on container state.
536538
WorkspaceFolder: workspaceFolder,
537539
ConfigPath: path,
538-
Status: "", // Updated later based on container state.
540+
Status: codersdk.WorkspaceAgentDevcontainerStatusStopped,
539541
Dirty: false, // Updated later based on config file changes.
540542
Container: nil,
541543
}
542544

545+
config, err := api.dccli.ReadConfig(api.ctx, workspaceFolder, path, []string{})
546+
if err != nil {
547+
logger.Error(api.ctx, "read project configuration", slog.Error(err))
548+
} else if config.Configuration.Customizations.Coder.AutoStart {
549+
dc.Status = codersdk.WorkspaceAgentDevcontainerStatusStarting
550+
}
551+
543552
api.knownDevcontainers[workspaceFolder] = dc
553+
api.broadcastUpdatesLocked()
554+
555+
if dc.Status == codersdk.WorkspaceAgentDevcontainerStatusStarting {
556+
api.asyncWg.Add(1)
557+
go func() {
558+
defer api.asyncWg.Done()
559+
560+
_ = api.CreateDevcontainer(dc.WorkspaceFolder, dc.ConfigPath)
561+
}()
562+
}
544563
}
545564
api.mu.Unlock()
546565
}

agent/agentcontainers/api_test.go

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3568,4 +3568,250 @@ func TestDevcontainerDiscovery(t *testing.T) {
35683568
// This is implicitly handled by `testutil.Logger` failing when it
35693569
// detects an error has been logged.
35703570
})
3571+
3572+
t.Run("AutoStart", func(t *testing.T) {
3573+
t.Parallel()
3574+
3575+
tests := []struct {
3576+
name string
3577+
agentDir string
3578+
fs map[string]string
3579+
expectDevcontainerCount int
3580+
setupMocks func(mDCCLI *acmock.MockDevcontainerCLI)
3581+
}{
3582+
{
3583+
name: "SingleEnabled",
3584+
agentDir: "/home/coder",
3585+
expectDevcontainerCount: 1,
3586+
fs: map[string]string{
3587+
"/home/coder/.git/HEAD": "",
3588+
"/home/coder/.devcontainer/devcontainer.json": "",
3589+
},
3590+
setupMocks: func(mDCCLI *acmock.MockDevcontainerCLI) {
3591+
gomock.InOrder(
3592+
// Given: This dev container has auto start enabled.
3593+
mDCCLI.EXPECT().ReadConfig(gomock.Any(),
3594+
"/home/coder",
3595+
"/home/coder/.devcontainer/devcontainer.json",
3596+
[]string{},
3597+
).Return(agentcontainers.DevcontainerConfig{
3598+
Configuration: agentcontainers.DevcontainerConfiguration{
3599+
Customizations: agentcontainers.DevcontainerCustomizations{
3600+
Coder: agentcontainers.CoderCustomization{
3601+
AutoStart: true,
3602+
},
3603+
},
3604+
},
3605+
}, nil),
3606+
3607+
// Then: We expect it to be started.
3608+
mDCCLI.EXPECT().Up(gomock.Any(),
3609+
"/home/coder",
3610+
"/home/coder/.devcontainer/devcontainer.json",
3611+
gomock.Any(),
3612+
).Return("", nil),
3613+
)
3614+
},
3615+
},
3616+
{
3617+
name: "SingleDisabled",
3618+
agentDir: "/home/coder",
3619+
expectDevcontainerCount: 1,
3620+
fs: map[string]string{
3621+
"/home/coder/.git/HEAD": "",
3622+
"/home/coder/.devcontainer/devcontainer.json": "",
3623+
},
3624+
setupMocks: func(mDCCLI *acmock.MockDevcontainerCLI) {
3625+
gomock.InOrder(
3626+
// Given: This dev container has auto start disabled.
3627+
mDCCLI.EXPECT().ReadConfig(gomock.Any(),
3628+
"/home/coder",
3629+
"/home/coder/.devcontainer/devcontainer.json",
3630+
[]string{},
3631+
).Return(agentcontainers.DevcontainerConfig{
3632+
Configuration: agentcontainers.DevcontainerConfiguration{
3633+
Customizations: agentcontainers.DevcontainerCustomizations{
3634+
Coder: agentcontainers.CoderCustomization{
3635+
AutoStart: false,
3636+
},
3637+
},
3638+
},
3639+
}, nil),
3640+
3641+
// Then: We expect it to _not_ be started.
3642+
mDCCLI.EXPECT().Up(gomock.Any(),
3643+
"/home/coder",
3644+
"/home/coder/.devcontainer/devcontainer.json",
3645+
gomock.Any(),
3646+
).Return("", nil).Times(0),
3647+
)
3648+
},
3649+
},
3650+
{
3651+
name: "OneEnabledOneDisabled",
3652+
agentDir: "/home/coder",
3653+
expectDevcontainerCount: 2,
3654+
fs: map[string]string{
3655+
"/home/coder/.git/HEAD": "",
3656+
"/home/coder/.devcontainer/devcontainer.json": "",
3657+
"/home/coder/project/.devcontainer.json": "",
3658+
},
3659+
setupMocks: func(mDCCLI *acmock.MockDevcontainerCLI) {
3660+
gomock.InOrder(
3661+
// Given: This dev container has auto start enabled.
3662+
mDCCLI.EXPECT().ReadConfig(gomock.Any(),
3663+
"/home/coder",
3664+
"/home/coder/.devcontainer/devcontainer.json",
3665+
[]string{},
3666+
).Return(agentcontainers.DevcontainerConfig{
3667+
Configuration: agentcontainers.DevcontainerConfiguration{
3668+
Customizations: agentcontainers.DevcontainerCustomizations{
3669+
Coder: agentcontainers.CoderCustomization{
3670+
AutoStart: true,
3671+
},
3672+
},
3673+
},
3674+
}, nil),
3675+
3676+
// Then: We expect it to be started.
3677+
mDCCLI.EXPECT().Up(gomock.Any(),
3678+
"/home/coder",
3679+
"/home/coder/.devcontainer/devcontainer.json",
3680+
gomock.Any(),
3681+
).Return("", nil),
3682+
)
3683+
3684+
gomock.InOrder(
3685+
// Given: This dev container has auto start disabled.
3686+
mDCCLI.EXPECT().ReadConfig(gomock.Any(),
3687+
"/home/coder/project",
3688+
"/home/coder/project/.devcontainer.json",
3689+
[]string{},
3690+
).Return(agentcontainers.DevcontainerConfig{
3691+
Configuration: agentcontainers.DevcontainerConfiguration{
3692+
Customizations: agentcontainers.DevcontainerCustomizations{
3693+
Coder: agentcontainers.CoderCustomization{
3694+
AutoStart: false,
3695+
},
3696+
},
3697+
},
3698+
}, nil),
3699+
3700+
// Then: We expect it to _not_ be started.
3701+
mDCCLI.EXPECT().Up(gomock.Any(),
3702+
"/home/coder/project",
3703+
"/home/coder/project/.devcontainer.json",
3704+
gomock.Any(),
3705+
).Return("", nil).Times(0),
3706+
)
3707+
},
3708+
},
3709+
{
3710+
name: "MultipleEnabled",
3711+
agentDir: "/home/coder",
3712+
expectDevcontainerCount: 2,
3713+
fs: map[string]string{
3714+
"/home/coder/.git/HEAD": "",
3715+
"/home/coder/.devcontainer/devcontainer.json": "",
3716+
"/home/coder/project/.devcontainer.json": "",
3717+
},
3718+
setupMocks: func(mDCCLI *acmock.MockDevcontainerCLI) {
3719+
gomock.InOrder(
3720+
// Given: This dev container has auto start enabled.
3721+
mDCCLI.EXPECT().ReadConfig(gomock.Any(),
3722+
"/home/coder",
3723+
"/home/coder/.devcontainer/devcontainer.json",
3724+
[]string{},
3725+
).Return(agentcontainers.DevcontainerConfig{
3726+
Configuration: agentcontainers.DevcontainerConfiguration{
3727+
Customizations: agentcontainers.DevcontainerCustomizations{
3728+
Coder: agentcontainers.CoderCustomization{
3729+
AutoStart: true,
3730+
},
3731+
},
3732+
},
3733+
}, nil),
3734+
3735+
// Then: We expect it to be started.
3736+
mDCCLI.EXPECT().Up(gomock.Any(),
3737+
"/home/coder",
3738+
"/home/coder/.devcontainer/devcontainer.json",
3739+
gomock.Any(),
3740+
).Return("", nil),
3741+
)
3742+
3743+
gomock.InOrder(
3744+
// Given: This dev container has auto start enabled.
3745+
mDCCLI.EXPECT().ReadConfig(gomock.Any(),
3746+
"/home/coder/project",
3747+
"/home/coder/project/.devcontainer.json",
3748+
[]string{},
3749+
).Return(agentcontainers.DevcontainerConfig{
3750+
Configuration: agentcontainers.DevcontainerConfiguration{
3751+
Customizations: agentcontainers.DevcontainerCustomizations{
3752+
Coder: agentcontainers.CoderCustomization{
3753+
AutoStart: true,
3754+
},
3755+
},
3756+
},
3757+
}, nil),
3758+
3759+
// Then: We expect it to be started.
3760+
mDCCLI.EXPECT().Up(gomock.Any(),
3761+
"/home/coder/project",
3762+
"/home/coder/project/.devcontainer.json",
3763+
gomock.Any(),
3764+
).Return("", nil),
3765+
)
3766+
},
3767+
},
3768+
}
3769+
3770+
for _, tt := range tests {
3771+
t.Run(tt.name, func(t *testing.T) {
3772+
t.Parallel()
3773+
3774+
var (
3775+
ctx = testutil.Context(t, testutil.WaitShort)
3776+
logger = testutil.Logger(t)
3777+
mClock = quartz.NewMock(t)
3778+
mDCCLI = acmock.NewMockDevcontainerCLI(gomock.NewController(t))
3779+
3780+
r = chi.NewRouter()
3781+
)
3782+
3783+
// Given: We setup our mocks. These mocks handle our expectations for these
3784+
// tests. If there are missing/unexpected mock calls, the test will fail.
3785+
tt.setupMocks(mDCCLI)
3786+
3787+
api := agentcontainers.NewAPI(logger,
3788+
agentcontainers.WithClock(mClock),
3789+
agentcontainers.WithWatcher(watcher.NewNoop()),
3790+
agentcontainers.WithFileSystem(initFS(t, tt.fs)),
3791+
agentcontainers.WithManifestInfo("owner", "workspace", "parent-agent", "/home/coder"),
3792+
agentcontainers.WithContainerCLI(&fakeContainerCLI{}),
3793+
agentcontainers.WithDevcontainerCLI(mDCCLI),
3794+
agentcontainers.WithProjectDiscovery(true),
3795+
)
3796+
api.Start()
3797+
defer api.Close()
3798+
r.Mount("/", api.Routes())
3799+
3800+
// When: All expected dev containers have been found.
3801+
require.Eventuallyf(t, func() bool {
3802+
req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx)
3803+
rec := httptest.NewRecorder()
3804+
r.ServeHTTP(rec, req)
3805+
3806+
got := codersdk.WorkspaceAgentListContainersResponse{}
3807+
err := json.NewDecoder(rec.Body).Decode(&got)
3808+
require.NoError(t, err)
3809+
3810+
return len(got.Devcontainers) >= tt.expectDevcontainerCount
3811+
}, testutil.WaitShort, testutil.IntervalFast, "dev containers never found")
3812+
3813+
// Then: We expect the mock infra to not fail.
3814+
})
3815+
}
3816+
})
35713817
}

agent/agentcontainers/devcontainercli.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ type CoderCustomization struct {
9191
Apps []SubAgentApp `json:"apps,omitempty"`
9292
Name string `json:"name,omitempty"`
9393
Ignore bool `json:"ignore,omitempty"`
94+
AutoStart bool `json:"autoStart,omitempty"`
9495
}
9596

9697
type DevcontainerWorkspace struct {

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy