From efbcf4375475ebd66db603063b98898e7f7b04c3 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Fri, 26 May 2023 00:06:50 +0000 Subject: [PATCH 1/8] feat(healthcheck): add websocket report --- coderd/coderd.go | 7 +- coderd/coderdtest/coderdtest.go | 2 +- coderd/debug.go | 26 ++++-- coderd/debug_test.go | 62 +++++++++++++-- coderd/healthcheck/healthcheck.go | 23 ++++-- coderd/healthcheck/websocket.go | 115 ++++++++++++++++++++++++++- coderd/healthcheck/websocket_test.go | 39 +++++++++ 7 files changed, 246 insertions(+), 28 deletions(-) create mode 100644 coderd/healthcheck/websocket_test.go diff --git a/coderd/coderd.go b/coderd/coderd.go index 712e52460fbfe..9a1fdd81c59fa 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -129,7 +129,7 @@ type Options struct { // AppSecurityKey is the crypto key used to sign and encrypt tokens related to // workspace applications. It consists of both a signing and encryption key. AppSecurityKey workspaceapps.SecurityKey - HealthcheckFunc func(ctx context.Context) (*healthcheck.Report, error) + HealthcheckFunc func(ctx context.Context, apiKey string) (*healthcheck.Report, error) HealthcheckTimeout time.Duration HealthcheckRefresh time.Duration @@ -256,10 +256,11 @@ func New(options *Options) *API { options.TemplateScheduleStore.Store(&v) } if options.HealthcheckFunc == nil { - options.HealthcheckFunc = func(ctx context.Context) (*healthcheck.Report, error) { + options.HealthcheckFunc = func(ctx context.Context, apiKey string) (*healthcheck.Report, error) { return healthcheck.Run(ctx, &healthcheck.ReportOptions{ AccessURL: options.AccessURL, DERPMap: options.DERPMap.Clone(), + APIKey: apiKey, }) } } @@ -787,6 +788,7 @@ func New(options *Options) *API { r.Get("/coordinator", api.debugCoordinator) r.Get("/health", api.debugDeploymentHealth) + r.Get("/ws", healthcheck.WebsocketEchoServer{}.ServeHTTP) }) }) @@ -874,6 +876,7 @@ type API struct { Experiments codersdk.Experiments healthCheckGroup *singleflight.Group[string, *healthcheck.Report] + healthCheckCache atomic.Pointer[healthcheck.Report] } // Close waits for all WebSocket connections to drain before returning. diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 063f77a193d0c..879acc737e75b 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -107,7 +107,7 @@ type Options struct { TrialGenerator func(context.Context, string) error TemplateScheduleStore schedule.TemplateScheduleStore - HealthcheckFunc func(ctx context.Context) (*healthcheck.Report, error) + HealthcheckFunc func(ctx context.Context, apiKey string) (*healthcheck.Report, error) HealthcheckTimeout time.Duration HealthcheckRefresh time.Duration diff --git a/coderd/debug.go b/coderd/debug.go index 3bad452bdd296..a95738d351a19 100644 --- a/coderd/debug.go +++ b/coderd/debug.go @@ -7,6 +7,7 @@ import ( "github.com/coder/coder/coderd/healthcheck" "github.com/coder/coder/coderd/httpapi" + "github.com/coder/coder/coderd/httpmw" "github.com/coder/coder/codersdk" ) @@ -29,11 +30,28 @@ func (api *API) debugCoordinator(rw http.ResponseWriter, r *http.Request) { // @Success 200 {object} healthcheck.Report // @Router /debug/health [get] func (api *API) debugDeploymentHealth(rw http.ResponseWriter, r *http.Request) { + apiKey := httpmw.APITokenFromRequest(r) ctx, cancel := context.WithTimeout(r.Context(), api.HealthcheckTimeout) defer cancel() + // Get cached report if it exists. + if report := api.healthCheckCache.Load(); report != nil { + if time.Since(report.Time) < api.HealthcheckRefresh { + httpapi.Write(ctx, rw, http.StatusOK, report) + return + } + } + resChan := api.healthCheckGroup.DoChan("", func() (*healthcheck.Report, error) { - return api.HealthcheckFunc(ctx) + // Create a new context not tied to the request. + ctx, cancel := context.WithTimeout(context.Background(), api.HealthcheckTimeout) + defer cancel() + + report, err := api.HealthcheckFunc(ctx, apiKey) + if err == nil { + api.healthCheckCache.Store(report) + } + return report, err }) select { @@ -43,12 +61,6 @@ func (api *API) debugDeploymentHealth(rw http.ResponseWriter, r *http.Request) { }) return case res := <-resChan: - if time.Since(res.Val.Time) > api.HealthcheckRefresh { - api.healthCheckGroup.Forget("") - api.debugDeploymentHealth(rw, r) - return - } - httpapi.Write(ctx, rw, http.StatusOK, res.Val) return } diff --git a/coderd/debug_test.go b/coderd/debug_test.go index 81022a10c25b1..9742ce959834b 100644 --- a/coderd/debug_test.go +++ b/coderd/debug_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coder/coder/coderd/coderdtest" @@ -14,15 +15,17 @@ import ( "github.com/coder/coder/testutil" ) -func TestDebug(t *testing.T) { +func TestDebugHealth(t *testing.T) { t.Parallel() - t.Run("Health/OK", func(t *testing.T) { + t.Run("OK", func(t *testing.T) { t.Parallel() var ( - ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort) - client = coderdtest.New(t, &coderdtest.Options{ - HealthcheckFunc: func(context.Context) (*healthcheck.Report, error) { + ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort) + sessionToken string + client = coderdtest.New(t, &coderdtest.Options{ + HealthcheckFunc: func(_ context.Context, apiKey string) (*healthcheck.Report, error) { + assert.Equal(t, sessionToken, apiKey) return &healthcheck.Report{}, nil }, }) @@ -30,6 +33,7 @@ func TestDebug(t *testing.T) { ) defer cancel() + sessionToken = client.SessionToken() res, err := client.Request(ctx, "GET", "/debug/health", nil) require.NoError(t, err) defer res.Body.Close() @@ -37,14 +41,14 @@ func TestDebug(t *testing.T) { require.Equal(t, http.StatusOK, res.StatusCode) }) - t.Run("Health/Timeout", func(t *testing.T) { + t.Run("Timeout", func(t *testing.T) { t.Parallel() var ( ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort) client = coderdtest.New(t, &coderdtest.Options{ HealthcheckTimeout: time.Microsecond, - HealthcheckFunc: func(context.Context) (*healthcheck.Report, error) { + HealthcheckFunc: func(context.Context, string) (*healthcheck.Report, error) { t := time.NewTimer(time.Second) defer t.Stop() @@ -66,4 +70,48 @@ func TestDebug(t *testing.T) { _, _ = io.ReadAll(res.Body) require.Equal(t, http.StatusNotFound, res.StatusCode) }) + + t.Run("Deduplicated", func(t *testing.T) { + t.Parallel() + + var ( + ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort) + calls int + client = coderdtest.New(t, &coderdtest.Options{ + HealthcheckRefresh: time.Hour, + HealthcheckTimeout: time.Hour, + HealthcheckFunc: func(context.Context, string) (*healthcheck.Report, error) { + calls++ + return &healthcheck.Report{ + Time: time.Now(), + }, nil + }, + }) + _ = coderdtest.CreateFirstUser(t, client) + ) + defer cancel() + + res, err := client.Request(ctx, "GET", "/api/v2/debug/health", nil) + require.NoError(t, err) + defer res.Body.Close() + _, _ = io.ReadAll(res.Body) + + require.Equal(t, http.StatusOK, res.StatusCode) + + res, err = client.Request(ctx, "GET", "/api/v2/debug/health", nil) + require.NoError(t, err) + defer res.Body.Close() + _, _ = io.ReadAll(res.Body) + + require.Equal(t, http.StatusOK, res.StatusCode) + require.Equal(t, 1, calls) + }) +} + +func TestDebugWebsocket(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + }) } diff --git a/coderd/healthcheck/healthcheck.go b/coderd/healthcheck/healthcheck.go index 88f9f0ad075d0..cc5def5719c1d 100644 --- a/coderd/healthcheck/healthcheck.go +++ b/coderd/healthcheck/healthcheck.go @@ -19,9 +19,7 @@ type Report struct { DERP DERPReport `json:"derp"` AccessURL AccessURLReport `json:"access_url"` - - // TODO: - // Websocket WebsocketReport `json:"websocket"` + Websocket WebsocketReport `json:"websocket"` } type ReportOptions struct { @@ -29,6 +27,7 @@ type ReportOptions struct { DERPMap *tailcfg.DERPMap AccessURL *url.URL Client *http.Client + APIKey string } func Run(ctx context.Context, opts *ReportOptions) (*Report, error) { @@ -65,11 +64,19 @@ func Run(ctx context.Context, opts *ReportOptions) (*Report, error) { }) }() - // wg.Add(1) - // go func() { - // defer wg.Done() - // report.Websocket.Run(ctx, opts.AccessURL) - // }() + wg.Add(1) + go func() { + defer wg.Done() + defer func() { + if err := recover(); err != nil { + report.Websocket.Error = xerrors.Errorf("%v", err) + } + }() + report.Websocket.Run(ctx, &WebsocketReportOptions{ + APIKey: opts.APIKey, + AccessURL: opts.AccessURL, + }) + }() wg.Wait() report.Time = time.Now() diff --git a/coderd/healthcheck/websocket.go b/coderd/healthcheck/websocket.go index a8063f3f6a9ee..1bdb72501c38f 100644 --- a/coderd/healthcheck/websocket.go +++ b/coderd/healthcheck/websocket.go @@ -2,11 +2,120 @@ package healthcheck import ( "context" + "io" + "net/http" "net/url" + "strconv" + "time" + + "golang.org/x/xerrors" + "nhooyr.io/websocket" + + "github.com/coder/coder/coderd/httpapi" ) -type WebsocketReport struct{} +type WebsocketReportOptions struct { + APIKey string + AccessURL *url.URL + HTTPClient *http.Client +} + +type WebsocketReport struct { + Error error `json:"error"` +} + +func (r *WebsocketReport) Run(ctx context.Context, opts *WebsocketReportOptions) { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + u, err := opts.AccessURL.Parse("/api/v2/debug/ws") + if err != nil { + r.Error = xerrors.Errorf("parse access url: %w", err) + return + } + if u.Scheme == "https" { + u.Scheme = "wss" + } else { + u.Scheme = "ws" + } + + //nolint:bodyclose + c, _, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{ + HTTPClient: opts.HTTPClient, + HTTPHeader: http.Header{"Coder-Session-Token": []string{opts.APIKey}}, + }) + if err != nil { + r.Error = xerrors.Errorf("websocket dial: %w", err) + return + } + defer c.Close(websocket.StatusGoingAway, "goodbye") + + for i := 0; i < 3; i++ { + msg := strconv.Itoa(i) + err := c.Write(ctx, websocket.MessageText, []byte(msg)) + if err != nil { + r.Error = xerrors.Errorf("write message: %w", err) + return + } + + ty, got, err := c.Read(ctx) + if err != nil { + r.Error = xerrors.Errorf("read message: %w", err) + return + } + + if ty != websocket.MessageText { + r.Error = xerrors.Errorf("received incorrect message type: %v", ty) + return + } + + if string(got) != msg { + r.Error = xerrors.Errorf("received incorrect message: wanted %q, got %q", msg, string(got)) + return + } + } + + c.Close(websocket.StatusGoingAway, "goodbye") +} + +type WebsocketEchoServer struct{} + +func (WebsocketEchoServer) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + c, err := websocket.Accept(rw, r, &websocket.AcceptOptions{}) + if err != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, "Invalid websocket.") + return + } + defer c.Close(websocket.StatusGoingAway, "goodbye") + + echo := func() error { + ctx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + + typ, r, err := c.Reader(ctx) + if err != nil { + return xerrors.Errorf("get reader: %w", err) + } + + w, err := c.Writer(ctx, typ) + if err != nil { + return xerrors.Errorf("get writer: %w", err) + } + + _, err = io.Copy(w, r) + if err != nil { + return xerrors.Errorf("echo message: %w", err) + } + + err = w.Close() + return err + } -func (*WebsocketReport) Run(ctx context.Context, accessURL *url.URL) { - _, _ = ctx, accessURL + for { + err := echo() + if err != nil { + return + } + } } diff --git a/coderd/healthcheck/websocket_test.go b/coderd/healthcheck/websocket_test.go new file mode 100644 index 0000000000000..c4408f8a262f1 --- /dev/null +++ b/coderd/healthcheck/websocket_test.go @@ -0,0 +1,39 @@ +package healthcheck_test + +import ( + "context" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/coderd/healthcheck" + "github.com/coder/coder/testutil" +) + +func TestWebsocket(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + srv := httptest.NewServer(healthcheck.WebsocketEchoServer{}) + defer srv.Close() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + u, err := url.Parse(srv.URL) + require.NoError(t, err) + + wsReport := healthcheck.WebsocketReport{} + wsReport.Run(ctx, &healthcheck.WebsocketReportOptions{ + AccessURL: u, + HTTPClient: srv.Client(), + APIKey: "test", + }) + + require.NoError(t, wsReport.Error) + }) +} From 4d989aace016350a1109b7d581f4cabb6cc07aca Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Tue, 30 May 2023 16:19:08 +0000 Subject: [PATCH 2/8] fixup! feat(healthcheck): add websocket report --- coderd/apidoc/docs.go | 9 +++++++++ coderd/apidoc/swagger.json | 9 +++++++++ coderd/coderd.go | 7 +++++++ docs/api/debug.md | 5 ++++- docs/api/schemas.md | 20 +++++++++++++++++++- 5 files changed, 48 insertions(+), 2 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 81675530aa57d..9c3cf7a7bd6f5 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -10425,9 +10425,18 @@ const docTemplate = `{ "time": { "description": "Time is the time the report was generated at.", "type": "string" + }, + "websocket": { + "$ref": "#/definitions/healthcheck.WebsocketReport" } } }, + "healthcheck.WebsocketReport": { + "type": "object", + "properties": { + "error": {} + } + }, "netcheck.Report": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 2c9080fe49b74..46395e27ab462 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -9412,9 +9412,18 @@ "time": { "description": "Time is the time the report was generated at.", "type": "string" + }, + "websocket": { + "$ref": "#/definitions/healthcheck.WebsocketReport" } } }, + "healthcheck.WebsocketReport": { + "type": "object", + "properties": { + "error": {} + } + }, "netcheck.Report": { "type": "object", "properties": { diff --git a/coderd/coderd.go b/coderd/coderd.go index 9a1fdd81c59fa..e594f4388e7e4 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -788,6 +788,13 @@ func New(options *Options) *API { r.Get("/coordinator", api.debugCoordinator) r.Get("/health", api.debugDeploymentHealth) + // @Summary Debug Info Deployment Health + // @ID debug-info-deployment-health + // @Security CoderSessionToken + // @Produce json + // @Tags Debug + // @Success 200 {object} healthcheck.Report + // @Router /debug/health [get] r.Get("/ws", healthcheck.WebsocketEchoServer{}.ServeHTTP) }) }) diff --git a/docs/api/debug.md b/docs/api/debug.md index 0f68215501c4e..41fe31e9dd4f5 100644 --- a/docs/api/debug.md +++ b/docs/api/debug.md @@ -207,7 +207,10 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ } }, "pass": true, - "time": "string" + "time": "string", + "websocket": { + "error": null + } } ``` diff --git a/docs/api/schemas.md b/docs/api/schemas.md index b613608e555e2..0ade75c9d620b 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -6393,7 +6393,10 @@ Parameter represents a set value for the scope. } }, "pass": true, - "time": "string" + "time": "string", + "websocket": { + "error": null + } } ``` @@ -6405,6 +6408,21 @@ Parameter represents a set value for the scope. | `derp` | [healthcheck.DERPReport](#healthcheckderpreport) | false | | | | `pass` | boolean | false | | Healthy is true if the report returns no errors. | | `time` | string | false | | Time is the time the report was generated at. | +| `websocket` | [healthcheck.WebsocketReport](#healthcheckwebsocketreport) | false | | | + +## healthcheck.WebsocketReport + +```json +{ + "error": null +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------- | ---- | -------- | ------------ | ----------- | +| `error` | any | false | | | ## netcheck.Report From e3182d7adf4cb2d22a7867309019be6f7bed72de Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Tue, 30 May 2023 17:20:55 +0000 Subject: [PATCH 3/8] fixup! feat(healthcheck): add websocket report --- coderd/apidoc/docs.go | 28 ++++++++++++++++++++++++++++ coderd/apidoc/swagger.json | 24 ++++++++++++++++++++++++ coderd/coderd.go | 7 ------- coderd/debug.go | 14 ++++++++++++++ 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 9c3cf7a7bd6f5..62f9612ebee6e 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -427,6 +427,34 @@ const docTemplate = `{ } } }, + "/debug/ws": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Debug" + ], + "summary": "Debug Websocket", + "operationId": "debug-info-websocket-test", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.Response" + } + } + }, + "x-apidocgen": { + "skip": true + } + } + }, "/deployment/config": { "get": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 46395e27ab462..e511e6d59adcd 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -363,6 +363,30 @@ } } }, + "/debug/ws": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Debug"], + "summary": "Debug Websocket", + "operationId": "debug-info-websocket-test", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.Response" + } + } + }, + "x-apidocgen": { + "skip": true + } + } + }, "/deployment/config": { "get": { "security": [ diff --git a/coderd/coderd.go b/coderd/coderd.go index e594f4388e7e4..9a1fdd81c59fa 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -788,13 +788,6 @@ func New(options *Options) *API { r.Get("/coordinator", api.debugCoordinator) r.Get("/health", api.debugDeploymentHealth) - // @Summary Debug Info Deployment Health - // @ID debug-info-deployment-health - // @Security CoderSessionToken - // @Produce json - // @Tags Debug - // @Success 200 {object} healthcheck.Report - // @Router /debug/health [get] r.Get("/ws", healthcheck.WebsocketEchoServer{}.ServeHTTP) }) }) diff --git a/coderd/debug.go b/coderd/debug.go index a95738d351a19..6c99e51ce3225 100644 --- a/coderd/debug.go +++ b/coderd/debug.go @@ -65,3 +65,17 @@ func (api *API) debugDeploymentHealth(rw http.ResponseWriter, r *http.Request) { return } } + +// @Summary Debug Websocket +// @ID debug-info-websocket-test +// @Security CoderSessionToken +// @Produce json +// @Tags Debug +// @Success 201 {object} codersdk.Response +// @Router /debug/ws [get] +// @x-apidocgen {"skip": true} +// +// For some reason the swagger docs need to be attached to a function. +// +//nolint:unused +func _debugws(http.ResponseWriter, *http.Request) {} From e86279c6fdf47804d9f87a52f138a999c52194fd Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Tue, 30 May 2023 17:45:35 +0000 Subject: [PATCH 4/8] fixup! feat(healthcheck): add websocket report --- coderd/apidoc/docs.go | 14 ++++++++++---- coderd/apidoc/swagger.json | 14 ++++++++++---- coderd/debug.go | 2 +- coderd/healthcheck/websocket.go | 24 +++++++++++++++++++++--- docs/api/debug.md | 4 +++- docs/api/schemas.md | 16 +++++++++++----- 6 files changed, 56 insertions(+), 18 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 62f9612ebee6e..b25ed5deed007 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -440,11 +440,11 @@ const docTemplate = `{ "tags": [ "Debug" ], - "summary": "Debug Websocket", + "summary": "Debug Info Websocket Test", "operationId": "debug-info-websocket-test", "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { "$ref": "#/definitions/codersdk.Response" } @@ -10462,7 +10462,13 @@ const docTemplate = `{ "healthcheck.WebsocketReport": { "type": "object", "properties": { - "error": {} + "code": { + "type": "integer" + }, + "error": {}, + "response": { + "type": "string" + } } }, "netcheck.Report": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index e511e6d59adcd..2130adccfa960 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -372,11 +372,11 @@ ], "produces": ["application/json"], "tags": ["Debug"], - "summary": "Debug Websocket", + "summary": "Debug Info Websocket Test", "operationId": "debug-info-websocket-test", "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { "$ref": "#/definitions/codersdk.Response" } @@ -9445,7 +9445,13 @@ "healthcheck.WebsocketReport": { "type": "object", "properties": { - "error": {} + "code": { + "type": "integer" + }, + "error": {}, + "response": { + "type": "string" + } } }, "netcheck.Report": { diff --git a/coderd/debug.go b/coderd/debug.go index 6c99e51ce3225..94b8138016a77 100644 --- a/coderd/debug.go +++ b/coderd/debug.go @@ -66,7 +66,7 @@ func (api *API) debugDeploymentHealth(rw http.ResponseWriter, r *http.Request) { } } -// @Summary Debug Websocket +// @Summary Debug Info Websocket Test // @ID debug-info-websocket-test // @Security CoderSessionToken // @Produce json diff --git a/coderd/healthcheck/websocket.go b/coderd/healthcheck/websocket.go index 1bdb72501c38f..3f8d47ff0ebba 100644 --- a/coderd/healthcheck/websocket.go +++ b/coderd/healthcheck/websocket.go @@ -21,7 +21,13 @@ type WebsocketReportOptions struct { } type WebsocketReport struct { - Error error `json:"error"` + Response WebsocketResponse `json:"response"` + Error error `json:"error"` +} + +type WebsocketResponse struct { + Body string `json:"body"` + Code int `json:"code"` } func (r *WebsocketReport) Run(ctx context.Context, opts *WebsocketReportOptions) { @@ -39,11 +45,23 @@ func (r *WebsocketReport) Run(ctx context.Context, opts *WebsocketReportOptions) u.Scheme = "ws" } - //nolint:bodyclose - c, _, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{ + //nolint:bodyclose // websocket package closes this for you + c, res, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{ HTTPClient: opts.HTTPClient, HTTPHeader: http.Header{"Coder-Session-Token": []string{opts.APIKey}}, }) + if res != nil { + var body string + b, err := io.ReadAll(res.Body) + if err == nil { + body = string(b) + } + + r.Response = WebsocketResponse{ + Body: body, + Code: res.StatusCode, + } + } if err != nil { r.Error = xerrors.Errorf("websocket dial: %w", err) return diff --git a/docs/api/debug.md b/docs/api/debug.md index 41fe31e9dd4f5..5854942464097 100644 --- a/docs/api/debug.md +++ b/docs/api/debug.md @@ -209,7 +209,9 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "pass": true, "time": "string", "websocket": { - "error": null + "code": 0, + "error": null, + "response": "string" } } ``` diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 0ade75c9d620b..0e8db6dfd5563 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -6395,7 +6395,9 @@ Parameter represents a set value for the scope. "pass": true, "time": "string", "websocket": { - "error": null + "code": 0, + "error": null, + "response": "string" } } ``` @@ -6414,15 +6416,19 @@ Parameter represents a set value for the scope. ```json { - "error": null + "code": 0, + "error": null, + "response": "string" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| ------- | ---- | -------- | ------------ | ----------- | -| `error` | any | false | | | +| Name | Type | Required | Restrictions | Description | +| ---------- | ------- | -------- | ------------ | ----------- | +| `code` | integer | false | | | +| `error` | any | false | | | +| `response` | string | false | | | ## netcheck.Report From ab3f2388e9cf06c97a606fbe6fe5d9192380d2eb Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Tue, 30 May 2023 18:01:54 +0000 Subject: [PATCH 5/8] fixup! feat(healthcheck): add websocket report --- coderd/apidoc/docs.go | 14 +++++++++++--- coderd/apidoc/swagger.json | 14 +++++++++++--- coderd/debug.go | 8 +++----- docs/api/debug.md | 6 ++++-- docs/api/schemas.md | 37 ++++++++++++++++++++++++++++--------- 5 files changed, 57 insertions(+), 22 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index b25ed5deed007..fb343ce01a2b4 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -10462,12 +10462,20 @@ const docTemplate = `{ "healthcheck.WebsocketReport": { "type": "object", "properties": { - "code": { - "type": "integer" - }, "error": {}, "response": { + "$ref": "#/definitions/healthcheck.WebsocketResponse" + } + } + }, + "healthcheck.WebsocketResponse": { + "type": "object", + "properties": { + "body": { "type": "string" + }, + "code": { + "type": "integer" } } }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 2130adccfa960..e2ae6b5a19d89 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -9445,12 +9445,20 @@ "healthcheck.WebsocketReport": { "type": "object", "properties": { - "code": { - "type": "integer" - }, "error": {}, "response": { + "$ref": "#/definitions/healthcheck.WebsocketResponse" + } + } + }, + "healthcheck.WebsocketResponse": { + "type": "object", + "properties": { + "body": { "type": "string" + }, + "code": { + "type": "integer" } } }, diff --git a/coderd/debug.go b/coderd/debug.go index 94b8138016a77..ef9a0018210d1 100644 --- a/coderd/debug.go +++ b/coderd/debug.go @@ -66,6 +66,8 @@ func (api *API) debugDeploymentHealth(rw http.ResponseWriter, r *http.Request) { } } +// For some reason the swagger docs need to be attached to a function. +// // @Summary Debug Info Websocket Test // @ID debug-info-websocket-test // @Security CoderSessionToken @@ -74,8 +76,4 @@ func (api *API) debugDeploymentHealth(rw http.ResponseWriter, r *http.Request) { // @Success 201 {object} codersdk.Response // @Router /debug/ws [get] // @x-apidocgen {"skip": true} -// -// For some reason the swagger docs need to be attached to a function. -// -//nolint:unused -func _debugws(http.ResponseWriter, *http.Request) {} +func _debugws(http.ResponseWriter, *http.Request) {} //nolint:unused diff --git a/docs/api/debug.md b/docs/api/debug.md index 5854942464097..d425cb4a5dd13 100644 --- a/docs/api/debug.md +++ b/docs/api/debug.md @@ -209,9 +209,11 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "pass": true, "time": "string", "websocket": { - "code": 0, "error": null, - "response": "string" + "response": { + "body": "string", + "code": 0 + } } } ``` diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 0e8db6dfd5563..914e98cd84aef 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -6395,9 +6395,11 @@ Parameter represents a set value for the scope. "pass": true, "time": "string", "websocket": { - "code": 0, "error": null, - "response": "string" + "response": { + "body": "string", + "code": 0 + } } } ``` @@ -6416,19 +6418,36 @@ Parameter represents a set value for the scope. ```json { - "code": 0, "error": null, - "response": "string" + "response": { + "body": "string", + "code": 0 + } } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| ---------- | ------- | -------- | ------------ | ----------- | -| `code` | integer | false | | | -| `error` | any | false | | | -| `response` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| ---------- | -------------------------------------------------------------- | -------- | ------------ | ----------- | +| `error` | any | false | | | +| `response` | [healthcheck.WebsocketResponse](#healthcheckwebsocketresponse) | false | | | + +## healthcheck.WebsocketResponse + +```json +{ + "body": "string", + "code": 0 +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------ | ------- | -------- | ------------ | ----------- | +| `body` | string | false | | | +| `code` | integer | false | | | ## netcheck.Report From 0ed839b5027f2c6783ffca61080513ca5b3cf60b Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Tue, 30 May 2023 18:15:52 +0000 Subject: [PATCH 6/8] fixup! feat(healthcheck): add websocket report --- coderd/healthcheck/websocket.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/healthcheck/websocket.go b/coderd/healthcheck/websocket.go index 3f8d47ff0ebba..02a64d59daccc 100644 --- a/coderd/healthcheck/websocket.go +++ b/coderd/healthcheck/websocket.go @@ -102,7 +102,7 @@ func (WebsocketEchoServer) ServeHTTP(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() c, err := websocket.Accept(rw, r, &websocket.AcceptOptions{}) if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, "Invalid websocket.") + httpapi.Write(ctx, rw, http.StatusBadRequest, "unable to accept: "+err.Error()) return } defer c.Close(websocket.StatusGoingAway, "goodbye") From b3eb880e93aa85c0ba2c58931f52fce7986101d5 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Tue, 30 May 2023 18:16:34 +0000 Subject: [PATCH 7/8] fixup! feat(healthcheck): add websocket report --- coderd/healthcheck/websocket.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/coderd/healthcheck/websocket.go b/coderd/healthcheck/websocket.go index 02a64d59daccc..214126c713cae 100644 --- a/coderd/healthcheck/websocket.go +++ b/coderd/healthcheck/websocket.go @@ -52,9 +52,11 @@ func (r *WebsocketReport) Run(ctx context.Context, opts *WebsocketReportOptions) }) if res != nil { var body string - b, err := io.ReadAll(res.Body) - if err == nil { - body = string(b) + if res.Body != nil { + b, err := io.ReadAll(res.Body) + if err == nil { + body = string(b) + } } r.Response = WebsocketResponse{ From b1a09b4c6aee886ddf8e2d8655588cf1f6f87dcb Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Tue, 30 May 2023 19:06:47 +0000 Subject: [PATCH 8/8] fixup! feat(healthcheck): add websocket report --- coderd/coderd.go | 2 +- coderd/healthcheck/websocket.go | 13 +++++++++-- coderd/healthcheck/websocket_test.go | 32 +++++++++++++++++++++++++++- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 9a1fdd81c59fa..d126f6bf63a68 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -788,7 +788,7 @@ func New(options *Options) *API { r.Get("/coordinator", api.debugCoordinator) r.Get("/health", api.debugDeploymentHealth) - r.Get("/ws", healthcheck.WebsocketEchoServer{}.ServeHTTP) + r.Get("/ws", (&healthcheck.WebsocketEchoServer{}).ServeHTTP) }) }) diff --git a/coderd/healthcheck/websocket.go b/coderd/healthcheck/websocket.go index 214126c713cae..cbab40e054e79 100644 --- a/coderd/healthcheck/websocket.go +++ b/coderd/healthcheck/websocket.go @@ -98,9 +98,18 @@ func (r *WebsocketReport) Run(ctx context.Context, opts *WebsocketReportOptions) c.Close(websocket.StatusGoingAway, "goodbye") } -type WebsocketEchoServer struct{} +type WebsocketEchoServer struct { + Error error + Code int +} + +func (s *WebsocketEchoServer) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + if s.Error != nil { + rw.WriteHeader(s.Code) + _, _ = rw.Write([]byte(s.Error.Error())) + return + } -func (WebsocketEchoServer) ServeHTTP(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() c, err := websocket.Accept(rw, r, &websocket.AcceptOptions{}) if err != nil { diff --git a/coderd/healthcheck/websocket_test.go b/coderd/healthcheck/websocket_test.go index c4408f8a262f1..6b5f17fc24c2b 100644 --- a/coderd/healthcheck/websocket_test.go +++ b/coderd/healthcheck/websocket_test.go @@ -2,11 +2,14 @@ package healthcheck_test import ( "context" + "net/http" "net/http/httptest" "net/url" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/xerrors" "github.com/coder/coder/coderd/healthcheck" "github.com/coder/coder/testutil" @@ -18,7 +21,7 @@ func TestWebsocket(t *testing.T) { t.Run("OK", func(t *testing.T) { t.Parallel() - srv := httptest.NewServer(healthcheck.WebsocketEchoServer{}) + srv := httptest.NewServer(&healthcheck.WebsocketEchoServer{}) defer srv.Close() ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) @@ -36,4 +39,31 @@ func TestWebsocket(t *testing.T) { require.NoError(t, wsReport.Error) }) + + t.Run("Error", func(t *testing.T) { + t.Parallel() + + srv := httptest.NewServer(&healthcheck.WebsocketEchoServer{ + Error: xerrors.New("test error"), + Code: http.StatusBadRequest, + }) + defer srv.Close() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + u, err := url.Parse(srv.URL) + require.NoError(t, err) + + wsReport := healthcheck.WebsocketReport{} + wsReport.Run(ctx, &healthcheck.WebsocketReportOptions{ + AccessURL: u, + HTTPClient: srv.Client(), + APIKey: "test", + }) + + require.Error(t, wsReport.Error) + assert.Equal(t, wsReport.Response.Body, "test error") + assert.Equal(t, wsReport.Response.Code, http.StatusBadRequest) + }) } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

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:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy