Skip to content

Commit 63c4298

Browse files
committed
feat: add WorkspaceUpdates rpc
1 parent fabe2d1 commit 63c4298

20 files changed

+1476
-232
lines changed

coderd/apidoc/docs.go

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,7 @@ func New(options *Options) *API {
10101010
r.Route("/roles", func(r chi.Router) {
10111011
r.Get("/", api.AssignableSiteRoles)
10121012
})
1013+
r.Get("/me/tailnet", api.tailnet)
10131014
r.Route("/{user}", func(r chi.Router) {
10141015
r.Use(httpmw.ExtractUserParam(options.Database))
10151016
r.Post("/convert-login", api.postConvertLoginType)

coderd/workspaceagents.go

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -856,26 +856,10 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R
856856
return
857857
}
858858

859-
// Accept a resume_token query parameter to use the same peer ID.
860-
var (
861-
peerID = uuid.New()
862-
resumeToken = r.URL.Query().Get("resume_token")
863-
)
864-
if resumeToken != "" {
865-
var err error
866-
peerID, err = api.Options.CoordinatorResumeTokenProvider.VerifyResumeToken(resumeToken)
867-
if err != nil {
868-
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
869-
Message: workspacesdk.CoordinateAPIInvalidResumeToken,
870-
Detail: err.Error(),
871-
Validations: []codersdk.ValidationError{
872-
{Field: "resume_token", Detail: workspacesdk.CoordinateAPIInvalidResumeToken},
873-
},
874-
})
875-
return
876-
}
877-
api.Logger.Debug(ctx, "accepted coordinate resume token for peer",
878-
slog.F("peer_id", peerID.String()))
859+
peerID, err := api.handleResumeToken(ctx, rw, r)
860+
if err != nil {
861+
// handleResumeToken has already written the response.
862+
return
879863
}
880864

881865
api.WebsocketWaitMutex.Lock()
@@ -905,6 +889,28 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R
905889
}
906890
}
907891

892+
// handleResumeToken accepts a resume_token query parameter to use the same peer ID
893+
func (api *API) handleResumeToken(ctx context.Context, rw http.ResponseWriter, r *http.Request) (peerID uuid.UUID, err error) {
894+
peerID = uuid.New()
895+
resumeToken := r.URL.Query().Get("resume_token")
896+
if resumeToken != "" {
897+
peerID, err = api.Options.CoordinatorResumeTokenProvider.VerifyResumeToken(resumeToken)
898+
if err != nil {
899+
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
900+
Message: workspacesdk.CoordinateAPIInvalidResumeToken,
901+
Detail: err.Error(),
902+
Validations: []codersdk.ValidationError{
903+
{Field: "resume_token", Detail: workspacesdk.CoordinateAPIInvalidResumeToken},
904+
},
905+
})
906+
return peerID, err
907+
}
908+
api.Logger.Debug(ctx, "accepted coordinate resume token for peer",
909+
slog.F("peer_id", peerID.String()))
910+
}
911+
return peerID, err
912+
}
913+
908914
// @Summary Post workspace agent log source
909915
// @ID post-workspace-agent-log-source
910916
// @Security CoderSessionToken

coderd/workspacebuilds.go

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -961,7 +961,7 @@ func (api *API) convertWorkspaceBuild(
961961
MaxDeadline: codersdk.NewNullTime(build.MaxDeadline, !build.MaxDeadline.IsZero()),
962962
Reason: codersdk.BuildReason(build.Reason),
963963
Resources: apiResources,
964-
Status: convertWorkspaceStatus(apiJob.Status, transition),
964+
Status: codersdk.ConvertWorkspaceStatus(apiJob.Status, transition),
965965
DailyCost: build.DailyCost,
966966
}, nil
967967
}
@@ -990,37 +990,3 @@ func convertWorkspaceResource(resource database.WorkspaceResource, agents []code
990990
DailyCost: resource.DailyCost,
991991
}
992992
}
993-
994-
func convertWorkspaceStatus(jobStatus codersdk.ProvisionerJobStatus, transition codersdk.WorkspaceTransition) codersdk.WorkspaceStatus {
995-
switch jobStatus {
996-
case codersdk.ProvisionerJobPending:
997-
return codersdk.WorkspaceStatusPending
998-
case codersdk.ProvisionerJobRunning:
999-
switch transition {
1000-
case codersdk.WorkspaceTransitionStart:
1001-
return codersdk.WorkspaceStatusStarting
1002-
case codersdk.WorkspaceTransitionStop:
1003-
return codersdk.WorkspaceStatusStopping
1004-
case codersdk.WorkspaceTransitionDelete:
1005-
return codersdk.WorkspaceStatusDeleting
1006-
}
1007-
case codersdk.ProvisionerJobSucceeded:
1008-
switch transition {
1009-
case codersdk.WorkspaceTransitionStart:
1010-
return codersdk.WorkspaceStatusRunning
1011-
case codersdk.WorkspaceTransitionStop:
1012-
return codersdk.WorkspaceStatusStopped
1013-
case codersdk.WorkspaceTransitionDelete:
1014-
return codersdk.WorkspaceStatusDeleted
1015-
}
1016-
case codersdk.ProvisionerJobCanceling:
1017-
return codersdk.WorkspaceStatusCanceling
1018-
case codersdk.ProvisionerJobCanceled:
1019-
return codersdk.WorkspaceStatusCanceled
1020-
case codersdk.ProvisionerJobFailed:
1021-
return codersdk.WorkspaceStatusFailed
1022-
}
1023-
1024-
// return error status since we should never get here
1025-
return codersdk.WorkspaceStatusFailed
1026-
}

