diff --git a/coderd/coderd.go b/coderd/coderd.go index 1f1a4ff18dfac..cc411ef71e255 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -310,6 +310,7 @@ func New(options *Options) (http.Handler, func()) { r.Route("/autostop", func(r chi.Router) { r.Put("/", api.putWorkspaceAutostop) }) + r.Get("/watch", api.watchWorkspace) }) }) r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) { diff --git a/coderd/coderd_test.go b/coderd/coderd_test.go index cc34d89ed6bee..23b82b0d374c6 100644 --- a/coderd/coderd_test.go +++ b/coderd/coderd_test.go @@ -128,6 +128,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) { "PUT:/api/v2/workspaces/{workspace}/autostop": {NoAuthorize: true}, "GET:/api/v2/workspaces/{workspace}/builds": {NoAuthorize: true}, "POST:/api/v2/workspaces/{workspace}/builds": {NoAuthorize: true}, + "GET:/api/v2/workspaces/{workspace}/watch": {NoAuthorize: true}, "POST:/api/v2/files": {NoAuthorize: true}, "GET:/api/v2/files/{hash}": {NoAuthorize: true}, diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index c3038ace73b6a..abf55128d55d7 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -17,8 +17,8 @@ import ( "github.com/coder/coder/coderd/httpapi" ) -// AuthCookie represents the name of the cookie the API key is stored in. -const AuthCookie = "session_token" +// SessionTokenKey represents the name of the cookie or query paramater the API key is stored in. +const SessionTokenKey = "session_token" type apiKeyContextKey struct{} @@ -43,18 +43,24 @@ type OAuth2Configs struct { func ExtractAPIKey(db database.Store, oauth *OAuth2Configs) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - cookie, err := r.Cookie(AuthCookie) + var cookieValue string + cookie, err := r.Cookie(SessionTokenKey) if err != nil { + cookieValue = r.URL.Query().Get(SessionTokenKey) + } else { + cookieValue = cookie.Value + } + if cookieValue == "" { httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: fmt.Sprintf("%q cookie must be provided", AuthCookie), + Message: fmt.Sprintf("%q cookie or query parameter must be provided", SessionTokenKey), }) return } - parts := strings.Split(cookie.Value, "-") + parts := strings.Split(cookieValue, "-") // APIKeys are formatted: ID-SECRET if len(parts) != 2 { httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: fmt.Sprintf("invalid %q cookie api key format", AuthCookie), + Message: fmt.Sprintf("invalid %q cookie api key format", SessionTokenKey), }) return } @@ -63,13 +69,13 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs) func(http.Handler) h // Ensuring key lengths are valid. if len(keyID) != 10 { httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: fmt.Sprintf("invalid %q cookie api key id", AuthCookie), + Message: fmt.Sprintf("invalid %q cookie api key id", SessionTokenKey), }) return } if len(keySecret) != 22 { httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: fmt.Sprintf("invalid %q cookie api key secret", AuthCookie), + Message: fmt.Sprintf("invalid %q cookie api key secret", SessionTokenKey), }) return } diff --git a/coderd/httpmw/apikey_test.go b/coderd/httpmw/apikey_test.go index 0c8d8d396e55b..3be2877426134 100644 --- a/coderd/httpmw/apikey_test.go +++ b/coderd/httpmw/apikey_test.go @@ -56,7 +56,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: "test-wow-hello", }) @@ -74,7 +74,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: "test-wow", }) @@ -92,7 +92,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: "testtestid-wow", }) @@ -111,7 +111,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) @@ -130,7 +130,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) @@ -157,7 +157,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) @@ -182,7 +182,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) @@ -209,6 +209,37 @@ func TestAPIKey(t *testing.T) { require.Equal(t, sentAPIKey.ExpiresAt, gotAPIKey.ExpiresAt) }) + t.Run("QueryParameter", func(t *testing.T) { + t.Parallel() + var ( + db = databasefake.New() + id, secret = randomAPIKeyParts() + hashed = sha256.Sum256([]byte(secret)) + r = httptest.NewRequest("GET", "/", nil) + rw = httptest.NewRecorder() + ) + q := r.URL.Query() + q.Add(httpmw.SessionTokenKey, fmt.Sprintf("%s-%s", id, secret)) + r.URL.RawQuery = q.Encode() + + _, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{ + ID: id, + HashedSecret: hashed[:], + ExpiresAt: database.Now().AddDate(0, 0, 1), + }) + require.NoError(t, err) + httpmw.ExtractAPIKey(db, nil)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + // Checks that it exists on the context! + _ = httpmw.APIKey(r) + httpapi.Write(rw, http.StatusOK, httpapi.Response{ + Message: "it worked!", + }) + })).ServeHTTP(rw, r) + res := rw.Result() + defer res.Body.Close() + require.Equal(t, http.StatusOK, res.StatusCode) + }) + t.Run("ValidUpdateLastUsed", func(t *testing.T) { t.Parallel() var ( @@ -219,7 +250,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) @@ -252,7 +283,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) @@ -285,7 +316,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) @@ -319,7 +350,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) diff --git a/coderd/httpmw/authorize_test.go b/coderd/httpmw/authorize_test.go index 59ee50c80d9be..0e2466da403cd 100644 --- a/coderd/httpmw/authorize_test.go +++ b/coderd/httpmw/authorize_test.go @@ -94,7 +94,7 @@ func TestExtractUserRoles(t *testing.T) { req := httptest.NewRequest("GET", "/", nil) req.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: token, }) diff --git a/coderd/httpmw/organizationparam_test.go b/coderd/httpmw/organizationparam_test.go index b062e63bc3819..2b3a01cc6aaa5 100644 --- a/coderd/httpmw/organizationparam_test.go +++ b/coderd/httpmw/organizationparam_test.go @@ -29,7 +29,7 @@ func TestOrganizationParam(t *testing.T) { hashed = sha256.Sum256([]byte(secret)) ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) diff --git a/coderd/httpmw/templateparam_test.go b/coderd/httpmw/templateparam_test.go index b4db9925391c3..201961ba26c54 100644 --- a/coderd/httpmw/templateparam_test.go +++ b/coderd/httpmw/templateparam_test.go @@ -29,7 +29,7 @@ func TestTemplateParam(t *testing.T) { ) r := httptest.NewRequest("GET", "/", nil) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) diff --git a/coderd/httpmw/templateversionparam_test.go b/coderd/httpmw/templateversionparam_test.go index f168661d8bde8..d4487b183b788 100644 --- a/coderd/httpmw/templateversionparam_test.go +++ b/coderd/httpmw/templateversionparam_test.go @@ -29,7 +29,7 @@ func TestTemplateVersionParam(t *testing.T) { ) r := httptest.NewRequest("GET", "/", nil) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) diff --git a/coderd/httpmw/userparam_test.go b/coderd/httpmw/userparam_test.go index 48e1da72e2142..d7a467d65940c 100644 --- a/coderd/httpmw/userparam_test.go +++ b/coderd/httpmw/userparam_test.go @@ -29,7 +29,7 @@ func TestUserParam(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) diff --git a/coderd/httpmw/workspaceagent.go b/coderd/httpmw/workspaceagent.go index 05e7fe213c242..8bcf10f086285 100644 --- a/coderd/httpmw/workspaceagent.go +++ b/coderd/httpmw/workspaceagent.go @@ -28,10 +28,10 @@ func WorkspaceAgent(r *http.Request) database.WorkspaceAgent { func ExtractWorkspaceAgent(db database.Store) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - cookie, err := r.Cookie(AuthCookie) + cookie, err := r.Cookie(SessionTokenKey) if err != nil { httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: fmt.Sprintf("%q cookie must be provided", AuthCookie), + Message: fmt.Sprintf("%q cookie must be provided", SessionTokenKey), }) return } diff --git a/coderd/httpmw/workspaceagent_test.go b/coderd/httpmw/workspaceagent_test.go index 650e68436b836..0661183abffcf 100644 --- a/coderd/httpmw/workspaceagent_test.go +++ b/coderd/httpmw/workspaceagent_test.go @@ -22,7 +22,7 @@ func TestWorkspaceAgent(t *testing.T) { token := uuid.New() r := httptest.NewRequest("GET", "/", nil) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: token.String(), }) return r, token diff --git a/coderd/httpmw/workspaceagentparam_test.go b/coderd/httpmw/workspaceagentparam_test.go index c7f931438901d..c985234824458 100644 --- a/coderd/httpmw/workspaceagentparam_test.go +++ b/coderd/httpmw/workspaceagentparam_test.go @@ -29,7 +29,7 @@ func TestWorkspaceAgentParam(t *testing.T) { ) r := httptest.NewRequest("GET", "/", nil) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) diff --git a/coderd/httpmw/workspacebuildparam_test.go b/coderd/httpmw/workspacebuildparam_test.go index 0e72e02fcc9b9..7ed74e274cc9b 100644 --- a/coderd/httpmw/workspacebuildparam_test.go +++ b/coderd/httpmw/workspacebuildparam_test.go @@ -29,7 +29,7 @@ func TestWorkspaceBuildParam(t *testing.T) { ) r := httptest.NewRequest("GET", "/", nil) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) diff --git a/coderd/httpmw/workspaceparam_test.go b/coderd/httpmw/workspaceparam_test.go index 2f1f522b5a211..52731dc10f1cb 100644 --- a/coderd/httpmw/workspaceparam_test.go +++ b/coderd/httpmw/workspaceparam_test.go @@ -29,7 +29,7 @@ func TestWorkspaceParam(t *testing.T) { ) r := httptest.NewRequest("GET", "/", nil) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) diff --git a/coderd/users.go b/coderd/users.go index fbbbde5e250c1..92e2135b2fd37 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -690,7 +690,7 @@ func (*api) postLogout(rw http.ResponseWriter, _ *http.Request) { cookie := &http.Cookie{ // MaxAge < 0 means to delete the cookie now MaxAge: -1, - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Path: "/", } @@ -748,7 +748,7 @@ func (api *api) createAPIKey(rw http.ResponseWriter, r *http.Request, params dat // This format is consumed by the APIKey middleware. sessionToken := fmt.Sprintf("%s-%s", keyID, keySecret) http.SetCookie(rw, &http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: sessionToken, Path: "/", HttpOnly: true, diff --git a/coderd/users_test.go b/coderd/users_test.go index 9c2846ed96cbd..e0e1033819526 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -122,7 +122,7 @@ func TestPostLogout(t *testing.T) { cookies := response.Cookies() require.Len(t, cookies, 1, "Exactly one cookie should be returned") - require.Equal(t, cookies[0].Name, httpmw.AuthCookie, "Cookie should be the auth cookie") + require.Equal(t, cookies[0].Name, httpmw.SessionTokenKey, "Cookie should be the auth cookie") require.Equal(t, cookies[0].MaxAge, -1, "Cookie should be set to delete") }) } diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 4ef3b61301fe6..1e567dfe77c40 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -7,12 +7,17 @@ import ( "errors" "fmt" "net/http" + "time" "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/moby/moby/pkg/namesgenerator" "golang.org/x/sync/errgroup" "golang.org/x/xerrors" + "nhooyr.io/websocket" + "nhooyr.io/websocket/wsjson" + + "cdr.dev/slog" "github.com/coder/coder/coderd/autobuild/schedule" "github.com/coder/coder/coderd/database" @@ -535,6 +540,95 @@ func (api *api) putWorkspaceAutostop(rw http.ResponseWriter, r *http.Request) { } } +func (api *api) watchWorkspace(rw http.ResponseWriter, r *http.Request) { + workspace := httpmw.WorkspaceParam(r) + + c, err := websocket.Accept(rw, r, &websocket.AcceptOptions{ + // Fix for Safari 15.1: + // There is a bug in latest Safari in which compressed web socket traffic + // isn't handled correctly. Turning off compression is a workaround: + // https://github.com/nhooyr/websocket/issues/218 + CompressionMode: websocket.CompressionDisabled, + }) + if err != nil { + api.Logger.Warn(r.Context(), "accept websocket connection", slog.Error(err)) + return + } + defer c.Close(websocket.StatusInternalError, "internal error") + + // Makes the websocket connection write-only + ctx := c.CloseRead(r.Context()) + + // Send a heartbeat every 15 seconds to avoid the websocket being killed. + go func() { + ticker := time.NewTicker(time.Second * 15) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + err := c.Ping(ctx) + if err != nil { + return + } + } + } + }() + + t := time.NewTicker(time.Second * 1) + defer t.Stop() + for { + select { + case <-t.C: + workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspace.ID) + if err != nil { + _ = wsjson.Write(ctx, c, httpapi.Response{ + Message: fmt.Sprintf("get workspace: %s", err), + }) + return + } + build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) + if err != nil { + _ = wsjson.Write(ctx, c, httpapi.Response{ + Message: fmt.Sprintf("get workspace build: %s", err), + }) + return + } + var ( + group errgroup.Group + job database.ProvisionerJob + template database.Template + owner database.User + ) + group.Go(func() (err error) { + job, err = api.Database.GetProvisionerJobByID(r.Context(), build.JobID) + return err + }) + group.Go(func() (err error) { + template, err = api.Database.GetTemplateByID(r.Context(), workspace.TemplateID) + return err + }) + group.Go(func() (err error) { + owner, err = api.Database.GetUserByID(r.Context(), workspace.OwnerID) + return err + }) + err = group.Wait() + if err != nil { + _ = wsjson.Write(ctx, c, httpapi.Response{ + Message: fmt.Sprintf("fetch resource: %s", err), + }) + return + } + + _ = wsjson.Write(ctx, c, convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), template, owner)) + case <-ctx.Done(): + return + } + } +} + func convertWorkspaces(ctx context.Context, db database.Store, workspaces []database.Workspace) ([]codersdk.Workspace, error) { workspaceIDs := make([]uuid.UUID, 0, len(workspaces)) templateIDs := make([]uuid.UUID, 0, len(workspaces)) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 58140b1d00e1d..6658aa8481940 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -621,3 +621,27 @@ func mustLocation(t *testing.T, location string) *time.Location { return loc } + +func TestWorkspaceWatcher(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + w, err := client.Workspace(context.Background(), workspace.ID) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + wc, err := client.WatchWorkspace(ctx, w.ID) + require.NoError(t, err) + for i := 0; i < 3; i++ { + _, more := <-wc + require.True(t, more) + } + cancel() + require.EqualValues(t, codersdk.Workspace{}, <-wc) +} diff --git a/codersdk/client.go b/codersdk/client.go index 48571adff5d0b..71786de4f0cf4 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -12,6 +12,7 @@ import ( "strings" "golang.org/x/xerrors" + "nhooyr.io/websocket" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/httpmw" @@ -63,7 +64,7 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac return nil, xerrors.Errorf("create request: %w", err) } req.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: c.SessionToken, }) if body != nil { @@ -80,6 +81,38 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac return resp, err } +// dialWebsocket opens a dialWebsocket connection on that path provided. +// The caller is responsible for closing the dialWebsocket.Conn. +func (c *Client) dialWebsocket(ctx context.Context, path string) (*websocket.Conn, error) { + serverURL, err := c.URL.Parse(path) + if err != nil { + return nil, xerrors.Errorf("parse path: %w", err) + } + + apiURL, err := url.Parse(serverURL.String()) + if err != nil { + return nil, xerrors.Errorf("parse server url: %w", err) + } + apiURL.Scheme = "ws" + if serverURL.Scheme == "https" { + apiURL.Scheme = "wss" + } + apiURL.Path = path + q := apiURL.Query() + q.Add(httpmw.SessionTokenKey, c.SessionToken) + apiURL.RawQuery = q.Encode() + + //nolint:bodyclose + conn, _, err := websocket.Dial(ctx, apiURL.String(), &websocket.DialOptions{ + HTTPClient: c.HTTPClient, + }) + if err != nil { + return nil, xerrors.Errorf("dial websocket: %w", err) + } + + return conn, nil +} + // readBodyAsError reads the response as an httpapi.Message, and // wraps it in a codersdk.Error type for easy marshaling. func readBodyAsError(res *http.Response) error { diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index d64b42bc5faaa..c634b1de7ea2a 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -188,7 +188,7 @@ func (c *Client) ListenWorkspaceAgent(ctx context.Context, logger slog.Logger) ( return agent.Metadata{}, nil, xerrors.Errorf("create cookie jar: %w", err) } jar.SetCookies(serverURL, []*http.Cookie{{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: c.SessionToken, }}) httpClient := &http.Client{ @@ -263,7 +263,7 @@ func (c *Client) DialWorkspaceAgent(ctx context.Context, agentID uuid.UUID, opti return nil, xerrors.Errorf("create cookie jar: %w", err) } jar.SetCookies(serverURL, []*http.Cookie{{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: c.SessionToken, }}) httpClient := &http.Client{ @@ -351,7 +351,7 @@ func (c *Client) WorkspaceAgentReconnectingPTY(ctx context.Context, agentID, rec return nil, xerrors.Errorf("create cookie jar: %w", err) } jar.SetCookies(serverURL, []*http.Cookie{{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: c.SessionToken, }}) httpClient := &http.Client{ diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 6e4ab7afd6e57..d43219e0f9d65 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -9,6 +9,8 @@ import ( "github.com/google/uuid" "golang.org/x/xerrors" + "nhooyr.io/websocket" + "nhooyr.io/websocket/wsjson" "github.com/coder/coder/coderd/database" ) @@ -98,6 +100,36 @@ func (c *Client) WorkspaceBuildByName(ctx context.Context, workspace uuid.UUID, return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) } +func (c *Client) WatchWorkspace(ctx context.Context, id uuid.UUID) (<-chan Workspace, error) { + conn, err := c.dialWebsocket(ctx, fmt.Sprintf("/api/v2/workspaces/%s/watch", id)) + if err != nil { + return nil, err + } + wc := make(chan Workspace, 256) + + go func() { + defer close(wc) + defer conn.Close(websocket.StatusNormalClosure, "") + + for { + select { + case <-ctx.Done(): + return + default: + var ws Workspace + err := wsjson.Read(ctx, conn, &ws) + if err != nil { + conn.Close(websocket.StatusInternalError, "failed to read workspace") + return + } + wc <- ws + } + } + }() + + return wc, nil +} + // UpdateWorkspaceAutostartRequest is a request to update a workspace's autostart schedule. type UpdateWorkspaceAutostartRequest struct { Schedule string `json:"schedule"`
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: