@@ -91,6 +91,7 @@ type Options struct {
9191 Execer agentexec.Execer
9292 Devcontainers bool
9393 DevcontainerAPIOptions []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 {
144145 if options .PortCacheDuration == 0 {
145146 options .PortCacheDuration = 1 * time .Second
146147 }
148+ if options .Clock == nil {
149+ options .Clock = quartz .NewReal ()
150+ }
147151
148152 prometheusRegistry := options .PrometheusRegistry
149153 if prometheusRegistry == nil {
@@ -157,6 +161,7 @@ func New(options Options) Agent {
157161 hardCtx , hardCancel := context .WithCancel (context .Background ())
158162 gracefulCtx , gracefulCancel := context .WithCancel (hardCtx )
159163 a := & agent {
164+ clock : options .Clock ,
160165 tailnetListenPort : options .TailnetListenPort ,
161166 reconnectingPTYTimeout : options .ReconnectingPTYTimeout ,
162167 logger : options .Logger ,
@@ -204,6 +209,7 @@ func New(options Options) Agent {
204209}
205210
206211type agent struct {
212+ clock quartz.Clock
207213 logger slog.Logger
208214 client Client
209215 exchangeToken func (ctx context.Context ) (string , error )
@@ -273,7 +279,7 @@ type agent struct {
273279
274280 devcontainers bool
275281 containerAPIOptions []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.
331337 a .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+
333352 a .reconnectingPTYServer = reconnectingpty .NewServer (
334353 a .logger .Named ("reconnecting-pty" ),
335354 a .sshServer ,
@@ -1141,17 +1160,27 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
11411160 }
11421161
11431162 var (
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 )
11551184 if err != nil {
11561185 return 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.
11701199 err := 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+
11721212 dur := time .Since (start ).Seconds ()
11731213 if err != nil {
11741214 a .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 }
11881228 a .metrics .startupScriptSeconds .WithLabelValues (label ).Set (dur )
11891229 a .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 })
11991231 if err != nil {
12001232 return 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.
12281292 network , err = a .createTailnet (
12291293 a .gracefulCtx ,
1230- aAPI ,
12311294 manifest .AgentID ,
12321295 manifest .DERPMap ,
12331296 manifest .DERPForceWebSockets ,
@@ -1262,9 +1325,9 @@ func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(co
12621325 network .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 {
12661329 client := agentcontainers .NewSubAgentClientFromAPI (a .logger , aAPI )
1267- cAPI .UpdateSubAgentClient (client )
1330+ a . containerAPI .UpdateSubAgentClient (client )
12681331 }
12691332 }
12701333 return nil
@@ -1382,7 +1445,6 @@ func (a *agent) trackGoroutine(fn func()) error {
13821445
13831446func (a * agent ) createTailnet (
13841447 ctx context.Context ,
1385- aAPI proto.DRPCAgentClient26 ,
13861448 agentID uuid.UUID ,
13871449 derpMap * tailcfg.DERPMap ,
13881450 derpForceWebSockets , disableDirectConnections bool ,
@@ -1515,10 +1577,7 @@ func (a *agent) createTailnet(
15151577 }()
15161578 if err = a .trackGoroutine (func () {
15171579 defer apiListener .Close ()
1518- apiHandler , closeAPIHAndler := a .apiHandler (aAPI )
1519- defer func () {
1520- _ = closeAPIHAndler ()
1521- }()
1580+ apiHandler := a .apiHandler ()
15221581 server := & http.Server {
15231582 BaseContext : func (net.Listener ) context.Context { return ctx },
15241583 Handler : apiHandler ,
@@ -1532,7 +1591,6 @@ func (a *agent) createTailnet(
15321591 case <- ctx .Done ():
15331592 case <- a .hardCtx .Done ():
15341593 }
1535- _ = closeAPIHAndler ()
15361594 _ = server .Close ()
15371595 }()
15381596
@@ -1871,6 +1929,12 @@ func (a *agent) Close() error {
18711929 a .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.
18761940 go func () {
0 commit comments