chore: add support for one-way websockets to backend (#16853)

Closes https://github.com/coder/coder/issues/16775

## Changes made
- Added `OneWayWebSocket` function that establishes WebSocket
connections that don't allow client-to-server communication
- Added tests for the new function
- Updated API endpoints to make new WS-based endpoints, and mark
previous SSE-based endpoints as deprecated
- Updated existing SSE handlers to use the same core logic as the new WS
handlers

## Notes
- Frontend changes handled via #16855
This commit is contained in:
Michael Smith
2025-03-28 17:13:20 -04:00
committed by GitHub
parent d3050a7e77
commit 9bc727e977
21 changed files with 1720 additions and 190 deletions
+31 -10
View File
@@ -1719,12 +1719,33 @@ func (api *API) resolveAutostart(rw http.ResponseWriter, r *http.Request) {
// @Param workspace path string true "Workspace ID" format(uuid)
// @Success 200 {object} codersdk.Response
// @Router /workspaces/{workspace}/watch [get]
func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
// @Deprecated Use /workspaces/{workspace}/watch-ws instead
func (api *API) watchWorkspaceSSE(rw http.ResponseWriter, r *http.Request) {
api.watchWorkspace(rw, r, httpapi.ServerSentEventSender)
}
// @Summary Watch workspace by ID via WebSockets
// @ID watch-workspace-by-id-via-websockets
// @Security CoderSessionToken
// @Produce json
// @Tags Workspaces
// @Param workspace path string true "Workspace ID" format(uuid)
// @Success 200 {object} codersdk.ServerSentEvent
// @Router /workspaces/{workspace}/watch-ws [get]
func (api *API) watchWorkspaceWS(rw http.ResponseWriter, r *http.Request) {
api.watchWorkspace(rw, r, httpapi.OneWayWebSocketEventSender)
}
func (api *API) watchWorkspace(
rw http.ResponseWriter,
r *http.Request,
connect httpapi.EventSender,
) {
ctx := r.Context()
workspace := httpmw.WorkspaceParam(r)
apiKey := httpmw.APIKey(r)
sendEvent, senderClosed, err := httpapi.ServerSentEventSender(rw, r)
sendEvent, senderClosed, err := connect(rw, r)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error setting up server-sent events.",
@@ -1740,7 +1761,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
sendUpdate := func(_ context.Context, _ []byte) {
workspace, err := api.Database.GetWorkspaceByID(ctx, workspace.ID)
if err != nil {
_ = sendEvent(ctx, codersdk.ServerSentEvent{
_ = sendEvent(codersdk.ServerSentEvent{
Type: codersdk.ServerSentEventTypeError,
Data: codersdk.Response{
Message: "Internal error fetching workspace.",
@@ -1752,7 +1773,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
data, err := api.workspaceData(ctx, []database.Workspace{workspace})
if err != nil {
_ = sendEvent(ctx, codersdk.ServerSentEvent{
_ = sendEvent(codersdk.ServerSentEvent{
Type: codersdk.ServerSentEventTypeError,
Data: codersdk.Response{
Message: "Internal error fetching workspace data.",
@@ -1762,7 +1783,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
return
}
if len(data.templates) == 0 {
_ = sendEvent(ctx, codersdk.ServerSentEvent{
_ = sendEvent(codersdk.ServerSentEvent{
Type: codersdk.ServerSentEventTypeError,
Data: codersdk.Response{
Message: "Forbidden reading template of selected workspace.",
@@ -1779,7 +1800,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
api.Options.AllowWorkspaceRenames,
)
if err != nil {
_ = sendEvent(ctx, codersdk.ServerSentEvent{
_ = sendEvent(codersdk.ServerSentEvent{
Type: codersdk.ServerSentEventTypeError,
Data: codersdk.Response{
Message: "Internal error converting workspace.",
@@ -1787,7 +1808,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
},
})
}
_ = sendEvent(ctx, codersdk.ServerSentEvent{
_ = sendEvent(codersdk.ServerSentEvent{
Type: codersdk.ServerSentEventTypeData,
Data: w,
})
@@ -1805,7 +1826,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
sendUpdate(ctx, nil)
}))
if err != nil {
_ = sendEvent(ctx, codersdk.ServerSentEvent{
_ = sendEvent(codersdk.ServerSentEvent{
Type: codersdk.ServerSentEventTypeError,
Data: codersdk.Response{
Message: "Internal error subscribing to workspace events.",
@@ -1819,7 +1840,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
// This is required to show whether the workspace is up-to-date.
cancelTemplateSubscribe, err := api.Pubsub.Subscribe(watchTemplateChannel(workspace.TemplateID), sendUpdate)
if err != nil {
_ = sendEvent(ctx, codersdk.ServerSentEvent{
_ = sendEvent(codersdk.ServerSentEvent{
Type: codersdk.ServerSentEventTypeError,
Data: codersdk.Response{
Message: "Internal error subscribing to template events.",
@@ -1832,7 +1853,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
// An initial ping signals to the request that the server is now ready
// and the client can begin servicing a channel with data.
_ = sendEvent(ctx, codersdk.ServerSentEvent{
_ = sendEvent(codersdk.ServerSentEvent{
Type: codersdk.ServerSentEventTypePing,
})
// Send updated workspace info after connection is established. This avoids