@@ -844,31 +844,10 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R
844
844
return
845
845
}
846
846
847
- // Accept a resume_token query parameter to use the same peer ID.
848
- var (
849
- peerID = uuid .New ()
850
- resumeToken = r .URL .Query ().Get ("resume_token" )
851
- )
852
- if resumeToken != "" {
853
- var err error
854
- peerID , err = api .Options .CoordinatorResumeTokenProvider .VerifyResumeToken (ctx , resumeToken )
855
- // If the token is missing the key ID, it's probably an old token in which
856
- // case we just want to generate a new peer ID.
857
- if xerrors .Is (err , jwtutils .ErrMissingKeyID ) {
858
- peerID = uuid .New ()
859
- } else if err != nil {
860
- httpapi .Write (ctx , rw , http .StatusUnauthorized , codersdk.Response {
861
- Message : workspacesdk .CoordinateAPIInvalidResumeToken ,
862
- Detail : err .Error (),
863
- Validations : []codersdk.ValidationError {
864
- {Field : "resume_token" , Detail : workspacesdk .CoordinateAPIInvalidResumeToken },
865
- },
866
- })
867
- return
868
- } else {
869
- api .Logger .Debug (ctx , "accepted coordinate resume token for peer" ,
870
- slog .F ("peer_id" , peerID .String ()))
871
- }
847
+ peerID , err := api .handleResumeToken (ctx , rw , r )
848
+ if err != nil {
849
+ // handleResumeToken has already written the response.
850
+ return
872
851
}
873
852
874
853
api .WebsocketWaitMutex .Lock ()
@@ -898,6 +877,33 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R
898
877
}
899
878
}
900
879
880
+ // handleResumeToken accepts a resume_token query parameter to use the same peer ID
881
+ func (api * API ) handleResumeToken (ctx context.Context , rw http.ResponseWriter , r * http.Request ) (peerID uuid.UUID , err error ) {
882
+ peerID = uuid .New ()
883
+ resumeToken := r .URL .Query ().Get ("resume_token" )
884
+ if resumeToken != "" {
885
+ peerID , err = api .Options .CoordinatorResumeTokenProvider .VerifyResumeToken (ctx , resumeToken )
886
+ // If the token is missing the key ID, it's probably an old token in which
887
+ // case we just want to generate a new peer ID.
888
+ if xerrors .Is (err , jwtutils .ErrMissingKeyID ) {
889
+ peerID = uuid .New ()
890
+ } else if err != nil {
891
+ httpapi .Write (ctx , rw , http .StatusUnauthorized , codersdk.Response {
892
+ Message : workspacesdk .CoordinateAPIInvalidResumeToken ,
893
+ Detail : err .Error (),
894
+ Validations : []codersdk.ValidationError {
895
+ {Field : "resume_token" , Detail : workspacesdk .CoordinateAPIInvalidResumeToken },
896
+ },
897
+ })
898
+ return
899
+ } else {
900
+ api .Logger .Debug (ctx , "accepted coordinate resume token for peer" ,
901
+ slog .F ("peer_id" , peerID .String ()))
902
+ }
903
+ }
904
+ return peerID , err
905
+ }
906
+
901
907
// @Summary Post workspace agent log source
902
908
// @ID post-workspace-agent-log-source
903
909
// @Security CoderSessionToken
@@ -1469,6 +1475,72 @@ func (api *API) workspaceAgentsExternalAuthListen(ctx context.Context, rw http.R
1469
1475
}
1470
1476
}
1471
1477
1478
+ // @Summary Coordinate multiple workspace agents
1479
+ // @ID coordinate-multiple-workspace-agents
1480
+ // @Security CoderSessionToken
1481
+ // @Tags Agents
1482
+ // @Success 101
1483
+ // @Router /users/me/tailnet [get]
1484
+ func (api * API ) tailnet (rw http.ResponseWriter , r * http.Request ) {
1485
+ ctx := r .Context ()
1486
+ apiKey , ok := httpmw .APIKeyOptional (r )
1487
+ if ! ok {
1488
+ httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
1489
+ Message : "Cannot use \" me\" without a valid session." ,
1490
+ })
1491
+ return
1492
+ }
1493
+
1494
+ version := "2.0"
1495
+ qv := r .URL .Query ().Get ("version" )
1496
+ if qv != "" {
1497
+ version = qv
1498
+ }
1499
+ if err := proto .CurrentVersion .Validate (version ); err != nil {
1500
+ httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
1501
+ Message : "Unknown or unsupported API version" ,
1502
+ Validations : []codersdk.ValidationError {
1503
+ {Field : "version" , Detail : err .Error ()},
1504
+ },
1505
+ })
1506
+ return
1507
+ }
1508
+
1509
+ peerID , err := api .handleResumeToken (ctx , rw , r )
1510
+ if err != nil {
1511
+ // handleResumeToken has already written the response.
1512
+ return
1513
+ }
1514
+
1515
+ api .WebsocketWaitMutex .Lock ()
1516
+ api .WebsocketWaitGroup .Add (1 )
1517
+ api .WebsocketWaitMutex .Unlock ()
1518
+ defer api .WebsocketWaitGroup .Done ()
1519
+
1520
+ conn , err := websocket .Accept (rw , r , nil )
1521
+ if err != nil {
1522
+ httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
1523
+ Message : "Failed to accept websocket." ,
1524
+ Detail : err .Error (),
1525
+ })
1526
+ return
1527
+ }
1528
+ ctx , wsNetConn := codersdk .WebsocketNetConn (ctx , conn , websocket .MessageBinary )
1529
+ defer wsNetConn .Close ()
1530
+ defer conn .Close (websocket .StatusNormalClosure , "" )
1531
+
1532
+ go httpapi .Heartbeat (ctx , conn )
1533
+ err = api .TailnetClientService .ServeUserClient (ctx , version , wsNetConn , tailnet.ServeUserClientOptions {
1534
+ PeerID : peerID ,
1535
+ UserID : apiKey .UserID ,
1536
+ UpdatesProvider : api .WorkspaceUpdatesProvider ,
1537
+ })
1538
+ if err != nil && ! xerrors .Is (err , io .EOF ) && ! xerrors .Is (err , context .Canceled ) {
1539
+ _ = conn .Close (websocket .StatusInternalError , err .Error ())
1540
+ return
1541
+ }
1542
+ }
1543
+
1472
1544
// createExternalAuthResponse creates an ExternalAuthResponse based on the
1473
1545
// provider type. This is to support legacy `/workspaceagents/me/gitauth`
1474
1546
// which uses `Username` and `Password`.
0 commit comments