@@ -91,6 +91,7 @@ type Options struct {
9191Execer agentexec.Execer
9292Devcontainers bool
9393DevcontainerAPIOptions []agentcontainers.Option // Enable Devcontainers for these to be effective.
94+ Clock quartz.Clock
9495}
9596
9697type Client interface {
@@ -144,6 +145,9 @@ func New(options Options) Agent {
144145if options .PortCacheDuration == 0 {
145146options .PortCacheDuration = 1 * time .Second
146147}
148+ if options .Clock == nil {
149+ options .Clock = quartz .NewReal ()
150+ }
147151
148152prometheusRegistry := options .PrometheusRegistry
149153if prometheusRegistry == nil {
@@ -157,6 +161,7 @@ func New(options Options) Agent {
157161hardCtx ,hardCancel := context .WithCancel (context .Background ())
158162gracefulCtx ,gracefulCancel := context .WithCancel (hardCtx )
159163a := & agent {
164+ clock :options .Clock ,
160165tailnetListenPort :options .TailnetListenPort ,
161166reconnectingPTYTimeout :options .ReconnectingPTYTimeout ,
162167logger :options .Logger ,
@@ -204,6 +209,7 @@ func New(options Options) Agent {
204209}
205210
206211type agent struct {
212+ clock quartz.Clock
207213logger slog.Logger
208214client Client
209215exchangeToken func (ctx context.Context ) (string ,error )
@@ -273,7 +279,7 @@ type agent struct {
273279
274280devcontainers bool
275281containerAPIOptions []agentcontainers.Option
276- containerAPI atomic. Pointer [ agentcontainers.API ] // Set by apiHandler.
282+ containerAPI * agentcontainers.API
277283}
278284
279285func (a * agent )TailnetConn ()* tailnet.Conn {
@@ -330,6 +336,19 @@ func (a *agent) init() {
330336// will not report anywhere.
331337a .scriptRunner .RegisterMetrics (a .prometheusRegistry )
332338
339+ if a .devcontainers {
340+ containerAPIOpts := []agentcontainers.Option {
341+ agentcontainers .WithExecer (a .execer ),
342+ agentcontainers .WithCommandEnv (a .sshServer .CommandEnv ),
343+ agentcontainers .WithScriptLogger (func (logSourceID uuid.UUID ) agentcontainers.ScriptLogger {
344+ return a .logSender .GetScriptLogger (logSourceID )
345+ }),
346+ }
347+ containerAPIOpts = append (containerAPIOpts ,a .containerAPIOptions ... )
348+
349+ a .containerAPI = agentcontainers .NewAPI (a .logger .Named ("containers" ),containerAPIOpts ... )
350+ }
351+
333352a .reconnectingPTYServer = reconnectingpty .NewServer (
334353a .logger .Named ("reconnecting-pty" ),
335354a .sshServer ,
@@ -1141,17 +1160,27 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
11411160}
11421161
11431162var (
1144- scripts = manifest .Scripts
1145- scriptRunnerOpts []agentscripts. InitOption
1163+ scripts = manifest .Scripts
1164+ devcontainerScripts map [uuid. UUID ]codersdk. WorkspaceAgentScript
11461165)
1147- if a .devcontainers {
1148- var dcScripts []codersdk.WorkspaceAgentScript
1149- scripts ,dcScripts = agentcontainers .ExtractAndInitializeDevcontainerScripts (manifest .Devcontainers ,scripts )
1150- // See ExtractAndInitializeDevcontainerScripts for motivation
1151- // behind running dcScripts as post start scripts.
1152- scriptRunnerOpts = append (scriptRunnerOpts ,agentscripts .WithPostStartScripts (dcScripts ... ))
1166+ if a .containerAPI != nil {
1167+ // Init the container API with the manifest and client so that
1168+ // we can start accepting requests. The final start of the API
1169+ // happens after the startup scripts have been executed to
1170+ // ensure the presence of required tools. This means we can
1171+ // return existing devcontainers but actual container detection
1172+ // and creation will be deferred.
1173+ a .containerAPI .Init (
1174+ agentcontainers .WithManifestInfo (manifest .OwnerName ,manifest .WorkspaceName ,manifest .AgentName ),
1175+ agentcontainers .WithDevcontainers (manifest .Devcontainers ,manifest .Scripts ),
1176+ agentcontainers .WithSubAgentClient (agentcontainers .NewSubAgentClientFromAPI (a .logger ,aAPI )),
1177+ )
1178+
1179+ // Since devcontainer are enabled, remove devcontainer scripts
1180+ // from the main scripts list to avoid showing an error.
1181+ scripts ,devcontainerScripts = agentcontainers .ExtractDevcontainerScripts (manifest .Devcontainers ,scripts )
11531182}
1154- err = a .scriptRunner .Init (scripts ,aAPI .ScriptCompleted , scriptRunnerOpts ... )
1183+ err = a .scriptRunner .Init (scripts ,aAPI .ScriptCompleted )
11551184if err != nil {
11561185return xerrors .Errorf ("init script runner: %w" ,err )
11571186}
@@ -1168,7 +1197,18 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
11681197// finished (both start and post start). For instance, an
11691198// autostarted devcontainer will be included in this time.
11701199err := a .scriptRunner .Execute (a .gracefulCtx ,agentscripts .ExecuteStartScripts )
1171- err = errors .Join (err ,a .scriptRunner .Execute (a .gracefulCtx ,agentscripts .ExecutePostStartScripts ))
1200+
1201+ if a .containerAPI != nil {
1202+ // Start the container API after the startup scripts have
1203+ // been executed to ensure that the required tools can be
1204+ // installed.
1205+ a .containerAPI .Start ()
1206+ for _ ,dc := range manifest .Devcontainers {
1207+ cErr := a .createDevcontainer (ctx ,aAPI ,dc ,devcontainerScripts [dc .ID ])
1208+ err = errors .Join (err ,cErr )
1209+ }
1210+ }
1211+
11721212dur := time .Since (start ).Seconds ()
11731213if err != nil {
11741214a .logger .Warn (ctx ,"startup script(s) failed" ,slog .Error (err ))
@@ -1187,14 +1227,6 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
11871227}
11881228a .metrics .startupScriptSeconds .WithLabelValues (label ).Set (dur )
11891229a .scriptRunner .StartCron ()
1190-
1191- // If the container API is enabled, trigger an immediate refresh
1192- // for quick sub agent injection.
1193- if cAPI := a .containerAPI .Load ();cAPI != nil {
1194- if err := cAPI .RefreshContainers (ctx );err != nil {
1195- a .logger .Error (ctx ,"failed to refresh containers" ,slog .Error (err ))
1196- }
1197- }
11981230})
11991231if err != nil {
12001232return xerrors .Errorf ("track conn goroutine: %w" ,err )
@@ -1204,6 +1236,38 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
12041236}
12051237}
12061238
1239+ func (a * agent )createDevcontainer (
1240+ ctx context.Context ,
1241+ aAPI proto.DRPCAgentClient26 ,
1242+ dc codersdk.WorkspaceAgentDevcontainer ,
1243+ script codersdk.WorkspaceAgentScript ,
1244+ ) (err error ) {
1245+ var (
1246+ exitCode = int32 (0 )
1247+ startTime = a .clock .Now ()
1248+ status = proto .Timing_OK
1249+ )
1250+ if err = a .containerAPI .CreateDevcontainer (dc .WorkspaceFolder ,dc .ConfigPath );err != nil {
1251+ exitCode = 1
1252+ status = proto .Timing_EXIT_FAILURE
1253+ }
1254+ endTime := a .clock .Now ()
1255+
1256+ if _ ,scriptErr := aAPI .ScriptCompleted (ctx ,& proto.WorkspaceAgentScriptCompletedRequest {
1257+ Timing :& proto.Timing {
1258+ ScriptId :script .ID [:],
1259+ Start :timestamppb .New (startTime ),
1260+ End :timestamppb .New (endTime ),
1261+ ExitCode :exitCode ,
1262+ Stage :proto .Timing_START ,
1263+ Status :status ,
1264+ },
1265+ });scriptErr != nil {
1266+ a .logger .Warn (ctx ,"reporting script completed failed" ,slog .Error (scriptErr ))
1267+ }
1268+ return err
1269+ }
1270+
12071271// createOrUpdateNetwork waits for the manifest to be set using manifestOK, then creates or updates
12081272// the tailnet using the information in the manifest
12091273func (a * agent )createOrUpdateNetwork (manifestOK ,networkOK * checkpoint )func (context.Context , proto.DRPCAgentClient26 )error {
@@ -1227,7 +1291,6 @@ func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(co
12271291// agent API.
12281292network ,err = a .createTailnet (
12291293a .gracefulCtx ,
1230- aAPI ,
12311294manifest .AgentID ,
12321295manifest .DERPMap ,
12331296manifest .DERPForceWebSockets ,
@@ -1262,9 +1325,9 @@ func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(co
12621325network .SetBlockEndpoints (manifest .DisableDirectConnections )
12631326
12641327// Update the subagent client if the container API is available.
1265- if cAPI := a .containerAPI . Load (); cAPI != nil {
1328+ if a .containerAPI != nil {
12661329client := agentcontainers .NewSubAgentClientFromAPI (a .logger ,aAPI )
1267- cAPI .UpdateSubAgentClient (client )
1330+ a . containerAPI .UpdateSubAgentClient (client )
12681331}
12691332}
12701333return nil
@@ -1382,7 +1445,6 @@ func (a *agent) trackGoroutine(fn func()) error {
13821445
13831446func (a * agent )createTailnet (
13841447ctx context.Context ,
1385- aAPI proto.DRPCAgentClient26 ,
13861448agentID uuid.UUID ,
13871449derpMap * tailcfg.DERPMap ,
13881450derpForceWebSockets ,disableDirectConnections bool ,
@@ -1515,10 +1577,7 @@ func (a *agent) createTailnet(
15151577}()
15161578if err = a .trackGoroutine (func () {
15171579defer apiListener .Close ()
1518- apiHandler ,closeAPIHAndler := a .apiHandler (aAPI )
1519- defer func () {
1520- _ = closeAPIHAndler ()
1521- }()
1580+ apiHandler := a .apiHandler ()
15221581server := & http.Server {
15231582BaseContext :func (net.Listener ) context.Context {return ctx },
15241583Handler :apiHandler ,
@@ -1532,7 +1591,6 @@ func (a *agent) createTailnet(
15321591case <- ctx .Done ():
15331592case <- a .hardCtx .Done ():
15341593}
1535- _ = closeAPIHAndler ()
15361594_ = server .Close ()
15371595}()
15381596
@@ -1871,6 +1929,12 @@ func (a *agent) Close() error {
18711929a .logger .Error (a .hardCtx ,"script runner close" ,slog .Error (err ))
18721930}
18731931
1932+ if a .containerAPI != nil {
1933+ if err := a .containerAPI .Close ();err != nil {
1934+ a .logger .Error (a .hardCtx ,"container API close" ,slog .Error (err ))
1935+ }
1936+ }
1937+
18741938// Wait for the graceful shutdown to complete, but don't wait forever so
18751939// that we don't break user expectations.
18761940go func () {