Skip to content

Commit f752a7e

Browse files
committed
feat: add WorkspaceUpdates rpc
1 parent 54cbfaf commit f752a7e

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
@@ -855,26 +855,10 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R
855855
return
856856
}
857857

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

880864
api.WebsocketWaitMutex.Lock()
@@ -904,6 +888,28 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R
904888
}
905889
}
906890

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

coderd/workspacebuilds.go

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

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"
@@ -36,6 +38,7 @@ import (
3638
"github.com/coder/coder/v2/coderd/wsbuilder"
3739
"github.com/coder/coder/v2/codersdk"
3840
"github.com/coder/coder/v2/codersdk/agentsdk"
41+
"github.com/coder/coder/v2/tailnet"
3942
)
4043

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

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

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)