@@ -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,15 +1160,18 @@ 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
+ scriptRunnerOpts []agentscripts.InitOption
1165
+ devcontainerScripts map [uuid.UUID ]codersdk.WorkspaceAgentScript
1146
1166
)
1147
1167
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 ... ))
1168
+ a .containerAPI .Init (
1169
+ agentcontainers .WithManifestInfo (manifest .OwnerName ,manifest .WorkspaceName ),
1170
+ agentcontainers .WithDevcontainers (manifest .Devcontainers ,scripts ),
1171
+ agentcontainers .WithSubAgentClient (agentcontainers .NewSubAgentClientFromAPI (a .logger ,aAPI )),
1172
+ )
1173
+
1174
+ scripts ,devcontainerScripts = agentcontainers .ExtractDevcontainerScripts (manifest .Devcontainers ,scripts )
1153
1175
}
1154
1176
err = a .scriptRunner .Init (scripts ,aAPI .ScriptCompleted ,scriptRunnerOpts ... )
1155
1177
if err != nil {
@@ -1168,7 +1190,12 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
1168
1190
// finished (both start and post start). For instance, an
1169
1191
// autostarted devcontainer will be included in this time.
1170
1192
err := a .scriptRunner .Execute (a .gracefulCtx ,agentscripts .ExecuteStartScripts )
1171
- err = errors .Join (err ,a .scriptRunner .Execute (a .gracefulCtx ,agentscripts .ExecutePostStartScripts ))
1193
+
1194
+ for _ ,dc := range manifest .Devcontainers {
1195
+ cErr := a .createDevcontainer (ctx ,aAPI ,dc ,devcontainerScripts [dc .ID ])
1196
+ err = errors .Join (err ,cErr )
1197
+ }
1198
+
1172
1199
dur := time .Since (start ).Seconds ()
1173
1200
if err != nil {
1174
1201
a .logger .Warn (ctx ,"startup script(s) failed" ,slog .Error (err ))
@@ -1187,14 +1214,6 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
1187
1214
}
1188
1215
a .metrics .startupScriptSeconds .WithLabelValues (label ).Set (dur )
1189
1216
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
1217
})
1199
1218
if err != nil {
1200
1219
return xerrors .Errorf ("track conn goroutine: %w" ,err )
@@ -1204,6 +1223,38 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
1204
1223
}
1205
1224
}
1206
1225
1226
+ func (a * agent )createDevcontainer (
1227
+ ctx context.Context ,
1228
+ aAPI proto.DRPCAgentClient26 ,
1229
+ dc codersdk.WorkspaceAgentDevcontainer ,
1230
+ script codersdk.WorkspaceAgentScript ,
1231
+ ) (err error ) {
1232
+ var (
1233
+ exitCode = int32 (0 )
1234
+ startTime = a .clock .Now ()
1235
+ status = proto .Timing_OK
1236
+ )
1237
+ if err = a .containerAPI .CreateDevcontainer (dc .WorkspaceFolder ,dc .ConfigPath );err != nil {
1238
+ exitCode = 1
1239
+ status = proto .Timing_EXIT_FAILURE
1240
+ }
1241
+ endTime := a .clock .Now ()
1242
+
1243
+ if _ ,scriptErr := aAPI .ScriptCompleted (ctx ,& proto.WorkspaceAgentScriptCompletedRequest {
1244
+ Timing :& proto.Timing {
1245
+ ScriptId :script .ID [:],
1246
+ Start :timestamppb .New (startTime ),
1247
+ End :timestamppb .New (endTime ),
1248
+ ExitCode :exitCode ,
1249
+ Stage :proto .Timing_START ,
1250
+ Status :status ,
1251
+ },
1252
+ });scriptErr != nil {
1253
+ a .logger .Warn (ctx ,"reporting script completed failed" ,slog .Error (scriptErr ))
1254
+ }
1255
+ return err
1256
+ }
1257
+
1207
1258
// createOrUpdateNetwork waits for the manifest to be set using manifestOK, then creates or updates
1208
1259
// the tailnet using the information in the manifest
1209
1260
func (a * agent )createOrUpdateNetwork (manifestOK ,networkOK * checkpoint )func (context.Context , proto.DRPCAgentClient26 )error {
@@ -1227,7 +1278,6 @@ func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(co
1227
1278
// agent API.
1228
1279
network ,err = a .createTailnet (
1229
1280
a .gracefulCtx ,
1230
- aAPI ,
1231
1281
manifest .AgentID ,
1232
1282
manifest .DERPMap ,
1233
1283
manifest .DERPForceWebSockets ,
@@ -1262,9 +1312,9 @@ func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(co
1262
1312
network .SetBlockEndpoints (manifest .DisableDirectConnections )
1263
1313
1264
1314
// Update the subagent client if the container API is available.
1265
- if cAPI := a .containerAPI . Load (); cAPI != nil {
1315
+ if a .containerAPI != nil {
1266
1316
client := agentcontainers .NewSubAgentClientFromAPI (a .logger ,aAPI )
1267
- cAPI .UpdateSubAgentClient (client )
1317
+ a . containerAPI .UpdateSubAgentClient (client )
1268
1318
}
1269
1319
}
1270
1320
return nil
@@ -1382,7 +1432,6 @@ func (a *agent) trackGoroutine(fn func()) error {
1382
1432
1383
1433
func (a * agent )createTailnet (
1384
1434
ctx context.Context ,
1385
- aAPI proto.DRPCAgentClient26 ,
1386
1435
agentID uuid.UUID ,
1387
1436
derpMap * tailcfg.DERPMap ,
1388
1437
derpForceWebSockets ,disableDirectConnections bool ,
@@ -1515,10 +1564,7 @@ func (a *agent) createTailnet(
1515
1564
}()
1516
1565
if err = a .trackGoroutine (func () {
1517
1566
defer apiListener .Close ()
1518
- apiHandler ,closeAPIHAndler := a .apiHandler (aAPI )
1519
- defer func () {
1520
- _ = closeAPIHAndler ()
1521
- }()
1567
+ apiHandler := a .apiHandler ()
1522
1568
server := & http.Server {
1523
1569
BaseContext :func (net.Listener ) context.Context {return ctx },
1524
1570
Handler :apiHandler ,
@@ -1532,7 +1578,6 @@ func (a *agent) createTailnet(
1532
1578
case <- ctx .Done ():
1533
1579
case <- a .hardCtx .Done ():
1534
1580
}
1535
- _ = closeAPIHAndler ()
1536
1581
_ = server .Close ()
1537
1582
}()
1538
1583
@@ -1871,6 +1916,12 @@ func (a *agent) Close() error {
1871
1916
a .logger .Error (a .hardCtx ,"script runner close" ,slog .Error (err ))
1872
1917
}
1873
1918
1919
+ if a .containerAPI != nil {
1920
+ if err := a .containerAPI .Close ();err != nil {
1921
+ a .logger .Error (a .hardCtx ,"container API close" ,slog .Error (err ))
1922
+ }
1923
+ }
1924
+
1874
1925
// Wait for the graceful shutdown to complete, but don't wait forever so
1875
1926
// that we don't break user expectations.
1876
1927
go func () {