@@ -42,22 +42,23 @@ const (
42
42
// API is responsible for container-related operations in the agent.
43
43
// It provides methods to list and manage containers.
44
44
type API struct {
45
- ctx context.Context
46
- cancel context.CancelFunc
47
- watcherDone chan struct {}
48
- updaterDone chan struct {}
49
- initialUpdateDone chan struct {}// Closed after first update in updaterLoop.
50
- updateTrigger chan chan error // Channel to trigger manual refresh.
51
- updateInterval time.Duration // Interval for periodic container updates.
52
- logger slog.Logger
53
- watcher watcher.Watcher
54
- execer agentexec.Execer
55
- ccli ContainerCLI
56
- dccli DevcontainerCLI
57
- clock quartz.Clock
58
- scriptLogger func (logSourceID uuid.UUID )ScriptLogger
59
- subAgentClient SubAgentClient
60
- subAgentURL string
45
+ ctx context.Context
46
+ cancel context.CancelFunc
47
+ watcherDone chan struct {}
48
+ updaterDone chan struct {}
49
+ initialUpdateDone chan struct {}// Closed after first update in updaterLoop.
50
+ updateTrigger chan chan error // Channel to trigger manual refresh.
51
+ updateInterval time.Duration // Interval for periodic container updates.
52
+ logger slog.Logger
53
+ watcher watcher.Watcher
54
+ execer agentexec.Execer
55
+ ccli ContainerCLI
56
+ containerLabelIncludeFilter map [string ]string // Labels to filter containers by.
57
+ dccli DevcontainerCLI
58
+ clock quartz.Clock
59
+ scriptLogger func (logSourceID uuid.UUID )ScriptLogger
60
+ subAgentClient SubAgentClient
61
+ subAgentURL string
61
62
62
63
mu sync.RWMutex
63
64
closed bool
@@ -106,6 +107,16 @@ func WithContainerCLI(ccli ContainerCLI) Option {
106
107
}
107
108
}
108
109
110
+ // WithContainerLabelIncludeFilter sets a label filter for containers.
111
+ // This option can be given multiple times to filter by multiple labels.
112
+ // The behavior is such that only containers matching one or more of the
113
+ // provided labels will be included.
114
+ func WithContainerLabelIncludeFilter (label ,value string )Option {
115
+ return func (api * API ) {
116
+ api .containerLabelIncludeFilter [label ]= value
117
+ }
118
+ }
119
+
109
120
// WithDevcontainerCLI sets the DevcontainerCLI implementation to use.
110
121
// This can be used in tests to modify @devcontainer/cli behavior.
111
122
func WithDevcontainerCLI (dccli DevcontainerCLI )Option {
@@ -198,24 +209,25 @@ func WithScriptLogger(scriptLogger func(logSourceID uuid.UUID) ScriptLogger) Opt
198
209
func NewAPI (logger slog.Logger ,options ... Option )* API {
199
210
ctx ,cancel := context .WithCancel (context .Background ())
200
211
api := & API {
201
- ctx :ctx ,
202
- cancel :cancel ,
203
- watcherDone :make (chan struct {}),
204
- updaterDone :make (chan struct {}),
205
- initialUpdateDone :make (chan struct {}),
206
- updateTrigger :make (chan chan error ),
207
- updateInterval :defaultUpdateInterval ,
208
- logger :logger ,
209
- clock :quartz .NewReal (),
210
- execer :agentexec .DefaultExecer ,
211
- subAgentClient :noopSubAgentClient {},
212
- devcontainerNames :make (map [string ]bool ),
213
- knownDevcontainers :make (map [string ]codersdk.WorkspaceAgentDevcontainer ),
214
- configFileModifiedTimes :make (map [string ]time.Time ),
215
- recreateSuccessTimes :make (map [string ]time.Time ),
216
- recreateErrorTimes :make (map [string ]time.Time ),
217
- scriptLogger :func (uuid.UUID )ScriptLogger {return noopScriptLogger {} },
218
- injectedSubAgentProcs :make (map [string ]subAgentProcess ),
212
+ ctx :ctx ,
213
+ cancel :cancel ,
214
+ watcherDone :make (chan struct {}),
215
+ updaterDone :make (chan struct {}),
216
+ initialUpdateDone :make (chan struct {}),
217
+ updateTrigger :make (chan chan error ),
218
+ updateInterval :defaultUpdateInterval ,
219
+ logger :logger ,
220
+ clock :quartz .NewReal (),
221
+ execer :agentexec .DefaultExecer ,
222
+ subAgentClient :noopSubAgentClient {},
223
+ containerLabelIncludeFilter :make (map [string ]string ),
224
+ devcontainerNames :make (map [string ]bool ),
225
+ knownDevcontainers :make (map [string ]codersdk.WorkspaceAgentDevcontainer ),
226
+ configFileModifiedTimes :make (map [string ]time.Time ),
227
+ recreateSuccessTimes :make (map [string ]time.Time ),
228
+ recreateErrorTimes :make (map [string ]time.Time ),
229
+ scriptLogger :func (uuid.UUID )ScriptLogger {return noopScriptLogger {} },
230
+ injectedSubAgentProcs :make (map [string ]subAgentProcess ),
219
231
}
220
232
// The ctx and logger must be set before applying options to avoid
221
233
// nil pointer dereference.
@@ -266,7 +278,7 @@ func (api *API) watcherLoop() {
266
278
continue
267
279
}
268
280
269
- now := api .clock .Now ("watcherLoop" )
281
+ now := api .clock .Now ("agentcontainers" , " watcherLoop" )
270
282
switch {
271
283
case event .Has (fsnotify .Create | fsnotify .Write ):
272
284
api .logger .Debug (api .ctx ,"devcontainer config file changed" ,slog .F ("file" ,event .Name ))
@@ -333,9 +345,9 @@ func (api *API) updaterLoop() {
333
345
}
334
346
335
347
return nil // Always nil to keep the ticker going.
336
- },"updaterLoop" )
348
+ },"agentcontainers" , " updaterLoop" )
337
349
defer func () {
338
- if err := ticker .Wait ("updaterLoop" );err != nil && ! errors .Is (err ,context .Canceled ) {
350
+ if err := ticker .Wait ("agentcontainers" , " updaterLoop" );err != nil && ! errors .Is (err ,context .Canceled ) {
339
351
api .logger .Error (api .ctx ,"updater loop ticker failed" ,slog .Error (err ))
340
352
}
341
353
}()
@@ -481,6 +493,22 @@ func (api *API) processUpdatedContainersLocked(ctx context.Context, updated code
481
493
slog .F ("config_file" ,configFile ),
482
494
)
483
495
496
+ if len (api .containerLabelIncludeFilter )> 0 {
497
+ var ok bool
498
+ for label ,value := range api .containerLabelIncludeFilter {
499
+ if v ,found := container .Labels [label ];found && v == value {
500
+ ok = true
501
+ }
502
+ }
503
+ // Verbose debug logging is fine here since typically filters
504
+ // are only used in development or testing environments.
505
+ if ! ok {
506
+ logger .Debug (ctx ,"container does not match include filter, ignoring dev container" ,slog .F ("container_labels" ,container .Labels ),slog .F ("include_filter" ,api .containerLabelIncludeFilter ))
507
+ continue
508
+ }
509
+ logger .Debug (ctx ,"container matches include filter, processing dev container" ,slog .F ("container_labels" ,container .Labels ),slog .F ("include_filter" ,api .containerLabelIncludeFilter ))
510
+ }
511
+
484
512
if dc ,ok := api .knownDevcontainers [workspaceFolder ];ok {
485
513
// If no config path is set, this devcontainer was defined
486
514
// in Terraform without the optional config file. Assume the
@@ -781,7 +809,7 @@ func (api *API) recreateDevcontainer(dc codersdk.WorkspaceAgentDevcontainer, con
781
809
dc .Container .DevcontainerStatus = dc .Status
782
810
}
783
811
api .knownDevcontainers [dc .WorkspaceFolder ]= dc
784
- api .recreateErrorTimes [dc .WorkspaceFolder ]= api .clock .Now ("recreate" ,"errorTimes" )
812
+ api .recreateErrorTimes [dc .WorkspaceFolder ]= api .clock .Now ("agentcontainers" , " recreate" ,"errorTimes" )
785
813
api .mu .Unlock ()
786
814
return
787
815
}
@@ -803,7 +831,7 @@ func (api *API) recreateDevcontainer(dc codersdk.WorkspaceAgentDevcontainer, con
803
831
dc .Container .DevcontainerStatus = dc .Status
804
832
}
805
833
dc .Dirty = false
806
- api .recreateSuccessTimes [dc .WorkspaceFolder ]= api .clock .Now ("recreate" ,"successTimes" )
834
+ api .recreateSuccessTimes [dc .WorkspaceFolder ]= api .clock .Now ("agentcontainers" , " recreate" ,"successTimes" )
807
835
api .knownDevcontainers [dc .WorkspaceFolder ]= dc
808
836
api .mu .Unlock ()
809
837