coderd/workspaces.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"errors"
88
"fmt"
9+
"io"
910
"net/http"
1011
"slices"
1112
"strconv"
@@ -15,6 +16,7 @@ import (
1516
"github.com/go-chi/chi/v5"
1617
"github.com/google/uuid"
1718
"golang.org/x/xerrors"
19+
"nhooyr.io/websocket"
1820

1921
"cdr.dev/slog"
2022
"github.com/coder/coder/v2/agent/proto"
@@ -37,6 +39,7 @@ import (
3739
"github.com/coder/coder/v2/coderd/wspubsub"
3840
"github.com/coder/coder/v2/codersdk"
3941
"github.com/coder/coder/v2/codersdk/agentsdk"
42+
"github.com/coder/coder/v2/tailnet"
4043
)
4144

4245
var (
@@ -2090,6 +2093,11 @@ func (api *API) publishWorkspaceUpdate(ctx context.Context, ownerID uuid.UUID, e
20902093
api.Logger.Warn(ctx, "failed to publish workspace update",
20912094
slog.F("workspace_id", event.WorkspaceID), slog.Error(err))
20922095
}
2096+
err = api.Pubsub.Publish(codersdk.AllWorkspacesNotifyChannel, []byte(workspaceID.String()))
2097+
if err != nil {
2098+
api.Logger.Warn(ctx, "failed to publish all workspaces update",
2099+
slog.F("workspace_id", workspaceID), slog.Error(err))
2100+
}
20932101
}
20942102

20952103
func (api *API) publishWorkspaceAgentLogsUpdate(ctx context.Context, workspaceAgentID uuid.UUID, m agentsdk.LogsNotifyMessage) {
@@ -2102,3 +2110,72 @@ func (api *API) publishWorkspaceAgentLogsUpdate(ctx context.Context, workspaceAg
21022110
api.Logger.Warn(ctx, "failed to publish workspace agent logs update", slog.F("workspace_agent_id", workspaceAgentID), slog.Error(err))
21032111
}
21042112
}
2113+
2114+
// @Summary Coordinate multiple workspace agents
2115+
// @ID coordinate-multiple-workspace-agents
2116+
// @Security CoderSessionToken
2117+
// @Tags Workspaces
2118+
// @Success 101
2119+
// @Router /users/me/tailnet [get]
2120+
func (api *API) tailnet(rw http.ResponseWriter, r *http.Request) {
2121+
ctx := r.Context()
2122+
owner := httpmw.UserParam(r)
2123+
ownerRoles := httpmw.UserAuthorization(r)
2124+
2125+
// Check if the actor is allowed to access any workspace owned by the user.
2126+
if !api.Authorize(r, policy.ActionSSH, rbac.ResourceWorkspace.WithOwner(owner.ID.String())) {
2127+
httpapi.ResourceNotFound(rw)
2128+
return
2129+
}
2130+
2131+
version := "1.0"
2132+
qv := r.URL.Query().Get("version")
2133+
if qv != "" {
2134+
version = qv
2135+
}
2136+
if err := proto.CurrentVersion.Validate(version); err != nil {
2137+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
2138+
Message: "Unknown or unsupported API version",
2139+
Validations: []codersdk.ValidationError{
2140+
{Field: "version", Detail: err.Error()},
2141+
},
2142+
})
2143+
return
2144+
}
2145+
2146+
peerID, err := api.handleResumeToken(ctx, rw, r)
2147+
if err != nil {
2148+
// handleResumeToken has already written the response.
2149+
return
2150+
}
2151+
2152+
api.WebsocketWaitMutex.Lock()
2153+
api.WebsocketWaitGroup.Add(1)
2154+
api.WebsocketWaitMutex.Unlock()
2155+
defer api.WebsocketWaitGroup.Done()
2156+
2157+
conn, err := websocket.Accept(rw, r, nil)
2158+
if err != nil {
2159+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
2160+
Message: "Failed to accept websocket.",
2161+
Detail: err.Error(),
2162+
})
2163+
return
2164+
}
2165+
ctx, wsNetConn := codersdk.WebsocketNetConn(ctx, conn, websocket.MessageBinary)
2166+
defer wsNetConn.Close()
2167+
defer conn.Close(websocket.StatusNormalClosure, "")
2168+
2169+
go httpapi.Heartbeat(ctx, conn)
2170+
err = api.TailnetClientService.ServeUserClient(ctx, version, wsNetConn, tailnet.ServeUserClientOptions{
2171+
PeerID: peerID,
2172+
UserID: owner.ID,
2173+
Subject: &ownerRoles,
2174+
Authz: api.Authorizer,
2175+
Database: api.Database,
2176+
})
2177+
if err != nil && !xerrors.Is(err, io.EOF) && !xerrors.Is(err, context.Canceled) {
2178+
_ = conn.Close(websocket.StatusInternalError, err.Error())
2179+
return
2180+
}
2181+
}

