Content-Length: 832602 | pFad | http://github.com/coder/coder/commit/66cf90c73600e29ad9b818f52a70d79005618624

77 feat(agent/agentcontainers): allow auto start for discovered containe… · coder/coder@66cf90c · GitHub
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)








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/coder/coder/commit/66cf90c73600e29ad9b818f52a70d79005618624

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy