Skip to content

Commit efe6e06

Browse files
committed
Merge branch 'main' into lilac/storybook-9
2 parents 363ac27 + 72b8ab5 commit efe6e06

File tree

143 files changed

+6387
-1207
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

143 files changed

+6387
-1207
lines changed

CODEOWNERS

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,9 @@ vpn/version.go @spikecurtis @johnstcn
1111
# This caching code is particularly tricky, and one must be very careful when
1212
# altering it.
1313
coderd/files/ @aslilac
14+
15+
coderd/dynamicparameters/ @Emyrk
16+
coderd/rbac/ @Emyrk
17+
18+
# Mainly dependent on coder/guts, which is maintained by @Emyrk
19+
scripts/apitypings/ @Emyrk

agent/agent.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1168,7 +1168,7 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
11681168
// return existing devcontainers but actual container detection
11691169
// and creation will be deferred.
11701170
a.containerAPI.Init(
1171-
agentcontainers.WithManifestInfo(manifest.OwnerName, manifest.WorkspaceName, manifest.AgentName),
1171+
agentcontainers.WithManifestInfo(manifest.OwnerName, manifest.WorkspaceName, manifest.AgentName, manifest.Directory),
11721172
agentcontainers.WithDevcontainers(manifest.Devcontainers, manifest.Scripts),
11731173
agentcontainers.WithSubAgentClient(agentcontainers.NewSubAgentClientFromAPI(a.logger, aAPI)),
11741174
)

agent/agentcontainers/api.go