codersdk/provisionerdaemons.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,3 +402,37 @@ func (c *Client) DeleteProvisionerKey(ctx context.Context, organizationID uuid.U
402402
}
403403
return nil
404404
}
405+
406+
func ConvertWorkspaceStatus(jobStatus ProvisionerJobStatus, transition WorkspaceTransition) WorkspaceStatus {
407+
switch jobStatus {
408+
case ProvisionerJobPending:
409+
return WorkspaceStatusPending
410+
case ProvisionerJobRunning:
411+
switch transition {
412+
case WorkspaceTransitionStart:
413+
return WorkspaceStatusStarting
414+
case WorkspaceTransitionStop:
415+
return WorkspaceStatusStopping
416+
case WorkspaceTransitionDelete:
417+
return WorkspaceStatusDeleting
418+
}
419+
case ProvisionerJobSucceeded:
420+
switch transition {
421+
case WorkspaceTransitionStart:
422+
return WorkspaceStatusRunning
423+
case WorkspaceTransitionStop:
424+
return WorkspaceStatusStopped
425+
case WorkspaceTransitionDelete:
426+
return WorkspaceStatusDeleted
427+
}
428+
case ProvisionerJobCanceling:
429+
return WorkspaceStatusCanceling
430+
case ProvisionerJobCanceled:
431+
return WorkspaceStatusCanceled
432+
case ProvisionerJobFailed:
433+
return WorkspaceStatusFailed
434+
}
435+
436+
// return error status since we should never get here
437+
return WorkspaceStatusFailed
438+
}

codersdk/workspacesdk/connector_internal_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,11 @@ func (f *fakeDRPCClient) RefreshResumeToken(_ context.Context, _ *proto.RefreshR
571571
}, nil
572572
}
573573

574+
// WorkspaceUpdates implements proto.DRPCTailnetClient.
575+
func (*fakeDRPCClient) WorkspaceUpdates(context.Context, *proto.WorkspaceUpdatesRequest) (proto.DRPCTailnet_WorkspaceUpdatesClient, error) {
576+
panic("unimplemented")
577+
}
578+
574579
type fakeDRPCConn struct{}
575580

576581
var _ drpc.Conn = &fakeDRPCConn{}

docs/reference/api/workspaces.md

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

enterprise/tailnet/connio.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ var errDisconnect = xerrors.New("graceful disconnect")
133133

134134
func (c *connIO) handleRequest(req *proto.CoordinateRequest) error {
135135
c.logger.Debug(c.peerCtx, "got request")
136-
err := c.auth.Authorize(req)
136+
err := c.auth.Authorize(c.coordCtx, req)
137137
if err != nil {
138138
c.logger.Warn(c.peerCtx, "unauthorized request", slog.Error(err))
139139
return xerrors.Errorf("authorize request: %w", err)

tailnet/convert.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"tailscale.com/tailcfg"
1010
"tailscale.com/types/key"
1111

12+
"github.com/coder/coder/v2/codersdk"
1213
"github.com/coder/coder/v2/tailnet/proto"
1314
)
1415

@@ -270,3 +271,30 @@ func DERPNodeFromProto(node *proto.DERPMap_Region_Node) *tailcfg.DERPNode {
270271
CanPort80: node.CanPort_80,
271272
}
272273
}
274+
275+
func WorkspaceStatusToProto(status codersdk.WorkspaceStatus) proto.Workspace_Status {
276+
switch status {
277+
case codersdk.WorkspaceStatusCanceled:
278+
return proto.Workspace_CANCELED
279+
case codersdk.WorkspaceStatusCanceling:
280+
return proto.Workspace_CANCELING
281+
case codersdk.WorkspaceStatusDeleted:
282+
return proto.Workspace_DELETED
283+
case codersdk.WorkspaceStatusDeleting:
284+
return proto.Workspace_DELETING
285+
case codersdk.WorkspaceStatusFailed:
286+
return proto.Workspace_FAILED
287+
case codersdk.WorkspaceStatusPending:
288+
return proto.Workspace_PENDING
289+
case codersdk.WorkspaceStatusRunning:
290+
return proto.Workspace_RUNNING
291+
case codersdk.WorkspaceStatusStarting:
292+
return proto.Workspace_STARTING
293+
case codersdk.WorkspaceStatusStopped:
294+
return proto.Workspace_STOPPED
295+
case codersdk.WorkspaceStatusStopping:
296+
return proto.Workspace_STOPPING
297+
default:
298+
return proto.Workspace_UNKNOWN
299+
}
300+
}

0 commit comments

Comments
 (0)