5
5
"encoding/json"
6
6
"errors"
7
7
"fmt"
8
+ "io/fs"
8
9
"maps"
9
10
"net/http"
10
11
"os"
@@ -21,6 +22,7 @@ import (
21
22
"github.com/fsnotify/fsnotify"
22
23
"github.com/go-chi/chi/v5"
23
24
"github.com/google/uuid"
25
+ "github.com/spf13/afero"
24
26
"golang.org/x/xerrors"
25
27
26
28
"cdr.dev/slog"
@@ -60,6 +62,7 @@ type API struct {
60
62
updateInterval time.Duration // Interval for periodic container updates.
61
63
logger slog.Logger
62
64
watcher watcher.Watcher
65
+ fs afero.Fs
63
66
execer agentexec.Execer
64
67
commandEnv CommandEnv
65
68
ccli ContainerCLI
@@ -71,9 +74,10 @@ type API struct {
71
74
subAgentURL string
72
75
subAgentEnv []string
73
76
74
- ownerName string
75
- workspaceName string
76
- parentAgent string
77
+ ownerName string
78
+ workspaceName string
79
+ parentAgent string
80
+ agentDirectory string
77
81
78
82
mu sync.RWMutex // Protects the following fields.
79
83
initDone chan struct {} // Closed by Init.
@@ -192,11 +196,12 @@ func WithSubAgentEnv(env ...string) Option {
192
196
193
197
// WithManifestInfo sets the owner name, and workspace name
194
198
// for the sub-agent.
195
- func WithManifestInfo (owner , workspace , parentAgent string ) Option {
199
+ func WithManifestInfo (owner , workspace , parentAgent , agentDirectory string ) Option {
196
200
return func (api * API ) {
197
201
api .ownerName = owner
198
202
api .workspaceName = workspace
199
203
api .parentAgent = parentAgent
204
+ api .agentDirectory = agentDirectory
200
205
}
201
206
}
202
207
@@ -261,6 +266,13 @@ func WithWatcher(w watcher.Watcher) Option {
261
266
}
262
267
}
263
268
269
+ // WithFileSystem sets the file system used for discovering projects.
270
+ func WithFileSystem (fs afero.Fs ) Option {
271
+ return func (api * API ) {
272
+ api .fs = fs
273
+ }
274
+ }
275
+
264
276
// ScriptLogger is an interface for sending devcontainer logs to the
265
277
// controlplane.
266
278
type ScriptLogger interface {
@@ -331,6 +343,9 @@ func NewAPI(logger slog.Logger, options ...Option) *API {
331
343
api .watcher = watcher .NewNoop ()
332
344
}
333
345
}
346
+ if api .fs == nil {
347
+ api .fs = afero .NewOsFs ()
348
+ }
334
349
if api .subAgentClient .Load () == nil {
335
350
var c SubAgentClient = noopSubAgentClient {}
336
351
api .subAgentClient .Store (& c )
@@ -375,10 +390,85 @@ func (api *API) Start() {
375
390
api .watcherDone = make (chan struct {})
376
391
api .updaterDone = make (chan struct {})
377
392
393
+ go func () {
394
+ if err := api .discoverDevcontainerProjects (); err != nil {
395
+ api .logger .Error (api .ctx , "discovering dev container projects" , slog .Error (err ))
396
+ }
397
+ }()
398
+
378
399
go api .watcherLoop ()
379
400
go api .updaterLoop ()
380
401
}
381
402
403
+ func (api * API ) discoverDevcontainerProjects () error {
404
+ isGitProject , err := afero .DirExists (api .fs , filepath .Join (api .agentDirectory , "/.git" ))
405
+ if err != nil {
406
+ return xerrors .Errorf (".git dir exists: %w" , err )
407
+ }
408
+
409
+ // If the agent directory is a git project, we'll search
410
+ // the project for any `.devcontainer/devcontainer.json`
411
+ // files.
412
+ if isGitProject {
413
+ return api .discoverDevcontainersInProject (api .agentDirectory )
414
+ }
415
+
416
+ // The agent directory is _not_ a git project, so we'll
417
+ // search the top level of the agent directory for any
418
+ // git projects, and search those.
419
+ entries , err := afero .ReadDir (api .fs , api .agentDirectory )
420
+ if err != nil {
421
+ return xerrors .Errorf ("read agent directory: %w" , err )
422
+ }
423
+
424
+ for _ , entry := range entries {
425
+ if ! entry .IsDir () {
426
+ continue
427
+ }
428
+
429
+ isGitProject , err = afero .DirExists (api .fs , filepath .Join (api .agentDirectory , entry .Name (), ".git" ))
430
+ if err != nil {
431
+ return xerrors .Errorf (".git dir exists: %w" , err )
432
+ }
433
+
434
+ // If this directory is a git project, we'll search
435
+ // it for any `.devcontainer/devcontainer.json` files.
436
+ if isGitProject {
437
+ if err := api .discoverDevcontainersInProject (filepath .Join (api .agentDirectory , entry .Name ())); err != nil {
438
+ return err
439
+ }
440
+ }
441
+ }
442
+
443
+ return nil
444
+ }
445
+
446
+ func (api * API ) discoverDevcontainersInProject (projectPath string ) error {
447
+ return afero .Walk (api .fs , projectPath , func (path string , info fs.FileInfo , err error ) error {
448
+ if strings .HasSuffix (path , ".devcontainer/devcontainer.json" ) {
449
+ workspaceFolder := strings .TrimSuffix (path , ".devcontainer/devcontainer.json" )
450
+
451
+ api .mu .Lock ()
452
+ if _ , found := api .knownDevcontainers [workspaceFolder ]; ! found {
453
+ dc := codersdk.WorkspaceAgentDevcontainer {
454
+ ID : uuid .New (),
455
+ Name : "" , // Updated later based on container state.
456
+ WorkspaceFolder : workspaceFolder ,
457
+ ConfigPath : path ,
458
+ Status : "" , // Updated later based on container state.
459
+ Dirty : false , // Updated later based on config file changes.
460
+ Container : nil ,
461
+ }
462
+
463
+ api .knownDevcontainers [workspaceFolder ] = dc
464
+ }
465
+ api .mu .Unlock ()
466
+ }
467
+
468
+ return nil
469
+ })
470
+ }
471
+
382
472
func (api * API ) watcherLoop () {
383
473
defer close (api .watcherDone )
384
474
defer api .logger .Debug (api .ctx , "watcher loop stopped" )
0 commit comments