8
8
"encoding/binary"
9
9
"encoding/json"
10
10
"errors"
11
+ "flag"
11
12
"fmt"
12
13
"io"
13
14
"net"
@@ -87,6 +88,7 @@ type Client interface {
87
88
PostLifecycle (ctx context.Context , state agentsdk.PostLifecycleRequest ) error
88
89
PostAppHealth (ctx context.Context , req agentsdk.PostAppHealthsRequest ) error
89
90
PostStartup (ctx context.Context , req agentsdk.PostStartupRequest ) error
91
+ PostMetadata (ctx context.Context , req agentsdk.PostMetadataRequest ) error
90
92
}
91
93
92
94
func New (options Options ) io.Closer {
@@ -152,8 +154,8 @@ type agent struct {
152
154
closed chan struct {}
153
155
154
156
envVars map [string ]string
155
- // metadata is atomic because values can change after reconnection.
156
- metadata atomic.Value
157
+ // manifest is atomic because values can change after reconnection.
158
+ manifest atomic.Pointer [agentsdk. Manifest ]
157
159
sessionToken atomic.Pointer [string ]
158
160
sshServer * ssh.Server
159
161
@@ -178,6 +180,7 @@ type agent struct {
178
180
// failure, you'll want the agent to reconnect.
179
181
func (a * agent ) runLoop (ctx context.Context ) {
180
182
go a .reportLifecycleLoop (ctx )
183
+ go a .reportMetadataLoop (ctx )
181
184
182
185
for retrier := retry .New (100 * time .Millisecond , 10 * time .Second ); retrier .Wait (ctx ); {
183
186
a .logger .Info (ctx , "connecting to coderd" )
@@ -200,6 +203,32 @@ func (a *agent) runLoop(ctx context.Context) {
200
203
}
201
204
}
202
205
206
+ func (a * agent ) reportMetadata (ctx context.Context ) error {
207
+ ma := a .manifest .Load ()
208
+ tickers := make ([]time.Ticker , 0 , len (ma .Metadata ))
209
+ }
210
+
211
+ func (a * agent ) reportMetadataLoop (ctx context.Context ) {
212
+ // In production, the minimum report interval is one second.
213
+ ticker := time .Second
214
+ if flag .Lookup ("test.v" ) != nil {
215
+ ticker = time .Millisecond * 100
216
+ }
217
+ baseTicker := time .NewTicker (ticker )
218
+
219
+ for {
220
+ select {
221
+ case <- ctx .Done ():
222
+ return
223
+ case <- baseTicker .C :
224
+ err := a .reportMetadata (ctx )
225
+ if err != nil {
226
+ a .logger .Error (ctx , "report metadata" , slog .Error (err ))
227
+ }
228
+ }
229
+ }
230
+ }
231
+
203
232
// reportLifecycleLoop reports the current lifecycle state once.
204
233
// Only the latest state is reported, intermediate states may be
205
234
// lost if the agent can't communicate with the API.
@@ -274,30 +303,30 @@ func (a *agent) run(ctx context.Context) error {
274
303
}
275
304
a .sessionToken .Store (& sessionToken )
276
305
277
- metadata , err := a .client .Manifest (ctx )
306
+ manifest , err := a .client .Manifest (ctx )
278
307
if err != nil {
279
308
return xerrors .Errorf ("fetch metadata: %w" , err )
280
309
}
281
- a .logger .Info (ctx , "fetched metadata" , slog .F ("metadata" , metadata ))
310
+ a .logger .Info (ctx , "fetched metadata" , slog .F ("metadata" , manifest ))
282
311
283
312
// Expand the directory and send it back to coderd so external
284
313
// applications that rely on the directory can use it.
285
314
//
286
315
// An example is VS Code Remote, which must know the directory
287
316
// before initializing a connection.
288
- metadata .Directory , err = expandDirectory (metadata .Directory )
317
+ manifest .Directory , err = expandDirectory (manifest .Directory )
289
318
if err != nil {
290
319
return xerrors .Errorf ("expand directory: %w" , err )
291
320
}
292
321
err = a .client .PostStartup (ctx , agentsdk.PostStartupRequest {
293
322
Version : buildinfo .Version (),
294
- ExpandedDirectory : metadata .Directory ,
323
+ ExpandedDirectory : manifest .Directory ,
295
324
})
296
325
if err != nil {
297
326
return xerrors .Errorf ("update workspace agent version: %w" , err )
298
327
}
299
328
300
- oldMetadata := a .metadata .Swap (metadata )
329
+ oldMetadata := a .manifest .Swap (& manifest )
301
330
302
331
// The startup script should only execute on the first run!
303
332
if oldMetadata == nil {
@@ -307,7 +336,7 @@ func (a *agent) run(ctx context.Context) error {
307
336
// connect to a workspace that is not yet ready. We don't run this
308
337
// concurrently with the startup script to avoid conflicts between
309
338
// them.
310
- if metadata .GitAuthConfigs > 0 {
339
+ if manifest .GitAuthConfigs > 0 {
311
340
// If this fails, we should consider surfacing the error in the
312
341
// startup log and setting the lifecycle state to be "start_error"
313
342
// (after startup script completion), but for now we'll just log it.
@@ -322,7 +351,7 @@ func (a *agent) run(ctx context.Context) error {
322
351
scriptStart := time .Now ()
323
352
err = a .trackConnGoroutine (func () {
324
353
defer close (scriptDone )
325
- scriptDone <- a .runStartupScript (ctx , metadata .StartupScript )
354
+ scriptDone <- a .runStartupScript (ctx , manifest .StartupScript )
326
355
})
327
356
if err != nil {
328
357
return xerrors .Errorf ("track startup script: %w" , err )
@@ -331,8 +360,8 @@ func (a *agent) run(ctx context.Context) error {
331
360
var timeout <- chan time.Time
332
361
// If timeout is zero, an older version of the coder
333
362
// provider was used. Otherwise a timeout is always > 0.
334
- if metadata .StartupScriptTimeout > 0 {
335
- t := time .NewTimer (metadata .StartupScriptTimeout )
363
+ if manifest .StartupScriptTimeout > 0 {
364
+ t := time .NewTimer (manifest .StartupScriptTimeout )
336
365
defer t .Stop ()
337
366
timeout = t .C
338
367
}
@@ -349,7 +378,7 @@ func (a *agent) run(ctx context.Context) error {
349
378
return
350
379
}
351
380
// Only log if there was a startup script.
352
- if metadata .StartupScript != "" {
381
+ if manifest .StartupScript != "" {
353
382
execTime := time .Since (scriptStart )
354
383
if err != nil {
355
384
a .logger .Warn (ctx , "startup script failed" , slog .F ("execution_time" , execTime ), slog .Error (err ))
@@ -366,13 +395,13 @@ func (a *agent) run(ctx context.Context) error {
366
395
appReporterCtx , appReporterCtxCancel := context .WithCancel (ctx )
367
396
defer appReporterCtxCancel ()
368
397
go NewWorkspaceAppHealthReporter (
369
- a .logger , metadata .Apps , a .client .PostAppHealth )(appReporterCtx )
398
+ a .logger , manifest .Apps , a .client .PostAppHealth )(appReporterCtx )
370
399
371
400
a .closeMutex .Lock ()
372
401
network := a .network
373
402
a .closeMutex .Unlock ()
374
403
if network == nil {
375
- network , err = a .createTailnet (ctx , metadata .DERPMap )
404
+ network , err = a .createTailnet (ctx , manifest .DERPMap )
376
405
if err != nil {
377
406
return xerrors .Errorf ("create tailnet: %w" , err )
378
407
}
@@ -391,7 +420,7 @@ func (a *agent) run(ctx context.Context) error {
391
420
a .startReportingConnectionStats (ctx )
392
421
} else {
393
422
// Update the DERP map!
394
- network .SetDERPMap (metadata .DERPMap )
423
+ network .SetDERPMap (manifest .DERPMap )
395
424
}
396
425
397
426
a .logger .Debug (ctx , "running tailnet connection coordinator" )
@@ -800,14 +829,10 @@ func (a *agent) createCommand(ctx context.Context, rawCommand string, env []stri
800
829
return nil , xerrors .Errorf ("get user shell: %w" , err )
801
830
}
802
831
803
- rawMetadata := a .metadata .Load ()
804
- if rawMetadata == nil {
832
+ manifest := a .manifest .Load ()
833
+ if manifest == nil {
805
834
return nil , xerrors .Errorf ("no metadata was provided" )
806
835
}
807
- metadata , valid := rawMetadata .(agentsdk.Manifest )
808
- if ! valid {
809
- return nil , xerrors .Errorf ("metadata is the wrong type: %T" , metadata )
810
- }
811
836
812
837
// OpenSSH executes all commands with the users current shell.
813
838
// We replicate that behavior for IDE support.
@@ -829,7 +854,7 @@ func (a *agent) createCommand(ctx context.Context, rawCommand string, env []stri
829
854
}
830
855
831
856
cmd := exec .CommandContext (ctx , shell , args ... )
832
- cmd .Dir = metadata .Directory
857
+ cmd .Dir = manifest .Directory
833
858
834
859
// If the metadata directory doesn't exist, we run the command
835
860
// in the users home directory.
@@ -870,14 +895,14 @@ func (a *agent) createCommand(ctx context.Context, rawCommand string, env []stri
870
895
871
896
// This adds the ports dialog to code-server that enables
872
897
// proxying a port dynamically.
873
- cmd .Env = append (cmd .Env , fmt .Sprintf ("VSCODE_PROXY_URI=%s" , metadata .VSCodePortProxyURI ))
898
+ cmd .Env = append (cmd .Env , fmt .Sprintf ("VSCODE_PROXY_URI=%s" , manifest .VSCodePortProxyURI ))
874
899
875
900
// Hide Coder message on code-server's "Getting Started" page
876
901
cmd .Env = append (cmd .Env , "CS_DISABLE_GETTING_STARTED_OVERRIDE=true" )
877
902
878
903
// Load environment variables passed via the agent.
879
904
// These should override all variables we manually specify.
880
- for envKey , value := range metadata .EnvironmentVariables {
905
+ for envKey , value := range manifest .EnvironmentVariables {
881
906
// Expanding environment variables allows for customization
882
907
// of the $PATH, among other variables. Customers can prepend
883
908
// or append to the $PATH, so allowing expand is required!
@@ -940,9 +965,9 @@ func (a *agent) handleSSHSession(session ssh.Session) (retErr error) {
940
965
session .DisablePTYEmulation ()
941
966
942
967
if ! isQuietLogin (session .RawCommand ()) {
943
- metadata , ok := a .metadata .Load ().(agentsdk. Manifest )
944
- if ok {
945
- err = showMOTD (session , metadata .MOTDFile )
968
+ manifest := a .manifest .Load ()
969
+ if manifest != nil {
970
+ err = showMOTD (session , manifest .MOTDFile )
946
971
if err != nil {
947
972
a .logger .Error (ctx , "show MOTD" , slog .Error (err ))
948
973
}
@@ -1330,19 +1355,19 @@ func (a *agent) Close() error {
1330
1355
a .setLifecycle (ctx , codersdk .WorkspaceAgentLifecycleShuttingDown )
1331
1356
1332
1357
lifecycleState := codersdk .WorkspaceAgentLifecycleOff
1333
- if metadata , ok := a .metadata .Load ().(agentsdk. Manifest ); ok && metadata .ShutdownScript != "" {
1358
+ if manifest := a .manifest .Load (); manifest != nil && manifest .ShutdownScript != "" {
1334
1359
scriptDone := make (chan error , 1 )
1335
1360
scriptStart := time .Now ()
1336
1361
go func () {
1337
1362
defer close (scriptDone )
1338
- scriptDone <- a .runShutdownScript (ctx , metadata .ShutdownScript )
1363
+ scriptDone <- a .runShutdownScript (ctx , manifest .ShutdownScript )
1339
1364
}()
1340
1365
1341
1366
var timeout <- chan time.Time
1342
1367
// If timeout is zero, an older version of the coder
1343
1368
// provider was used. Otherwise a timeout is always > 0.
1344
- if metadata .ShutdownScriptTimeout > 0 {
1345
- t := time .NewTimer (metadata .ShutdownScriptTimeout )
1369
+ if manifest .ShutdownScriptTimeout > 0 {
1370
+ t := time .NewTimer (manifest .ShutdownScriptTimeout )
1346
1371
defer t .Stop ()
1347
1372
timeout = t .C
1348
1373
}
0 commit comments