@@ -42,22 +42,23 @@ const (
4242// API is responsible for container-related operations in the agent.
4343// It provides methods to list and manage containers.
4444type 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
6162
6263mu sync.RWMutex
6364closed bool
@@ -106,6 +107,16 @@ func WithContainerCLI(ccli ContainerCLI) Option {
106107}
107108}
108109
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+
109120// WithDevcontainerCLI sets the DevcontainerCLI implementation to use.
110121// This can be used in tests to modify @devcontainer/cli behavior.
111122func WithDevcontainerCLI (dccli DevcontainerCLI )Option {
@@ -198,24 +209,25 @@ func WithScriptLogger(scriptLogger func(logSourceID uuid.UUID) ScriptLogger) Opt
198209func NewAPI (logger slog.Logger ,options ... Option )* API {
199210ctx ,cancel := context .WithCancel (context .Background ())
200211api := & 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 ),
219231}
220232// The ctx and logger must be set before applying options to avoid
221233// nil pointer dereference.
@@ -266,7 +278,7 @@ func (api *API) watcherLoop() {
266278continue
267279}
268280
269- now := api .clock .Now ("watcherLoop" )
281+ now := api .clock .Now ("agentcontainers" , " watcherLoop" )
270282switch {
271283case event .Has (fsnotify .Create | fsnotify .Write ):
272284api .logger .Debug (api .ctx ,"devcontainer config file changed" ,slog .F ("file" ,event .Name ))
@@ -333,9 +345,9 @@ func (api *API) updaterLoop() {
333345}
334346
335347return nil // Always nil to keep the ticker going.
336- },"updaterLoop" )
348+ },"agentcontainers" , " updaterLoop" )
337349defer 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 ) {
339351api .logger .Error (api .ctx ,"updater loop ticker failed" ,slog .Error (err ))
340352}
341353}()
@@ -481,6 +493,22 @@ func (api *API) processUpdatedContainersLocked(ctx context.Context, updated code
481493slog .F ("config_file" ,configFile ),
482494)
483495
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+
484512if dc ,ok := api .knownDevcontainers [workspaceFolder ];ok {
485513// If no config path is set, this devcontainer was defined
486514// in Terraform without the optional config file. Assume the
@@ -781,7 +809,7 @@ func (api *API) recreateDevcontainer(dc codersdk.WorkspaceAgentDevcontainer, con
781809dc .Container .DevcontainerStatus = dc .Status
782810}
783811api .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" )
785813api .mu .Unlock ()
786814return
787815}
@@ -803,7 +831,7 @@ func (api *API) recreateDevcontainer(dc codersdk.WorkspaceAgentDevcontainer, con
803831dc .Container .DevcontainerStatus = dc .Status
804832}
805833dc .Dirty = false
806- api .recreateSuccessTimes [dc .WorkspaceFolder ]= api .clock .Now ("recreate" ,"successTimes" )
834+ api .recreateSuccessTimes [dc .WorkspaceFolder ]= api .clock .Now ("agentcontainers" , " recreate" ,"successTimes" )
807835api .knownDevcontainers [dc .WorkspaceFolder ]= dc
808836api .mu .Unlock ()
809837