@@ -91,6 +91,7 @@ type Options struct {
91
91
Execer agentexec.Execer
92
92
Devcontainers bool
93
93
DevcontainerAPIOptions []agentcontainers.Option // Enable Devcontainers for these to be effective.
94
+ Clock quartz.Clock
94
95
}
95
96
96
97
type Client interface {
@@ -144,6 +145,9 @@ func New(options Options) Agent {
144
145
if options .PortCacheDuration == 0 {
145
146
options .PortCacheDuration = 1 * time .Second
146
147
}
148
+ if options .Clock == nil {
149
+ options .Clock = quartz .NewReal ()
150
+ }
147
151
148
152
prometheusRegistry := options .PrometheusRegistry
149
153
if prometheusRegistry == nil {
@@ -157,6 +161,7 @@ func New(options Options) Agent {
157
161
hardCtx , hardCancel := context .WithCancel (context .Background ())
158
162
gracefulCtx , gracefulCancel := context .WithCancel (hardCtx )
159
163
a := & agent {
164
+ clock : options .Clock ,
160
165
tailnetListenPort : options .TailnetListenPort ,
161
166
reconnectingPTYTimeout : options .ReconnectingPTYTimeout ,
162
167
logger : options .Logger ,
@@ -204,6 +209,7 @@ func New(options Options) Agent {
204
209
}
205
210
206
211
type agent struct {
212
+ clock quartz.Clock
207
213
logger slog.Logger
208
214
client Client
209
215
exchangeToken func (ctx context.Context ) (string , error )
@@ -273,7 +279,7 @@ type agent struct {
273
279
274
280
devcontainers bool
275
281
containerAPIOptions []agentcontainers.Option
276
- containerAPI atomic. Pointer [ agentcontainers.API ] // Set by apiHandler.
282
+ containerAPI * agentcontainers.API
277
283
}
278
284
279
285
func (a * agent ) TailnetConn () * tailnet.Conn {
@@ -330,6 +336,19 @@ func (a *agent) init() {
330
336
// will not report anywhere.
331
337
a .scriptRunner .RegisterMetrics (a .prometheusRegistry )
332
338
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
+
333
352
a .reconnectingPTYServer = reconnectingpty .NewServer (
334
353
a .logger .Named ("reconnecting-pty" ),
335
354
a .sshServer ,
@@ -1141,17 +1160,27 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
1141
1160
}
1142
1161
1143
1162
var (
1144
- scripts = manifest .Scripts
1145
- scriptRunnerOpts []agentscripts. InitOption
1163
+ scripts = manifest .Scripts
1164
+ devcontainerScripts map [uuid. UUID ]codersdk. WorkspaceAgentScript
1146
1165
)
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 )
1153
1182
}
1154
- err = a .scriptRunner .Init (scripts , aAPI .ScriptCompleted , scriptRunnerOpts ... )
1183
+ err = a .scriptRunner .Init (scripts , aAPI .ScriptCompleted )
1155
1184
if err != nil {
1156
1185
return xerrors .Errorf ("init script runner: %w" , err )
1157
1186
}
@@ -1168,7 +1197,18 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
1168
1197
// finished (both start and post start). For instance, an
1169
1198
// autostarted devcontainer will be included in this time.
1170
1199
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
+
1172
1212
dur := time .Since (start ).Seconds ()
1173
1213
if err != nil {
1174
1214
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,
1187
1227
}
1188
1228
a .metrics .startupScriptSeconds .WithLabelValues (label ).Set (dur )
1189
1229
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
- }
1198
1230
})
1199
1231
if err != nil {
1200
1232
return xerrors .Errorf ("track conn goroutine: %w" , err )
@@ -1204,6 +1236,38 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
1204
1236
}
1205
1237
}
1206
1238
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
+
1207
1271
// createOrUpdateNetwork waits for the manifest to be set using manifestOK, then creates or updates
1208
1272
// the tailnet using the information in the manifest
1209
1273
func (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
1227
1291
// agent API.
1228
1292
network , err = a .createTailnet (
1229
1293
a .gracefulCtx ,
1230
- aAPI ,
1231
1294
manifest .AgentID ,
1232
1295
manifest .DERPMap ,
1233
1296
manifest .DERPForceWebSockets ,
@@ -1262,9 +1325,9 @@ func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(co
1262
1325
network .SetBlockEndpoints (manifest .DisableDirectConnections )
1263
1326
1264
1327
// Update the subagent client if the container API is available.
1265
- if cAPI := a .containerAPI . Load (); cAPI != nil {
1328
+ if a .containerAPI != nil {
1266
1329
client := agentcontainers .NewSubAgentClientFromAPI (a .logger , aAPI )
1267
- cAPI .UpdateSubAgentClient (client )
1330
+ a . containerAPI .UpdateSubAgentClient (client )
1268
1331
}
1269
1332
}
1270
1333
return nil
@@ -1382,7 +1445,6 @@ func (a *agent) trackGoroutine(fn func()) error {
1382
1445
1383
1446
func (a * agent ) createTailnet (
1384
1447
ctx context.Context ,
1385
- aAPI proto.DRPCAgentClient26 ,
1386
1448
agentID uuid.UUID ,
1387
1449
derpMap * tailcfg.DERPMap ,
1388
1450
derpForceWebSockets , disableDirectConnections bool ,
@@ -1515,10 +1577,7 @@ func (a *agent) createTailnet(
1515
1577
}()
1516
1578
if err = a .trackGoroutine (func () {
1517
1579
defer apiListener .Close ()
1518
- apiHandler , closeAPIHAndler := a .apiHandler (aAPI )
1519
- defer func () {
1520
- _ = closeAPIHAndler ()
1521
- }()
1580
+ apiHandler := a .apiHandler ()
1522
1581
server := & http.Server {
1523
1582
BaseContext : func (net.Listener ) context.Context { return ctx },
1524
1583
Handler : apiHandler ,
@@ -1532,7 +1591,6 @@ func (a *agent) createTailnet(
1532
1591
case <- ctx .Done ():
1533
1592
case <- a .hardCtx .Done ():
1534
1593
}
1535
- _ = closeAPIHAndler ()
1536
1594
_ = server .Close ()
1537
1595
}()
1538
1596
@@ -1871,6 +1929,12 @@ func (a *agent) Close() error {
1871
1929
a .logger .Error (a .hardCtx , "script runner close" , slog .Error (err ))
1872
1930
}
1873
1931
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
+
1874
1938
// Wait for the graceful shutdown to complete, but don't wait forever so
1875
1939
// that we don't break user expectations.
1876
1940
go func () {
0 commit comments