From eb4fd94274725fc430f7c93bac3ea06a69922a78 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 24 Nov 2023 16:34:36 +0100 Subject: [PATCH 01/14] Stub for debug/health/settings --- coderd/coderd.go | 8 +++++++- coderd/debug.go | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 8b54b708cc85d..d3888ced6181b 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -956,7 +956,13 @@ func New(options *Options) *API { r.Get("/coordinator", api.debugCoordinator) r.Get("/tailnet", api.debugTailnet) - r.Get("/health", api.debugDeploymentHealth) + r.Route("/health", func(r chi.Router) { + r.Get("/", api.debugDeploymentHealth) + r.Route("/settings", func(r chi.Router) { + r.Get("/", api.deploymentHealthSettings) + r.Put("/", api.putDeploymentHealthSettings) + }) + }) r.Get("/ws", (&healthcheck.WebsocketEchoServer{}).ServeHTTP) }) }) diff --git a/coderd/debug.go b/coderd/debug.go index ba6eaf9696d99..b5a057e61e0d3 100644 --- a/coderd/debug.go +++ b/coderd/debug.go @@ -82,6 +82,30 @@ func (api *API) debugDeploymentHealth(rw http.ResponseWriter, r *http.Request) { } } +// @Summary Get health settings +// @ID get-health-settings +// @Security CoderSessionToken +// @Produce json +// @Tags Debug +// @Success 200 {object} codersdk.HealthSettings +// @Router /debug/health/settings [get] +func (api *API) deploymentHealthSettings(rw http.ResponseWriter, r *http.Request) { + // TODO +} + +// @Summary Update health settings +// @ID update-health-settings +// @Security CoderSessionToken +// @Accept json +// @Produce json +// @Tags Debug +// @Param request body codersdk.UpdateHealthSettings true "Update health settings" +// @Success 200 {object} codersdk.UpdateHealthSettings +// @Router /debug/health/settings [put] +func (api *API) putDeploymentHealthSettings(rw http.ResponseWriter, r *http.Request) { + // TODO +} + func formatHealthcheck(ctx context.Context, rw http.ResponseWriter, r *http.Request, hc *healthcheck.Report) { format := r.URL.Query().Get("format") switch format { From a820c1bc25d9dab4dd8af0d867b2850281cbc153 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 24 Nov 2023 17:17:39 +0100 Subject: [PATCH 02/14] stub for HealthSettings API --- coderd/apidoc/docs.go | 84 ++++++++++++++++++++++++++++++++++ coderd/apidoc/swagger.json | 74 ++++++++++++++++++++++++++++++ codersdk/health.go | 9 ++++ docs/api/debug.md | 77 +++++++++++++++++++++++++++++++ docs/api/schemas.md | 28 ++++++++++++ site/src/api/typesGenerated.ts | 10 ++++ 6 files changed, 282 insertions(+) create mode 100644 codersdk/health.go diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index ed7968577b455..0fc8905931244 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -430,6 +430,68 @@ const docTemplate = `{ } } }, + "/debug/health/settings": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Debug" + ], + "summary": "Get health settings", + "operationId": "get-health-settings", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.HealthSettings" + } + } + } + }, + "put": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Debug" + ], + "summary": "Update health settings", + "operationId": "update-health-settings", + "parameters": [ + { + "description": "Update health settings", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.UpdateHealthSettings" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.UpdateHealthSettings" + } + } + } + } + }, "/debug/tailnet": { "get": { "security": [ @@ -8873,6 +8935,17 @@ const docTemplate = `{ "GroupSourceOIDC" ] }, + "codersdk.HealthSettings": { + "type": "object", + "properties": { + "dismissed_healthchecks": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "codersdk.Healthcheck": { "type": "object", "properties": { @@ -10738,6 +10811,17 @@ const docTemplate = `{ } } }, + "codersdk.UpdateHealthSettings": { + "type": "object", + "properties": { + "dismissed_healthchecks": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "codersdk.UpdateRoles": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index f92e11b92609f..776ed4c20f7ce 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -366,6 +366,58 @@ } } }, + "/debug/health/settings": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Debug"], + "summary": "Get health settings", + "operationId": "get-health-settings", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.HealthSettings" + } + } + } + }, + "put": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Debug"], + "summary": "Update health settings", + "operationId": "update-health-settings", + "parameters": [ + { + "description": "Update health settings", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.UpdateHealthSettings" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.UpdateHealthSettings" + } + } + } + } + }, "/debug/tailnet": { "get": { "security": [ @@ -7971,6 +8023,17 @@ "enum": ["user", "oidc"], "x-enum-varnames": ["GroupSourceUser", "GroupSourceOIDC"] }, + "codersdk.HealthSettings": { + "type": "object", + "properties": { + "dismissed_healthchecks": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "codersdk.Healthcheck": { "type": "object", "properties": { @@ -9723,6 +9786,17 @@ } } }, + "codersdk.UpdateHealthSettings": { + "type": "object", + "properties": { + "dismissed_healthchecks": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "codersdk.UpdateRoles": { "type": "object", "properties": { diff --git a/codersdk/health.go b/codersdk/health.go new file mode 100644 index 0000000000000..9f1d869b91ee7 --- /dev/null +++ b/codersdk/health.go @@ -0,0 +1,9 @@ +package codersdk + +type HealthSettings struct { + DismissedHealthchecks []string `json:"dismissed_healthchecks"` +} + +type UpdateHealthSettings struct { + DismissedHealthchecks []string `json:"dismissed_healthchecks"` +} diff --git a/docs/api/debug.md b/docs/api/debug.md index c2a26321716d3..6ba3ce3c37bd5 100644 --- a/docs/api/debug.md +++ b/docs/api/debug.md @@ -265,6 +265,83 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Get health settings + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/debug/health/settings \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /debug/health/settings` + +### Example responses + +> 200 Response + +```json +{ + "dismissed_healthchecks": ["string"] +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------ | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.HealthSettings](schemas.md#codersdkhealthsettings) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Update health settings + +### Code samples + +```shell +# Example request using curl +curl -X PUT http://coder-server:8080/api/v2/debug/health/settings \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PUT /debug/health/settings` + +> Body parameter + +```json +{ + "dismissed_healthchecks": ["string"] +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +| ------ | ---- | ------------------------------------------------------------------------ | -------- | ---------------------- | +| `body` | body | [codersdk.UpdateHealthSettings](schemas.md#codersdkupdatehealthsettings) | true | Update health settings | + +### Example responses + +> 200 Response + +```json +{ + "dismissed_healthchecks": ["string"] +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------------ | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.UpdateHealthSettings](schemas.md#codersdkupdatehealthsettings) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Debug Info Tailnet ### Code samples diff --git a/docs/api/schemas.md b/docs/api/schemas.md index c4dc42883987d..af9c9da91954e 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -3166,6 +3166,20 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `user` | | `oidc` | +## codersdk.HealthSettings + +```json +{ + "dismissed_healthchecks": ["string"] +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------------------------ | --------------- | -------- | ------------ | ----------- | +| `dismissed_healthchecks` | array of string | false | | | + ## codersdk.Healthcheck ```json @@ -5157,6 +5171,20 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `url` | string | false | | URL to download the latest release of Coder. | | `version` | string | false | | Version is the semantic version for the latest release of Coder. | +## codersdk.UpdateHealthSettings + +```json +{ + "dismissed_healthchecks": ["string"] +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------------------------ | --------------- | -------- | ------------ | ----------- | +| `dismissed_healthchecks` | array of string | false | | | + ## codersdk.UpdateRoles ```json diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index ccf06da3f8b47..76e8c2dce5198 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -542,6 +542,11 @@ export interface Group { readonly source: GroupSource; } +// From codersdk/health.go +export interface HealthSettings { + readonly dismissed_healthchecks: string[]; +} + // From codersdk/workspaceapps.go export interface Healthcheck { readonly url: string; @@ -1155,6 +1160,11 @@ export interface UpdateCheckResponse { readonly url: string; } +// From codersdk/health.go +export interface UpdateHealthSettings { + readonly dismissed_healthchecks: string[]; +} + // From codersdk/users.go export interface UpdateRoles { readonly roles: string[]; From a01878ecce9e632ebd146741e9ab0e0e2d25a41b Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 28 Nov 2023 11:41:03 +0100 Subject: [PATCH 03/14] API --- coderd/debug.go | 120 ++++++++++++++++++++++++------ coderd/healthcheck/healthcheck.go | 2 + 2 files changed, 101 insertions(+), 21 deletions(-) diff --git a/coderd/debug.go b/coderd/debug.go index b5a057e61e0d3..37aae1042a7a9 100644 --- a/coderd/debug.go +++ b/coderd/debug.go @@ -2,6 +2,7 @@ package coderd import ( "context" + "encoding/json" "fmt" "net/http" "time" @@ -9,7 +10,10 @@ import ( "github.com/coder/coder/v2/coderd/healthcheck" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" + "golang.org/x/exp/slices" + "golang.org/x/xerrors" ) // @Summary Debug Info Wireguard Coordinator @@ -82,6 +86,31 @@ func (api *API) debugDeploymentHealth(rw http.ResponseWriter, r *http.Request) { } } +func formatHealthcheck(ctx context.Context, rw http.ResponseWriter, r *http.Request, hc *healthcheck.Report) { + format := r.URL.Query().Get("format") + switch format { + case "text": + rw.Header().Set("Content-Type", "text/plain; charset=utf-8") + rw.WriteHeader(http.StatusOK) + + _, _ = fmt.Fprintln(rw, "time:", hc.Time.Format(time.RFC3339)) + _, _ = fmt.Fprintln(rw, "healthy:", hc.Healthy) + _, _ = fmt.Fprintln(rw, "derp:", hc.DERP.Healthy) + _, _ = fmt.Fprintln(rw, "access_url:", hc.AccessURL.Healthy) + _, _ = fmt.Fprintln(rw, "websocket:", hc.Websocket.Healthy) + _, _ = fmt.Fprintln(rw, "database:", hc.Database.Healthy) + + case "", "json": + httpapi.WriteIndent(ctx, rw, http.StatusOK, hc) + + default: + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: fmt.Sprintf("Invalid format option %q.", format), + Detail: "Allowed values are: \"json\", \"simple\".", + }) + } +} + // @Summary Get health settings // @ID get-health-settings // @Security CoderSessionToken @@ -90,7 +119,30 @@ func (api *API) debugDeploymentHealth(rw http.ResponseWriter, r *http.Request) { // @Success 200 {object} codersdk.HealthSettings // @Router /debug/health/settings [get] func (api *API) deploymentHealthSettings(rw http.ResponseWriter, r *http.Request) { - // TODO + settingsJSON, err := api.Database.GetHealthSettings(r.Context()) + if err != nil { + httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to fetch health settings.", + Detail: err.Error(), + }) + return + } + + var settings codersdk.HealthSettings + err = json.Unmarshal([]byte(settingsJSON), &settings) + if err != nil { + httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to unmarshal health settings.", + Detail: err.Error(), + }) + return + } + + if len(settings.DismissedHealthchecks) == 0 { + settings.DismissedHealthchecks = []string{} + } + + httpapi.Write(r.Context(), rw, http.StatusOK, settings) } // @Summary Update health settings @@ -103,32 +155,58 @@ func (api *API) deploymentHealthSettings(rw http.ResponseWriter, r *http.Request // @Success 200 {object} codersdk.UpdateHealthSettings // @Router /debug/health/settings [put] func (api *API) putDeploymentHealthSettings(rw http.ResponseWriter, r *http.Request) { - // TODO -} + ctx := r.Context() -func formatHealthcheck(ctx context.Context, rw http.ResponseWriter, r *http.Request, hc *healthcheck.Report) { - format := r.URL.Query().Get("format") - switch format { - case "text": - rw.Header().Set("Content-Type", "text/plain; charset=utf-8") - rw.WriteHeader(http.StatusOK) + if !api.Authorize(r, rbac.ActionUpdate, rbac.ResourceDeploymentValues) { + httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ + Message: "Insufficient permissions to update health settings.", + }) + return + } - _, _ = fmt.Fprintln(rw, "time:", hc.Time.Format(time.RFC3339)) - _, _ = fmt.Fprintln(rw, "healthy:", hc.Healthy) - _, _ = fmt.Fprintln(rw, "derp:", hc.DERP.Healthy) - _, _ = fmt.Fprintln(rw, "access_url:", hc.AccessURL.Healthy) - _, _ = fmt.Fprintln(rw, "websocket:", hc.Websocket.Healthy) - _, _ = fmt.Fprintln(rw, "database:", hc.Database.Healthy) + var settings codersdk.HealthSettings + if !httpapi.Read(ctx, rw, r, &settings) { + return + } - case "", "json": - httpapi.WriteIndent(ctx, rw, http.StatusOK, hc) + err := validateHealthSettings(settings) + if err != nil { + httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to validate health settings.", + Detail: err.Error(), + }) + return + } - default: - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: fmt.Sprintf("Invalid format option %q.", format), - Detail: "Allowed values are: \"json\", \"simple\".", + settingsJSON, err := json.Marshal(&settings) + if err != nil { + httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to marshal health settings.", + Detail: err.Error(), + }) + return + } + + err = api.Database.UpsertHealthSettings(ctx, string(settingsJSON)) + if err != nil { + httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to update health settings.", + Detail: err.Error(), }) + return + } + + httpapi.Write(r.Context(), rw, http.StatusOK, settings) +} + +func validateHealthSettings(settings codersdk.HealthSettings) error { + for _, dismissed := range settings.DismissedHealthchecks { + ok := slices.Contains(healthcheck.Sections, dismissed) + if !ok { + return xerrors.Errorf("unknown healthcheck section: %s", dismissed) + } } + return nil } // For some reason the swagger docs need to be attached to a function. diff --git a/coderd/healthcheck/healthcheck.go b/coderd/healthcheck/healthcheck.go index 9233626419634..9ecb9b9d13a44 100644 --- a/coderd/healthcheck/healthcheck.go +++ b/coderd/healthcheck/healthcheck.go @@ -20,6 +20,8 @@ const ( SectionWorkspaceProxy string = "WorkspaceProxy" ) +var Sections = []string{SectionAccessURL, SectionDatabase, SectionDERP, SectionWebsocket, SectionWorkspaceProxy} + type Checker interface { DERP(ctx context.Context, opts *derphealth.ReportOptions) derphealth.Report AccessURL(ctx context.Context, opts *AccessURLReportOptions) AccessURLReport From 36b8c1c209d650a126ded5d98e59fd43ee8a835e Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 28 Nov 2023 12:34:29 +0100 Subject: [PATCH 04/14] API tests --- coderd/debug.go | 5 +++-- coderd/debug_test.go | 47 ++++++++++++++++++++++++++++++++++++++++++++ codersdk/health.go | 31 +++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/coderd/debug.go b/coderd/debug.go index 37aae1042a7a9..4165914be0a3f 100644 --- a/coderd/debug.go +++ b/coderd/debug.go @@ -7,13 +7,14 @@ import ( "net/http" "time" + "golang.org/x/exp/slices" + "golang.org/x/xerrors" + "github.com/coder/coder/v2/coderd/healthcheck" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" - "golang.org/x/exp/slices" - "golang.org/x/xerrors" ) // @Summary Debug Info Wireguard Coordinator diff --git a/coderd/debug_test.go b/coderd/debug_test.go index 186539b82d90f..59ebd3d3a8af4 100644 --- a/coderd/debug_test.go +++ b/coderd/debug_test.go @@ -14,6 +14,7 @@ import ( "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/healthcheck" "github.com/coder/coder/v2/coderd/healthcheck/derphealth" + "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" ) @@ -232,6 +233,52 @@ func TestDebugHealth(t *testing.T) { }) } +func TestHealthSettings(t *testing.T) { + t.Parallel() + + t.Run("InitialState", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + // given + adminClient := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, adminClient) + + // when + settings, err := adminClient.HealthSettings(ctx) + require.NoError(t, err) + + // then + require.Equal(t, codersdk.HealthSettings{DismissedHealthchecks: []string{}}, settings) + }) + + t.Run("Updated", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + // given + adminClient := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, adminClient) + + expected := codersdk.HealthSettings{ + DismissedHealthchecks: []string{healthcheck.SectionDERP, healthcheck.SectionWebsocket}, + } + + // when + err := adminClient.UpdateHealthSettings(ctx, expected) + require.NoError(t, err) + + // then + settings, err := adminClient.HealthSettings(ctx) + require.NoError(t, err) + require.Equal(t, expected, settings) + }) +} + func TestDebugWebsocket(t *testing.T) { t.Parallel() diff --git a/codersdk/health.go b/codersdk/health.go index 9f1d869b91ee7..47a421d6a0452 100644 --- a/codersdk/health.go +++ b/codersdk/health.go @@ -1,5 +1,11 @@ package codersdk +import ( + "context" + "encoding/json" + "net/http" +) + type HealthSettings struct { DismissedHealthchecks []string `json:"dismissed_healthchecks"` } @@ -7,3 +13,28 @@ type HealthSettings struct { type UpdateHealthSettings struct { DismissedHealthchecks []string `json:"dismissed_healthchecks"` } + +func (c *Client) HealthSettings(ctx context.Context) (HealthSettings, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/debug/health/settings", nil) + if err != nil { + return HealthSettings{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return HealthSettings{}, ReadBodyAsError(res) + } + var settings HealthSettings + return settings, json.NewDecoder(res.Body).Decode(&settings) +} + +func (c *Client) UpdateHealthSettings(ctx context.Context, settings HealthSettings) error { + res, err := c.Request(ctx, http.MethodPut, "/api/v2/debug/health/settings", settings) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return ReadBodyAsError(res) + } + return nil +} From 7f43838cf82dc031f4bbab21142ef9b9a484656c Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 28 Nov 2023 14:34:54 +0100 Subject: [PATCH 05/14] WIP --- coderd/healthcheck/accessurl.go | 9 ++++++--- coderd/healthcheck/database.go | 9 ++++++--- coderd/healthcheck/derphealth/derp.go | 4 ++++ coderd/healthcheck/healthcheck.go | 3 +++ coderd/healthcheck/websocket.go | 9 ++++++--- coderd/healthcheck/workspaceproxy.go | 11 +++++++---- 6 files changed, 32 insertions(+), 13 deletions(-) diff --git a/coderd/healthcheck/accessurl.go b/coderd/healthcheck/accessurl.go index 110ec3486a896..cc4153bfe7c89 100644 --- a/coderd/healthcheck/accessurl.go +++ b/coderd/healthcheck/accessurl.go @@ -16,9 +16,10 @@ import ( // @typescript-generate AccessURLReport type AccessURLReport struct { // Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead. - Healthy bool `json:"healthy"` - Severity health.Severity `json:"severity" enums:"ok,warning,error"` - Warnings []string `json:"warnings"` + Healthy bool `json:"healthy"` + Severity health.Severity `json:"severity" enums:"ok,warning,error"` + Warnings []string `json:"warnings"` + Dismissed bool `json:"dismissed` AccessURL string `json:"access_url"` Reachable bool `json:"reachable"` @@ -30,6 +31,8 @@ type AccessURLReport struct { type AccessURLReportOptions struct { AccessURL *url.URL Client *http.Client + + Dismissed bool } func (r *AccessURLReport) Run(ctx context.Context, opts *AccessURLReportOptions) { diff --git a/coderd/healthcheck/database.go b/coderd/healthcheck/database.go index 07120ad533054..db63c6a783382 100644 --- a/coderd/healthcheck/database.go +++ b/coderd/healthcheck/database.go @@ -18,9 +18,10 @@ const ( // @typescript-generate DatabaseReport type DatabaseReport struct { // Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead. - Healthy bool `json:"healthy"` - Severity health.Severity `json:"severity" enums:"ok,warning,error"` - Warnings []string `json:"warnings"` + Healthy bool `json:"healthy"` + Severity health.Severity `json:"severity" enums:"ok,warning,error"` + Warnings []string `json:"warnings"` + Dismissed bool `json:"dismissed` Reachable bool `json:"reachable"` Latency string `json:"latency"` @@ -32,6 +33,8 @@ type DatabaseReport struct { type DatabaseReportOptions struct { DB database.Store Threshold time.Duration + + Dismissed bool } func (r *DatabaseReport) Run(ctx context.Context, opts *DatabaseReportOptions) { diff --git a/coderd/healthcheck/derphealth/derp.go b/coderd/healthcheck/derphealth/derp.go index 4f6271cfc8169..e636ad95273c1 100644 --- a/coderd/healthcheck/derphealth/derp.go +++ b/coderd/healthcheck/derphealth/derp.go @@ -47,6 +47,8 @@ type Report struct { NetcheckLogs []string `json:"netcheck_logs"` Error *string `json:"error"` + + Dismissed bool `json:"dismissed` } // @typescript-generate RegionReport @@ -95,6 +97,8 @@ type StunReport struct { } type ReportOptions struct { + Dismissed bool + DERPMap *tailcfg.DERPMap } diff --git a/coderd/healthcheck/healthcheck.go b/coderd/healthcheck/healthcheck.go index 9ecb9b9d13a44..e6bf7bf0df983 100644 --- a/coderd/healthcheck/healthcheck.go +++ b/coderd/healthcheck/healthcheck.go @@ -7,6 +7,7 @@ import ( "time" "github.com/coder/coder/v2/buildinfo" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/healthcheck/derphealth" "github.com/coder/coder/v2/coderd/healthcheck/health" "github.com/coder/coder/v2/coderd/util/ptr" @@ -60,6 +61,8 @@ type ReportOptions struct { WorkspaceProxy WorkspaceProxyReportOptions Checker Checker + + DB database.Store } type defaultChecker struct{} diff --git a/coderd/healthcheck/websocket.go b/coderd/healthcheck/websocket.go index c49ba1b2be10d..36dbc0368e4c1 100644 --- a/coderd/healthcheck/websocket.go +++ b/coderd/healthcheck/websocket.go @@ -19,14 +19,17 @@ type WebsocketReportOptions struct { APIKey string AccessURL *url.URL HTTPClient *http.Client + + Dismissed bool } // @typescript-generate WebsocketReport type WebsocketReport struct { // Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead. - Healthy bool `json:"healthy"` - Severity health.Severity `json:"severity" enums:"ok,warning,error"` - Warnings []string `json:"warnings"` + Healthy bool `json:"healthy"` + Severity health.Severity `json:"severity" enums:"ok,warning,error"` + Warnings []string `json:"warnings"` + Dismissed bool `json:"dismissed` Body string `json:"body"` Code int `json:"code"` diff --git a/coderd/healthcheck/workspaceproxy.go b/coderd/healthcheck/workspaceproxy.go index 9acd527b942e2..71936dea557c8 100644 --- a/coderd/healthcheck/workspaceproxy.go +++ b/coderd/healthcheck/workspaceproxy.go @@ -19,14 +19,17 @@ type WorkspaceProxyReportOptions struct { // We pass this in to make it easier to test. CurrentVersion string WorkspaceProxiesFetchUpdater WorkspaceProxiesFetchUpdater + + Dismissed bool } // @typescript-generate WorkspaceProxyReport type WorkspaceProxyReport struct { - Healthy bool `json:"healthy"` - Severity health.Severity `json:"severity"` - Warnings []string `json:"warnings"` - Error *string `json:"error"` + Healthy bool `json:"healthy"` + Severity health.Severity `json:"severity"` + Warnings []string `json:"warnings"` + Error *string `json:"error"` + Dismissed bool `json:"dismissed` WorkspaceProxies codersdk.RegionsResponse[codersdk.WorkspaceProxy] `json:"workspace_proxies"` } From 29a5cda17955b81a8a0799af76a41cce406574c8 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 29 Nov 2023 13:41:19 +0100 Subject: [PATCH 06/14] Dismissed property --- coderd/healthcheck/accessurl.go | 2 ++ coderd/healthcheck/database.go | 2 ++ coderd/healthcheck/derphealth/derp.go | 12 ++++++------ coderd/healthcheck/websocket.go | 18 ++++++++++-------- coderd/healthcheck/workspaceproxy.go | 19 ++++++++++--------- 5 files changed, 30 insertions(+), 23 deletions(-) diff --git a/coderd/healthcheck/accessurl.go b/coderd/healthcheck/accessurl.go index cc4153bfe7c89..e893bbadb7621 100644 --- a/coderd/healthcheck/accessurl.go +++ b/coderd/healthcheck/accessurl.go @@ -41,6 +41,8 @@ func (r *AccessURLReport) Run(ctx context.Context, opts *AccessURLReportOptions) r.Severity = health.SeverityOK r.Warnings = []string{} + r.Dismissed = opts.Dismissed + if opts.AccessURL == nil { r.Error = ptr.Ref("access URL is nil") r.Severity = health.SeverityError diff --git a/coderd/healthcheck/database.go b/coderd/healthcheck/database.go index db63c6a783382..3744f13a0b63e 100644 --- a/coderd/healthcheck/database.go +++ b/coderd/healthcheck/database.go @@ -40,6 +40,8 @@ type DatabaseReportOptions struct { func (r *DatabaseReport) Run(ctx context.Context, opts *DatabaseReportOptions) { r.Warnings = []string{} r.Severity = health.SeverityOK + r.Dismissed = opts.Dismissed + r.ThresholdMS = opts.Threshold.Milliseconds() if r.ThresholdMS == 0 { r.ThresholdMS = DatabaseDefaultThreshold.Milliseconds() diff --git a/coderd/healthcheck/derphealth/derp.go b/coderd/healthcheck/derphealth/derp.go index e636ad95273c1..912a5d0c81199 100644 --- a/coderd/healthcheck/derphealth/derp.go +++ b/coderd/healthcheck/derphealth/derp.go @@ -36,9 +36,10 @@ const ( // @typescript-generate Report type Report struct { // Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead. - Healthy bool `json:"healthy"` - Severity health.Severity `json:"severity" enums:"ok,warning,error"` - Warnings []string `json:"warnings"` + Healthy bool `json:"healthy"` + Severity health.Severity `json:"severity" enums:"ok,warning,error"` + Warnings []string `json:"warnings"` + Dismissed bool `json:"dismissed` Regions map[int]*RegionReport `json:"regions"` @@ -47,8 +48,6 @@ type Report struct { NetcheckLogs []string `json:"netcheck_logs"` Error *string `json:"error"` - - Dismissed bool `json:"dismissed` } // @typescript-generate RegionReport @@ -105,9 +104,10 @@ type ReportOptions struct { func (r *Report) Run(ctx context.Context, opts *ReportOptions) { r.Healthy = true r.Severity = health.SeverityOK + r.Warnings = []string{} + r.Dismissed = opts.Dismissed r.Regions = map[int]*RegionReport{} - r.Warnings = []string{} wg := &sync.WaitGroup{} mu := sync.Mutex{} diff --git a/coderd/healthcheck/websocket.go b/coderd/healthcheck/websocket.go index 36dbc0368e4c1..b75c080c0278f 100644 --- a/coderd/healthcheck/websocket.go +++ b/coderd/healthcheck/websocket.go @@ -15,14 +15,6 @@ import ( "github.com/coder/coder/v2/coderd/healthcheck/health" ) -type WebsocketReportOptions struct { - APIKey string - AccessURL *url.URL - HTTPClient *http.Client - - Dismissed bool -} - // @typescript-generate WebsocketReport type WebsocketReport struct { // Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead. @@ -36,12 +28,22 @@ type WebsocketReport struct { Error *string `json:"error"` } +type WebsocketReportOptions struct { + APIKey string + AccessURL *url.URL + HTTPClient *http.Client + + Dismissed bool +} + func (r *WebsocketReport) Run(ctx context.Context, opts *WebsocketReportOptions) { ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() r.Severity = health.SeverityOK r.Warnings = []string{} + r.Dismissed = opts.Dismissed + u, err := opts.AccessURL.Parse("/api/v2/debug/ws") if err != nil { r.Error = convertError(xerrors.Errorf("parse access url: %w", err)) diff --git a/coderd/healthcheck/workspaceproxy.go b/coderd/healthcheck/workspaceproxy.go index 71936dea557c8..d51952c13b6df 100644 --- a/coderd/healthcheck/workspaceproxy.go +++ b/coderd/healthcheck/workspaceproxy.go @@ -14,15 +14,6 @@ import ( "github.com/coder/coder/v2/codersdk" ) -type WorkspaceProxyReportOptions struct { - // CurrentVersion is the current server version. - // We pass this in to make it easier to test. - CurrentVersion string - WorkspaceProxiesFetchUpdater WorkspaceProxiesFetchUpdater - - Dismissed bool -} - // @typescript-generate WorkspaceProxyReport type WorkspaceProxyReport struct { Healthy bool `json:"healthy"` @@ -34,6 +25,15 @@ type WorkspaceProxyReport struct { WorkspaceProxies codersdk.RegionsResponse[codersdk.WorkspaceProxy] `json:"workspace_proxies"` } +type WorkspaceProxyReportOptions struct { + // CurrentVersion is the current server version. + // We pass this in to make it easier to test. + CurrentVersion string + WorkspaceProxiesFetchUpdater WorkspaceProxiesFetchUpdater + + Dismissed bool +} + type WorkspaceProxiesFetchUpdater interface { Fetch(context.Context) (codersdk.RegionsResponse[codersdk.WorkspaceProxy], error) Update(context.Context) error @@ -55,6 +55,7 @@ func (r *WorkspaceProxyReport) Run(ctx context.Context, opts *WorkspaceProxyRepo r.Healthy = true r.Severity = health.SeverityOK r.Warnings = []string{} + r.Dismissed = opts.Dismissed if opts.WorkspaceProxiesFetchUpdater == nil { opts.WorkspaceProxiesFetchUpdater = &AGPLWorkspaceProxiesFetchUpdater{} From 60a5b89b1c7246db161aca1aaa491da323aca62d Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 29 Nov 2023 13:58:50 +0100 Subject: [PATCH 07/14] update generated --- coderd/apidoc/docs.go | 15 +++++++++++++ coderd/apidoc/swagger.json | 15 +++++++++++++ coderd/healthcheck/accessurl.go | 2 +- coderd/healthcheck/database.go | 2 +- coderd/healthcheck/derphealth/derp.go | 2 +- coderd/healthcheck/websocket.go | 2 +- coderd/healthcheck/workspaceproxy.go | 2 +- docs/api/debug.md | 5 +++++ docs/api/schemas.md | 31 ++++++++++++++++++++------- site/src/api/typesGenerated.ts | 5 +++++ 10 files changed, 68 insertions(+), 13 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 64ea030b1e0b7..a0c0abba77c09 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -12305,6 +12305,9 @@ const docTemplate = `{ "derphealth.Report": { "type": "object", "properties": { + "dismissed": { + "type": "boolean" + }, "error": { "type": "string" }, @@ -12383,6 +12386,9 @@ const docTemplate = `{ "access_url": { "type": "string" }, + "dismissed": { + "type": "boolean" + }, "error": { "type": "string" }, @@ -12422,6 +12428,9 @@ const docTemplate = `{ "healthcheck.DatabaseReport": { "type": "object", "properties": { + "dismissed": { + "type": "boolean" + }, "error": { "type": "string" }, @@ -12522,6 +12531,9 @@ const docTemplate = `{ "code": { "type": "integer" }, + "dismissed": { + "type": "boolean" + }, "error": { "type": "string" }, @@ -12552,6 +12564,9 @@ const docTemplate = `{ "healthcheck.WorkspaceProxyReport": { "type": "object", "properties": { + "dismissed": { + "type": "boolean" + }, "error": { "type": "string" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 1f6a8f07f5670..f2c0226755036 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -11206,6 +11206,9 @@ "derphealth.Report": { "type": "object", "properties": { + "dismissed": { + "type": "boolean" + }, "error": { "type": "string" }, @@ -11272,6 +11275,9 @@ "access_url": { "type": "string" }, + "dismissed": { + "type": "boolean" + }, "error": { "type": "string" }, @@ -11307,6 +11313,9 @@ "healthcheck.DatabaseReport": { "type": "object", "properties": { + "dismissed": { + "type": "boolean" + }, "error": { "type": "string" }, @@ -11399,6 +11408,9 @@ "code": { "type": "integer" }, + "dismissed": { + "type": "boolean" + }, "error": { "type": "string" }, @@ -11425,6 +11437,9 @@ "healthcheck.WorkspaceProxyReport": { "type": "object", "properties": { + "dismissed": { + "type": "boolean" + }, "error": { "type": "string" }, diff --git a/coderd/healthcheck/accessurl.go b/coderd/healthcheck/accessurl.go index e893bbadb7621..6f3b0fdc07975 100644 --- a/coderd/healthcheck/accessurl.go +++ b/coderd/healthcheck/accessurl.go @@ -19,7 +19,7 @@ type AccessURLReport struct { Healthy bool `json:"healthy"` Severity health.Severity `json:"severity" enums:"ok,warning,error"` Warnings []string `json:"warnings"` - Dismissed bool `json:"dismissed` + Dismissed bool `json:"dismissed"` AccessURL string `json:"access_url"` Reachable bool `json:"reachable"` diff --git a/coderd/healthcheck/database.go b/coderd/healthcheck/database.go index 3744f13a0b63e..3df3fcd972f59 100644 --- a/coderd/healthcheck/database.go +++ b/coderd/healthcheck/database.go @@ -21,7 +21,7 @@ type DatabaseReport struct { Healthy bool `json:"healthy"` Severity health.Severity `json:"severity" enums:"ok,warning,error"` Warnings []string `json:"warnings"` - Dismissed bool `json:"dismissed` + Dismissed bool `json:"dismissed"` Reachable bool `json:"reachable"` Latency string `json:"latency"` diff --git a/coderd/healthcheck/derphealth/derp.go b/coderd/healthcheck/derphealth/derp.go index 912a5d0c81199..3f9f78b319d39 100644 --- a/coderd/healthcheck/derphealth/derp.go +++ b/coderd/healthcheck/derphealth/derp.go @@ -39,7 +39,7 @@ type Report struct { Healthy bool `json:"healthy"` Severity health.Severity `json:"severity" enums:"ok,warning,error"` Warnings []string `json:"warnings"` - Dismissed bool `json:"dismissed` + Dismissed bool `json:"dismissed"` Regions map[int]*RegionReport `json:"regions"` diff --git a/coderd/healthcheck/websocket.go b/coderd/healthcheck/websocket.go index b75c080c0278f..2a4792c874e80 100644 --- a/coderd/healthcheck/websocket.go +++ b/coderd/healthcheck/websocket.go @@ -21,7 +21,7 @@ type WebsocketReport struct { Healthy bool `json:"healthy"` Severity health.Severity `json:"severity" enums:"ok,warning,error"` Warnings []string `json:"warnings"` - Dismissed bool `json:"dismissed` + Dismissed bool `json:"dismissed"` Body string `json:"body"` Code int `json:"code"` diff --git a/coderd/healthcheck/workspaceproxy.go b/coderd/healthcheck/workspaceproxy.go index d51952c13b6df..9282684eb9e0f 100644 --- a/coderd/healthcheck/workspaceproxy.go +++ b/coderd/healthcheck/workspaceproxy.go @@ -20,7 +20,7 @@ type WorkspaceProxyReport struct { Severity health.Severity `json:"severity"` Warnings []string `json:"warnings"` Error *string `json:"error"` - Dismissed bool `json:"dismissed` + Dismissed bool `json:"dismissed"` WorkspaceProxies codersdk.RegionsResponse[codersdk.WorkspaceProxy] `json:"workspace_proxies"` } diff --git a/docs/api/debug.md b/docs/api/debug.md index de9348e1404e4..826ef3e138e6b 100644 --- a/docs/api/debug.md +++ b/docs/api/debug.md @@ -47,6 +47,7 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ { "access_url": { "access_url": "string", + "dismissed": true, "error": "string", "healthy": true, "healthz_response": "string", @@ -57,6 +58,7 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ }, "coder_version": "string", "database": { + "dismissed": true, "error": "string", "healthy": true, "latency": "string", @@ -67,6 +69,7 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "warnings": ["string"] }, "derp": { + "dismissed": true, "error": "string", "healthy": true, "netcheck": { @@ -249,12 +252,14 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "websocket": { "body": "string", "code": 0, + "dismissed": true, "error": "string", "healthy": true, "severity": "ok", "warnings": ["string"] }, "workspace_proxy": { + "dismissed": true, "error": "string", "healthy": true, "severity": "ok", diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 6de323c0968fe..d00c1a739004e 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -7302,6 +7302,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| ```json { + "dismissed": true, "error": "string", "healthy": true, "netcheck": { @@ -7483,6 +7484,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | Name | Type | Required | Restrictions | Description | | ------------------ | -------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------- | +| `dismissed` | boolean | false | | | | `error` | string | false | | | | `healthy` | boolean | false | | Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead. | | `netcheck` | [netcheck.Report](#netcheckreport) | false | | | @@ -7540,6 +7542,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| ```json { "access_url": "string", + "dismissed": true, "error": "string", "healthy": true, "healthz_response": "string", @@ -7555,6 +7558,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | Name | Type | Required | Restrictions | Description | | ------------------ | ---------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------- | | `access_url` | string | false | | | +| `dismissed` | boolean | false | | | | `error` | string | false | | | | `healthy` | boolean | false | | Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead. | | `healthz_response` | string | false | | | @@ -7575,6 +7579,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| ```json { + "dismissed": true, "error": "string", "healthy": true, "latency": "string", @@ -7590,6 +7595,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | Name | Type | Required | Restrictions | Description | | -------------- | ---------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------- | +| `dismissed` | boolean | false | | | | `error` | string | false | | | | `healthy` | boolean | false | | Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead. | | `latency` | string | false | | | @@ -7613,6 +7619,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| { "access_url": { "access_url": "string", + "dismissed": true, "error": "string", "healthy": true, "healthz_response": "string", @@ -7623,6 +7630,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| }, "coder_version": "string", "database": { + "dismissed": true, "error": "string", "healthy": true, "latency": "string", @@ -7633,6 +7641,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "warnings": ["string"] }, "derp": { + "dismissed": true, "error": "string", "healthy": true, "netcheck": { @@ -7815,12 +7824,14 @@ If the schedule is empty, the user will be updated to use the default schedule.| "websocket": { "body": "string", "code": 0, + "dismissed": true, "error": "string", "healthy": true, "severity": "ok", "warnings": ["string"] }, "workspace_proxy": { + "dismissed": true, "error": "string", "healthy": true, "severity": "ok", @@ -7885,6 +7896,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| { "body": "string", "code": 0, + "dismissed": true, "error": "string", "healthy": true, "severity": "ok", @@ -7894,14 +7906,15 @@ If the schedule is empty, the user will be updated to use the default schedule.| ### Properties -| Name | Type | Required | Restrictions | Description | -| ---------- | ---------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------- | -| `body` | string | false | | | -| `code` | integer | false | | | -| `error` | string | false | | | -| `healthy` | boolean | false | | Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead. | -| `severity` | [health.Severity](#healthseverity) | false | | | -| `warnings` | array of string | false | | | +| Name | Type | Required | Restrictions | Description | +| ----------- | ---------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------- | +| `body` | string | false | | | +| `code` | integer | false | | | +| `dismissed` | boolean | false | | | +| `error` | string | false | | | +| `healthy` | boolean | false | | Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead. | +| `severity` | [health.Severity](#healthseverity) | false | | | +| `warnings` | array of string | false | | | #### Enumerated Values @@ -7915,6 +7928,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| ```json { + "dismissed": true, "error": "string", "healthy": true, "severity": "ok", @@ -7953,6 +7967,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | Name | Type | Required | Restrictions | Description | | ------------------- | ---------------------------------------------------------------------------------------------------- | -------- | ------------ | ----------- | +| `dismissed` | boolean | false | | | | `error` | string | false | | | | `healthy` | boolean | false | | | | `severity` | [health.Severity](#healthseverity) | false | | | diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 901f0c213bb02..a3d64b1cffc90 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2100,6 +2100,7 @@ export interface HealthcheckAccessURLReport { readonly healthy: boolean; readonly severity: HealthSeverity; readonly warnings: string[]; + readonly dismissed: boolean; readonly access_url: string; readonly reachable: boolean; readonly status_code: number; @@ -2112,6 +2113,7 @@ export interface HealthcheckDatabaseReport { readonly healthy: boolean; readonly severity: HealthSeverity; readonly warnings: string[]; + readonly dismissed: boolean; readonly reachable: boolean; readonly latency: string; readonly latency_ms: number; @@ -2138,6 +2140,7 @@ export interface HealthcheckWebsocketReport { readonly healthy: boolean; readonly severity: HealthSeverity; readonly warnings: string[]; + readonly dismissed: boolean; readonly body: string; readonly code: number; readonly error?: string; @@ -2149,6 +2152,7 @@ export interface HealthcheckWorkspaceProxyReport { readonly severity: HealthSeverity; readonly warnings: string[]; readonly error?: string; + readonly dismissed: boolean; readonly workspace_proxies: RegionsResponse; } @@ -2246,6 +2250,7 @@ export interface DerphealthReport { // This is likely an enum in an external package ("github.com/coder/coder/v2/coderd/healthcheck/health.Severity") readonly severity: string; readonly warnings: string[]; + readonly dismissed: boolean; readonly regions: Record; // Named type "tailscale.com/net/netcheck.Report" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type From 3c4f53a900b0824d464ac45916eb4eaee3d01a4e Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 29 Nov 2023 14:08:31 +0100 Subject: [PATCH 08/14] Fix: entities.ts --- site/src/testHelpers/entities.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 076bdf2167e68..6596ed358158b 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -2404,6 +2404,7 @@ export const MockHealth: TypesGen.HealthcheckReport = { healthy: true, severity: "ok", warnings: [], + dismissed: false, regions: { "999": { healthy: true, @@ -2806,6 +2807,7 @@ export const MockHealth: TypesGen.HealthcheckReport = { healthy: true, severity: "ok", warnings: [], + dismissed: false, access_url: "https://dev.coder.com", reachable: true, status_code: 200, @@ -2815,6 +2817,7 @@ export const MockHealth: TypesGen.HealthcheckReport = { healthy: true, severity: "ok", warnings: [], + dismissed: false, body: "", code: 101, }, @@ -2822,6 +2825,7 @@ export const MockHealth: TypesGen.HealthcheckReport = { healthy: true, severity: "ok", warnings: [], + dismissed: false, reachable: true, latency: "92570", latency_ms: 92570, @@ -2831,6 +2835,7 @@ export const MockHealth: TypesGen.HealthcheckReport = { healthy: true, severity: "ok", warnings: [], + dismissed: false, workspace_proxies: { regions: [], }, @@ -2857,6 +2862,7 @@ export const DeploymentHealthUnhealthy: TypesGen.HealthcheckReport = { healthy: true, severity: "ok", warnings: [], + dismissed: false, access_url: "", healthz_response: "", reachable: true, @@ -2866,6 +2872,7 @@ export const DeploymentHealthUnhealthy: TypesGen.HealthcheckReport = { healthy: false, severity: "ok", warnings: [], + dismissed: false, latency: "", latency_ms: 0, reachable: true, @@ -2875,6 +2882,7 @@ export const DeploymentHealthUnhealthy: TypesGen.HealthcheckReport = { healthy: false, severity: "ok", warnings: [], + dismissed: false, regions: [], netcheck_logs: [], }, @@ -2882,6 +2890,7 @@ export const DeploymentHealthUnhealthy: TypesGen.HealthcheckReport = { healthy: false, severity: "ok", warnings: [], + dismissed: false, body: "", code: 0, }, @@ -2890,6 +2899,7 @@ export const DeploymentHealthUnhealthy: TypesGen.HealthcheckReport = { error: "some error", severity: "error", warnings: [], + dismissed: false, workspace_proxies: { regions: [ { From b8f655631618da680382e01123db405269aecfe5 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 29 Nov 2023 15:11:06 +0100 Subject: [PATCH 09/14] link dismissed --- coderd/coderd.go | 9 ++++++++- coderd/debug.go | 18 ++++++++++++++++++ coderd/healthcheck/healthcheck.go | 3 --- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 091ef987ff31e..965e75d6105ad 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -25,6 +25,7 @@ import ( "github.com/prometheus/client_golang/prometheus" httpSwagger "github.com/swaggo/http-swagger/v2" "go.opentelemetry.io/otel/trace" + "golang.org/x/exp/slices" "golang.org/x/xerrors" "google.golang.org/api/idtoken" "storj.io/drpc/drpcmux" @@ -407,24 +408,30 @@ func New(options *Options) *API { if options.HealthcheckFunc == nil { options.HealthcheckFunc = func(ctx context.Context, apiKey string) *healthcheck.Report { + dismissedHealthchecks := loadDismissedHealthcheck(ctx, options.Database, options.Logger) return healthcheck.Run(ctx, &healthcheck.ReportOptions{ Database: healthcheck.DatabaseReportOptions{ DB: options.Database, Threshold: options.DeploymentValues.Healthcheck.ThresholdDatabase.Value(), + Dismissed: slices.Contains(dismissedHealthchecks, healthcheck.SectionDatabase), }, Websocket: healthcheck.WebsocketReportOptions{ AccessURL: options.AccessURL, APIKey: apiKey, + Dismissed: slices.Contains(dismissedHealthchecks, healthcheck.SectionWebsocket), }, AccessURL: healthcheck.AccessURLReportOptions{ AccessURL: options.AccessURL, + Dismissed: slices.Contains(dismissedHealthchecks, healthcheck.SectionAccessURL), }, DerpHealth: derphealth.ReportOptions{ - DERPMap: api.DERPMap(), + DERPMap: api.DERPMap(), + Dismissed: slices.Contains(dismissedHealthchecks, healthcheck.SectionDERP), }, WorkspaceProxy: healthcheck.WorkspaceProxyReportOptions{ CurrentVersion: buildinfo.Version(), WorkspaceProxiesFetchUpdater: *(options.WorkspaceProxiesFetchUpdater).Load(), + Dismissed: slices.Contains(dismissedHealthchecks, healthcheck.SectionWorkspaceProxy), }, }) } diff --git a/coderd/debug.go b/coderd/debug.go index 08916e1b4d37b..e2dfb4540cd9e 100644 --- a/coderd/debug.go +++ b/coderd/debug.go @@ -11,6 +11,8 @@ import ( "golang.org/x/exp/slices" "golang.org/x/xerrors" + "cdr.dev/slog" + "github.com/google/uuid" "github.com/coder/coder/v2/coderd/audit" @@ -253,3 +255,19 @@ func validateHealthSettings(settings codersdk.HealthSettings) error { // @Router /debug/ws [get] // @x-apidocgen {"skip": true} func _debugws(http.ResponseWriter, *http.Request) {} //nolint:unused + +func loadDismissedHealthcheck(ctx context.Context, db database.Store, logger slog.Logger) []string { + dismissedHealthchecks := []string{} + settingsJSON, err := db.GetHealthSettings(ctx) + if err == nil { + var settings codersdk.HealthSettings + err = json.Unmarshal([]byte(settingsJSON), &settings) + if len(settings.DismissedHealthchecks) > 0 { + dismissedHealthchecks = settings.DismissedHealthchecks + } + } + if err != nil { + logger.Error(ctx, "unable to fetch health settings: %w", err) + } + return dismissedHealthchecks +} diff --git a/coderd/healthcheck/healthcheck.go b/coderd/healthcheck/healthcheck.go index e6bf7bf0df983..9ecb9b9d13a44 100644 --- a/coderd/healthcheck/healthcheck.go +++ b/coderd/healthcheck/healthcheck.go @@ -7,7 +7,6 @@ import ( "time" "github.com/coder/coder/v2/buildinfo" - "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/healthcheck/derphealth" "github.com/coder/coder/v2/coderd/healthcheck/health" "github.com/coder/coder/v2/coderd/util/ptr" @@ -61,8 +60,6 @@ type ReportOptions struct { WorkspaceProxy WorkspaceProxyReportOptions Checker Checker - - DB database.Store } type defaultChecker struct{} From 6ce2d00b264d39fc7460ea2d772df9ab0c819f89 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 29 Nov 2023 15:32:50 +0100 Subject: [PATCH 10/14] unit tests --- coderd/coderd.go | 2 +- coderd/debug.go | 5 ++--- coderd/healthcheck/accessurl_test.go | 17 +++++++++++++++++ coderd/healthcheck/database_test.go | 20 ++++++++++++++++++++ coderd/healthcheck/derphealth/derp_test.go | 2 ++ coderd/healthcheck/websocket_test.go | 18 ++++++++++++++++++ coderd/healthcheck/workspaceproxy_test.go | 12 ++++++++++++ 7 files changed, 72 insertions(+), 4 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 965e75d6105ad..08784b6ff4e43 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -408,7 +408,7 @@ func New(options *Options) *API { if options.HealthcheckFunc == nil { options.HealthcheckFunc = func(ctx context.Context, apiKey string) *healthcheck.Report { - dismissedHealthchecks := loadDismissedHealthcheck(ctx, options.Database, options.Logger) + dismissedHealthchecks := loadDismissedHealthchecks(ctx, options.Database, options.Logger) return healthcheck.Run(ctx, &healthcheck.ReportOptions{ Database: healthcheck.DatabaseReportOptions{ DB: options.Database, diff --git a/coderd/debug.go b/coderd/debug.go index e2dfb4540cd9e..a46b7822aea0a 100644 --- a/coderd/debug.go +++ b/coderd/debug.go @@ -8,13 +8,12 @@ import ( "net/http" "time" + "github.com/google/uuid" "golang.org/x/exp/slices" "golang.org/x/xerrors" "cdr.dev/slog" - "github.com/google/uuid" - "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/healthcheck" @@ -256,7 +255,7 @@ func validateHealthSettings(settings codersdk.HealthSettings) error { // @x-apidocgen {"skip": true} func _debugws(http.ResponseWriter, *http.Request) {} //nolint:unused -func loadDismissedHealthcheck(ctx context.Context, db database.Store, logger slog.Logger) []string { +func loadDismissedHealthchecks(ctx context.Context, db database.Store, logger slog.Logger) []string { dismissedHealthchecks := []string{} settingsJSON, err := db.GetHealthSettings(ctx) if err == nil { diff --git a/coderd/healthcheck/accessurl_test.go b/coderd/healthcheck/accessurl_test.go index 43e839c853b96..9e368cc679708 100644 --- a/coderd/healthcheck/accessurl_test.go +++ b/coderd/healthcheck/accessurl_test.go @@ -109,6 +109,23 @@ func TestAccessURL(t *testing.T) { require.NotNil(t, report.Error) assert.Contains(t, *report.Error, expErr.Error()) }) + + t.Run("DismissedError", func(t *testing.T) { + t.Parallel() + + var ( + ctx, cancel = context.WithCancel(context.Background()) + report healthcheck.AccessURLReport + ) + defer cancel() + + report.Run(ctx, &healthcheck.AccessURLReportOptions{ + Dismissed: true, + }) + + assert.True(t, report.Dismissed) + assert.Equal(t, health.SeverityError, report.Severity) + }) } type roundTripFunc func(r *http.Request) (*http.Response, error) diff --git a/coderd/healthcheck/database_test.go b/coderd/healthcheck/database_test.go index a0d817e59606f..8ac5bbe38c2e7 100644 --- a/coderd/healthcheck/database_test.go +++ b/coderd/healthcheck/database_test.go @@ -67,6 +67,26 @@ func TestDatabase(t *testing.T) { assert.Contains(t, *report.Error, err.Error()) }) + t.Run("DismissedError", func(t *testing.T) { + t.Parallel() + + var ( + ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort) + report = healthcheck.DatabaseReport{} + db = dbmock.NewMockStore(gomock.NewController(t)) + err = xerrors.New("ping error") + ) + defer cancel() + + db.EXPECT().Ping(gomock.Any()).Return(time.Duration(0), err) + + report.Run(ctx, &healthcheck.DatabaseReportOptions{DB: db, Dismissed: true}) + + assert.Equal(t, health.SeverityError, report.Severity) + assert.True(t, report.Dismissed) + require.NotNil(t, report.Error) + }) + t.Run("Median", func(t *testing.T) { t.Parallel() diff --git a/coderd/healthcheck/derphealth/derp_test.go b/coderd/healthcheck/derphealth/derp_test.go index fd389f4e62f53..cf307637ac401 100644 --- a/coderd/healthcheck/derphealth/derp_test.go +++ b/coderd/healthcheck/derphealth/derp_test.go @@ -120,6 +120,7 @@ func TestDERP(t *testing.T) { }}, }, }}, + Dismissed: true, // Let's sneak an extra unit test } ) @@ -127,6 +128,7 @@ func TestDERP(t *testing.T) { assert.True(t, report.Healthy) assert.Equal(t, health.SeverityWarning, report.Severity) + assert.True(t, report.Dismissed) for _, region := range report.Regions { assert.True(t, region.Healthy) assert.True(t, region.NodeReports[0].Healthy) diff --git a/coderd/healthcheck/websocket_test.go b/coderd/healthcheck/websocket_test.go index 861e1acfd07fa..1beb96ea0631b 100644 --- a/coderd/healthcheck/websocket_test.go +++ b/coderd/healthcheck/websocket_test.go @@ -68,4 +68,22 @@ func TestWebsocket(t *testing.T) { assert.Equal(t, wsReport.Body, "test error") assert.Equal(t, wsReport.Code, http.StatusBadRequest) }) + + t.Run("DismissedError", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + wsReport := healthcheck.WebsocketReport{} + wsReport.Run(ctx, &healthcheck.WebsocketReportOptions{ + AccessURL: &url.URL{Host: "fake"}, + Dismissed: true, + }) + + require.True(t, wsReport.Dismissed) + require.Equal(t, health.SeverityError, wsReport.Severity) + require.NotNil(t, wsReport.Error) + require.Equal(t, health.SeverityError, wsReport.Severity) + }) } diff --git a/coderd/healthcheck/workspaceproxy_test.go b/coderd/healthcheck/workspaceproxy_test.go index a96c13384fb5b..22d01949bc006 100644 --- a/coderd/healthcheck/workspaceproxy_test.go +++ b/coderd/healthcheck/workspaceproxy_test.go @@ -236,3 +236,15 @@ func fakeUpdateProxyHealth(err error) func(context.Context) error { return err } } + +func TestWorkspaceProxy_Dismissed(t *testing.T) { + t.Parallel() + + var report healthcheck.WorkspaceProxyReport + report.Run(context.Background(), &healthcheck.WorkspaceProxyReportOptions{ + Dismissed: true, + }) + + assert.True(t, report.Dismissed) + assert.Equal(t, health.SeverityOK, report.Severity) +} From 1bf4fbf10898bd7032bdb2fa14bb8ad9efc80f63 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 29 Nov 2023 16:48:57 +0100 Subject: [PATCH 11/14] sql.ErrNoRows --- coderd/debug.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coderd/debug.go b/coderd/debug.go index a46b7822aea0a..8f61bcfb78859 100644 --- a/coderd/debug.go +++ b/coderd/debug.go @@ -3,6 +3,7 @@ package coderd import ( "bytes" "context" + "database/sql" "encoding/json" "fmt" "net/http" @@ -265,7 +266,7 @@ func loadDismissedHealthchecks(ctx context.Context, db database.Store, logger sl dismissedHealthchecks = settings.DismissedHealthchecks } } - if err != nil { + if err != nil && !xerrors.Is(err, sql.ErrNoRows) { logger.Error(ctx, "unable to fetch health settings: %w", err) } return dismissedHealthchecks From ec2bb2f95b58ffe65fbf8ac102f41136c2afa0b7 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 29 Nov 2023 17:19:58 +0100 Subject: [PATCH 12/14] ErrorDismissed --- coderd/healthcheck/workspaceproxy_test.go | 28 +++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/coderd/healthcheck/workspaceproxy_test.go b/coderd/healthcheck/workspaceproxy_test.go index 22d01949bc006..3cd560ad3897c 100644 --- a/coderd/healthcheck/workspaceproxy_test.go +++ b/coderd/healthcheck/workspaceproxy_test.go @@ -191,6 +191,22 @@ func TestWorkspaceProxies(t *testing.T) { } } +func TestWorkspaceProxy_ErrorDismissed(t *testing.T) { + t.Parallel() + + var report healthcheck.WorkspaceProxyReport + report.Run(context.Background(), &healthcheck.WorkspaceProxyReportOptions{ + WorkspaceProxiesFetchUpdater: &fakeWorkspaceProxyFetchUpdater{ + fetchFunc: fakeFetchWorkspaceProxiesErr(assert.AnError), + updateFunc: fakeUpdateProxyHealth(assert.AnError), + }, + Dismissed: true, + }) + + assert.True(t, report.Dismissed) + assert.Equal(t, health.SeverityWarning, report.Severity) +} + // yet another implementation of the thing type fakeWorkspaceProxyFetchUpdater struct { fetchFunc func(context.Context) (codersdk.RegionsResponse[codersdk.WorkspaceProxy], error) @@ -236,15 +252,3 @@ func fakeUpdateProxyHealth(err error) func(context.Context) error { return err } } - -func TestWorkspaceProxy_Dismissed(t *testing.T) { - t.Parallel() - - var report healthcheck.WorkspaceProxyReport - report.Run(context.Background(), &healthcheck.WorkspaceProxyReportOptions{ - Dismissed: true, - }) - - assert.True(t, report.Dismissed) - assert.Equal(t, health.SeverityOK, report.Severity) -} From 82b991ba0026f0c523499388a9d2a058ed8812ef Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 29 Nov 2023 17:22:07 +0100 Subject: [PATCH 13/14] order --- coderd/healthcheck/workspaceproxy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/healthcheck/workspaceproxy.go b/coderd/healthcheck/workspaceproxy.go index 9282684eb9e0f..8ab8e86dd47cc 100644 --- a/coderd/healthcheck/workspaceproxy.go +++ b/coderd/healthcheck/workspaceproxy.go @@ -19,8 +19,8 @@ type WorkspaceProxyReport struct { Healthy bool `json:"healthy"` Severity health.Severity `json:"severity"` Warnings []string `json:"warnings"` - Error *string `json:"error"` Dismissed bool `json:"dismissed"` + Error *string `json:"error"` WorkspaceProxies codersdk.RegionsResponse[codersdk.WorkspaceProxy] `json:"workspace_proxies"` } From e9a8753df5bf0a391f80fb6ab884148c41dd8eb1 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 29 Nov 2023 17:27:56 +0100 Subject: [PATCH 14/14] Fix --- site/src/api/typesGenerated.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index a3d64b1cffc90..9d930d27f1311 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2151,8 +2151,8 @@ export interface HealthcheckWorkspaceProxyReport { readonly healthy: boolean; readonly severity: HealthSeverity; readonly warnings: string[]; - readonly error?: string; readonly dismissed: boolean; + readonly error?: string; readonly workspace_proxies: RegionsResponse; }