Lines changed: 197 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8+
"io/fs"
89
"maps"
910
"net/http"
1011
"os"
@@ -20,10 +21,13 @@ import (
2021

2122
"github.com/fsnotify/fsnotify"
2223
"github.com/go-chi/chi/v5"
24+
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
2325
"github.com/google/uuid"
26+
"github.com/spf13/afero"
2427
"golang.org/x/xerrors"
2528

2629
"cdr.dev/slog"
30+
"github.com/coder/coder/v2/agent/agentcontainers/ignore"
2731
"github.com/coder/coder/v2/agent/agentcontainers/watcher"
2832
"github.com/coder/coder/v2/agent/agentexec"
2933
"github.com/coder/coder/v2/agent/usershell"
@@ -56,10 +60,12 @@ type API struct {
5660
cancel context.CancelFunc
5761
watcherDone chan struct{}
5862
updaterDone chan struct{}
63+
discoverDone chan struct{}
5964
updateTrigger chan chan error // Channel to trigger manual refresh.
6065
updateInterval time.Duration // Interval for periodic container updates.
6166
logger slog.Logger
6267
watcher watcher.Watcher
68+
fs afero.Fs
6369
execer agentexec.Execer
6470
commandEnv CommandEnv
6571
ccli ContainerCLI
@@ -71,9 +77,12 @@ type API struct {
7177
subAgentURL string
7278
subAgentEnv []string
7379

74-
ownerName string
75-
workspaceName string
76-
parentAgent string
80+
projectDiscovery bool // If we should perform project discovery or not.
81+
82+
ownerName string
83+
workspaceName string
84+
parentAgent string
85+
agentDirectory string
7786

7887
mu sync.RWMutex // Protects the following fields.
7988
initDone chan struct{} // Closed by Init.
@@ -134,7 +143,8 @@ func WithCommandEnv(ce CommandEnv) Option {
134143
strings.HasPrefix(s, "CODER_WORKSPACE_AGENT_URL=") ||
135144
strings.HasPrefix(s, "CODER_AGENT_TOKEN=") ||
136145
strings.HasPrefix(s, "CODER_AGENT_AUTH=") ||
137-
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=")
138148
})
139149
return shell, dir, env, nil
140150
}
@@ -192,11 +202,12 @@ func WithSubAgentEnv(env ...string) Option {
192202

193203
// WithManifestInfo sets the owner name, and workspace name
194204
// for the sub-agent.
195-
func WithManifestInfo(owner, workspace, parentAgent string) Option {
205+
func WithManifestInfo(owner, workspace, parentAgent, agentDirectory string) Option {
196206
return func(api *API) {
197207
api.ownerName = owner
198208
api.workspaceName = workspace
199209
api.parentAgent = parentAgent
210+
api.agentDirectory = agentDirectory
200211
}
201212
}
202213

@@ -261,6 +272,21 @@ func WithWatcher(w watcher.Watcher) Option {
261272
}
262273
}
263274

275+
// WithFileSystem sets the file system used for discovering projects.
276+
func WithFileSystem(fileSystem afero.Fs) Option {
277+
return func(api *API) {
278+
api.fs = fileSystem
279+
}
280+
}
281+
282+
// WithProjectDiscovery sets if the API should attempt to discover
283+
// projects on the filesystem.
284+
func WithProjectDiscovery(projectDiscovery bool) Option {
285+
return func(api *API) {
286+
api.projectDiscovery = projectDiscovery
287+
}
288+
}
289+
264290
// ScriptLogger is an interface for sending devcontainer logs to the
265291
// controlplane.
266292
type ScriptLogger interface {
@@ -331,6 +357,9 @@ func NewAPI(logger slog.Logger, options ...Option) *API {
331357
api.watcher = watcher.NewNoop()
332358
}
333359
}
360+
if api.fs == nil {
361+
api.fs = afero.NewOsFs()
362+
}
334363
if api.subAgentClient.Load() == nil {
335364
var c SubAgentClient = noopSubAgentClient{}
336365
api.subAgentClient.Store(&c)
@@ -372,13 +401,173 @@ func (api *API) Start() {
372401
return
373402
}
374403

404+
if api.projectDiscovery && api.agentDirectory != "" {
405+
api.discoverDone = make(chan struct{})
406+
407+
go api.discover()
408+
}
409+
375410
api.watcherDone = make(chan struct{})
376411
api.updaterDone = make(chan struct{})
377412

378413
go api.watcherLoop()
379414
go api.updaterLoop()
380415
}
381416

417+
func (api *API) discover() {
418+
defer close(api.discoverDone)
419+
defer api.logger.Debug(api.ctx, "project discovery finished")
420+
api.logger.Debug(api.ctx, "project discovery started")
421+
422+
if err := api.discoverDevcontainerProjects(); err != nil {
423+
api.logger.Error(api.ctx, "discovering dev container projects", slog.Error(err))
424+
}
425+
426+
if err := api.RefreshContainers(api.ctx); err != nil {
427+
api.logger.Error(api.ctx, "refreshing containers after discovery", slog.Error(err))
428+
}
429+
}
430+
431+
func (api *API) discoverDevcontainerProjects() error {
432+
isGitProject, err := afero.DirExists(api.fs, filepath.Join(api.agentDirectory, ".git"))
433+
if err != nil {
434+
return xerrors.Errorf(".git dir exists: %w", err)
435+
}
436+
437+
// If the agent directory is a git project, we'll search
438+
// the project for any `.devcontainer/devcontainer.json`
439+
// files.
440+
if isGitProject {
441+
return api.discoverDevcontainersInProject(api.agentDirectory)
442+
}
443+
444+
// The agent directory is _not_ a git project, so we'll
445+
// search the top level of the agent directory for any
446+
// git projects, and search those.
447+
entries, err := afero.ReadDir(api.fs, api.agentDirectory)
448+
if err != nil {
449+
return xerrors.Errorf("read agent directory: %w", err)
450+
}
451+
452+
for _, entry := range entries {
453+
if !entry.IsDir() {
454+
continue
455+
}
456+
457+
isGitProject, err = afero.DirExists(api.fs, filepath.Join(api.agentDirectory, entry.Name(), ".git"))
458+
if err != nil {
459+
return xerrors.Errorf(".git dir exists: %w", err)
460+
}
461+
462+
// If this directory is a git project, we'll search
463+
// it for any `.devcontainer/devcontainer.json` files.
464+
if isGitProject {
465+
if err := api.discoverDevcontainersInProject(filepath.Join(api.agentDirectory, entry.Name())); err != nil {
466+
return err
467+
}
468+
}
469+
}
470+
471+
return nil
472+
}
473+
474+
func (api *API) discoverDevcontainersInProject(projectPath string) error {
475+
logger := api.logger.
476+
Named("project-discovery").
477+
With(slog.F("project_path", projectPath))
478+
479+
globalPatterns, err := ignore.LoadGlobalPatterns(api.fs)
480+
if err != nil {
481+
return xerrors.Errorf("read global git ignore patterns: %w", err)
482+
}
483+
484+
patterns, err := ignore.ReadPatterns(api.ctx, logger, api.fs, projectPath)
485+
if err != nil {
486+
return xerrors.Errorf("read git ignore patterns: %w", err)
487+
}
488+
489+
matcher := gitignore.NewMatcher(append(globalPatterns, patterns...))
490+
491+
devcontainerConfigPaths := []string{
492+
"/.devcontainer/devcontainer.json",
493+
"/.devcontainer.json",
494+
}
495+
496+
return afero.Walk(api.fs, projectPath, func(path string, info fs.FileInfo, err error) error {
497+
if err != nil {
498+
logger.Error(api.ctx, "encountered error while walking for dev container projects",
499+
slog.F("path", path),
500+
slog.Error(err))
501+
return nil
502+
}
503+
504+
pathParts := ignore.FilePathToParts(path)
505+
506+
// We know that a directory entry cannot be a `devcontainer.json` file, so we
507+
// always skip processing directories. If the directory happens to be ignored
508+
// by git then we'll make sure to ignore all of the children of that directory.
509+
if info.IsDir() {
510+
if matcher.Match(pathParts, true) {
511+
return fs.SkipDir
512+
}
513+
514+
return nil
515+
}
516+
517+
if matcher.Match(pathParts, false) {
518+
return nil
519+
}
520+
521+
for _, relativeConfigPath := range devcontainerConfigPaths {
522+
if !strings.HasSuffix(path, relativeConfigPath) {
523+
continue
524+
}
525+
526+
workspaceFolder := strings.TrimSuffix(path, relativeConfigPath)
527+
528+
logger := logger.With(slog.F("workspace_folder", workspaceFolder))
529+
logger.Debug(api.ctx, "discovered dev container project")
530+
531+
api.mu.Lock()
532+
if _, found := api.knownDevcontainers[workspaceFolder]; !found {
533+
logger.Debug(api.ctx, "adding dev container project")
534+
535+
dc := codersdk.WorkspaceAgentDevcontainer{
536+
ID: uuid.New(),
537+
Name: "", // Updated later based on container state.
538+
WorkspaceFolder: workspaceFolder,
539+
ConfigPath: path,
540+
Status: codersdk.WorkspaceAgentDevcontainerStatusStopped,
541+
Dirty: false, // Updated later based on config file changes.
542+
Container: nil,
543+
}
544+
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+
552+
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+
}
563+
}
564+
api.mu.Unlock()
565+
}
566+
567+
return nil
568+
})
569+
}
570+
382571
func (api *API) watcherLoop() {
383572
defer close(api.watcherDone)
384573
defer api.logger.Debug(api.ctx, "watcher loop stopped")
@@ -1808,6 +1997,9 @@ func (api *API) Close() error {
18081997
if api.updaterDone != nil {
18091998
<-api.updaterDone
18101999
}
2000+
if api.discoverDone != nil {
2001+
<-api.discoverDone
2002+
}
18112003

18122004
// Wait for all async tasks to complete.
18132005
api.asyncWg.Wait()

0 commit comments

Comments
 (0)