diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index bbf238df73e60..64ea030b1e0b7 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": [ @@ -8904,6 +8966,17 @@ const docTemplate = `{ "GroupSourceOIDC" ] }, + "codersdk.HealthSettings": { + "type": "object", + "properties": { + "dismissed_healthchecks": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "codersdk.Healthcheck": { "type": "object", "properties": { @@ -9851,6 +9924,7 @@ const docTemplate = `{ "group", "license", "convert_login", + "health_settings", "workspace_proxy", "organization" ], @@ -9865,6 +9939,7 @@ const docTemplate = `{ "ResourceTypeGroup", "ResourceTypeLicense", "ResourceTypeConvertLogin", + "ResourceTypeHealthSettings", "ResourceTypeWorkspaceProxy", "ResourceTypeOrganization" ] @@ -10771,6 +10846,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 a692f7258e981..1f6a8f07f5670 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": [ @@ -8000,6 +8052,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": { @@ -8877,6 +8940,7 @@ "group", "license", "convert_login", + "health_settings", "workspace_proxy", "organization" ], @@ -8891,6 +8955,7 @@ "ResourceTypeGroup", "ResourceTypeLicense", "ResourceTypeConvertLogin", + "ResourceTypeHealthSettings", "ResourceTypeWorkspaceProxy", "ResourceTypeOrganization" ] @@ -9754,6 +9819,17 @@ } } }, + "codersdk.UpdateHealthSettings": { + "type": "object", + "properties": { + "dismissed_healthchecks": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "codersdk.UpdateRoles": { "type": "object", "properties": { diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go index 8cf0a1d0ddaf3..bdaef00bb082b 100644 --- a/coderd/audit/diff.go +++ b/coderd/audit/diff.go @@ -18,7 +18,8 @@ type Auditable interface { database.AuditableGroup | database.License | database.WorkspaceProxy | - database.AuditOAuthConvertState + database.AuditOAuthConvertState | + database.HealthSettings } // Map is a map of changed fields in an audited resource. It maps field names to diff --git a/coderd/audit/request.go b/coderd/audit/request.go index cc1f60779a7dc..6e738f9929bbb 100644 --- a/coderd/audit/request.go +++ b/coderd/audit/request.go @@ -93,6 +93,8 @@ func ResourceTarget[T Auditable](tgt T) string { return typed.Name case database.AuditOAuthConvertState: return string(typed.ToLoginType) + case database.HealthSettings: + return "" // no target? default: panic(fmt.Sprintf("unknown resource %T", tgt)) } @@ -123,6 +125,9 @@ func ResourceID[T Auditable](tgt T) uuid.UUID { case database.AuditOAuthConvertState: // The merge state is for the given user return typed.UserID + case database.HealthSettings: + // Artificial ID for auditing purposes + return typed.ID default: panic(fmt.Sprintf("unknown resource %T", tgt)) } @@ -152,6 +157,8 @@ func ResourceType[T Auditable](tgt T) database.ResourceType { return database.ResourceTypeWorkspaceProxy case database.AuditOAuthConvertState: return database.ResourceTypeConvertLogin + case database.HealthSettings: + return database.ResourceTypeHealthSettings default: panic(fmt.Sprintf("unknown resource %T", typed)) } diff --git a/coderd/coderd.go b/coderd/coderd.go index 2940eb64c3128..091ef987ff31e 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -970,7 +970,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) r.Route("/{user}", func(r chi.Router) { r.Use(httpmw.ExtractUserParam(options.Database)) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index d65da03ed3448..f4200f7ea3109 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -133,7 +133,8 @@ CREATE TYPE resource_type AS ENUM ( 'workspace_build', 'license', 'workspace_proxy', - 'convert_login' + 'convert_login', + 'health_settings' ); CREATE TYPE startup_script_behavior AS ENUM ( diff --git a/coderd/database/migrations/000172_health_settings_audit.down.sql b/coderd/database/migrations/000172_health_settings_audit.down.sql new file mode 100644 index 0000000000000..362f597df0911 --- /dev/null +++ b/coderd/database/migrations/000172_health_settings_audit.down.sql @@ -0,0 +1 @@ +-- Nothing to do diff --git a/coderd/database/migrations/000172_health_settings_audit.up.sql b/coderd/database/migrations/000172_health_settings_audit.up.sql new file mode 100644 index 0000000000000..09dd8e17bfe0c --- /dev/null +++ b/coderd/database/migrations/000172_health_settings_audit.up.sql @@ -0,0 +1,2 @@ +-- This has to be outside a transaction +ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'health_settings'; diff --git a/coderd/database/models.go b/coderd/database/models.go index 3fe6c8fa12b87..bd7625657b7bd 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1158,6 +1158,7 @@ const ( ResourceTypeLicense ResourceType = "license" ResourceTypeWorkspaceProxy ResourceType = "workspace_proxy" ResourceTypeConvertLogin ResourceType = "convert_login" + ResourceTypeHealthSettings ResourceType = "health_settings" ) func (e *ResourceType) Scan(src interface{}) error { @@ -1208,7 +1209,8 @@ func (e ResourceType) Valid() bool { ResourceTypeWorkspaceBuild, ResourceTypeLicense, ResourceTypeWorkspaceProxy, - ResourceTypeConvertLogin: + ResourceTypeConvertLogin, + ResourceTypeHealthSettings: return true } return false @@ -1228,6 +1230,7 @@ func AllResourceTypeValues() []ResourceType { ResourceTypeLicense, ResourceTypeWorkspaceProxy, ResourceTypeConvertLogin, + ResourceTypeHealthSettings, } } diff --git a/coderd/database/types.go b/coderd/database/types.go index 5099733601f65..b1fb389d88794 100644 --- a/coderd/database/types.go +++ b/coderd/database/types.go @@ -23,6 +23,11 @@ type AuditOAuthConvertState struct { UserID uuid.UUID `db:"user_id" json:"user_id"` } +type HealthSettings struct { + ID uuid.UUID `db:"id" json:"id"` + DismissedHealthchecks []string `db:"dismissed_healthchecks" json:"dismissed_healthchecks"` +} + type Actions []rbac.Action func (a *Actions) Scan(src interface{}) error { diff --git a/coderd/debug.go b/coderd/debug.go index ba6eaf9696d99..08916e1b4d37b 100644 --- a/coderd/debug.go +++ b/coderd/debug.go @@ -1,14 +1,24 @@ package coderd import ( + "bytes" "context" + "encoding/json" "fmt" "net/http" "time" + "golang.org/x/exp/slices" + "golang.org/x/xerrors" + + "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" "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" ) @@ -107,6 +117,131 @@ func formatHealthcheck(ctx context.Context, rw http.ResponseWriter, r *http.Requ } } +// @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) { + 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 +// @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) { + ctx := r.Context() + + if !api.Authorize(r, rbac.ActionUpdate, rbac.ResourceDeploymentValues) { + httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ + Message: "Insufficient permissions to update health settings.", + }) + return + } + + var settings codersdk.HealthSettings + if !httpapi.Read(ctx, rw, r, &settings) { + return + } + + 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 + } + + 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 + } + + currentSettingsJSON, err := api.Database.GetHealthSettings(r.Context()) + if err != nil { + httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to fetch current health settings.", + Detail: err.Error(), + }) + return + } + + if bytes.Equal(settingsJSON, []byte(currentSettingsJSON)) { + httpapi.Write(r.Context(), rw, http.StatusNotModified, nil) + return + } + + auditor := api.Auditor.Load() + aReq, commitAudit := audit.InitRequest[database.HealthSettings](rw, &audit.RequestParams{ + Audit: *auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + }) + defer commitAudit() + aReq.New = database.HealthSettings{ + ID: uuid.New(), + DismissedHealthchecks: settings.DismissedHealthchecks, + } + + 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. // // @Summary Debug Info Websocket Test diff --git a/coderd/debug_test.go b/coderd/debug_test.go index 186539b82d90f..855827c34d0f4 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,108 @@ 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("DismissSection", 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: dismiss "derp" and "websocket" + err := adminClient.PutHealthSettings(ctx, expected) + require.NoError(t, err) + + // then + settings, err := adminClient.HealthSettings(ctx) + require.NoError(t, err) + require.Equal(t, expected, settings) + }) + + t.Run("UnDismissSection", 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) + + initial := codersdk.HealthSettings{ + DismissedHealthchecks: []string{healthcheck.SectionDERP, healthcheck.SectionWebsocket}, + } + + err := adminClient.PutHealthSettings(ctx, initial) + require.NoError(t, err) + + expected := codersdk.HealthSettings{ + DismissedHealthchecks: []string{healthcheck.SectionDERP}, + } + + // when: undismiss "websocket" + err = adminClient.PutHealthSettings(ctx, expected) + require.NoError(t, err) + + // then + settings, err := adminClient.HealthSettings(ctx) + require.NoError(t, err) + require.Equal(t, expected, settings) + }) + + t.Run("NotModified", 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}, + } + + err := adminClient.PutHealthSettings(ctx, expected) + require.NoError(t, err) + + // when + err = adminClient.PutHealthSettings(ctx, expected) + + // then + require.Error(t, err) + require.Contains(t, err.Error(), "health settings not modified") + }) +} + func TestDebugWebsocket(t *testing.T) { t.Parallel() 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 diff --git a/codersdk/audit.go b/codersdk/audit.go index 5ceae81a21c42..c1ea077ec0831 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -24,6 +24,7 @@ const ( ResourceTypeGroup ResourceType = "group" ResourceTypeLicense ResourceType = "license" ResourceTypeConvertLogin ResourceType = "convert_login" + ResourceTypeHealthSettings ResourceType = "health_settings" ResourceTypeWorkspaceProxy ResourceType = "workspace_proxy" ResourceTypeOrganization ResourceType = "organization" ) @@ -56,6 +57,8 @@ func (r ResourceType) FriendlyString() string { return "workspace proxy" case ResourceTypeOrganization: return "organization" + case ResourceTypeHealthSettings: + return "health_settings" default: return "unknown" } diff --git a/codersdk/health.go b/codersdk/health.go new file mode 100644 index 0000000000000..ece12ba424771 --- /dev/null +++ b/codersdk/health.go @@ -0,0 +1,45 @@ +package codersdk + +import ( + "context" + "encoding/json" + "net/http" + + "golang.org/x/xerrors" +) + +type HealthSettings struct { + DismissedHealthchecks []string `json:"dismissed_healthchecks"` +} + +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) PutHealthSettings(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.StatusNotModified { + return xerrors.New("health settings not modified") + } + if res.StatusCode != http.StatusOK { + return ReadBodyAsError(res) + } + return nil +} diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md index 09ad0aae5bc80..527134f88befb 100644 --- a/docs/admin/audit-logs.md +++ b/docs/admin/audit-logs.md @@ -14,6 +14,7 @@ We track the following resources: | AuditOAuthConvertState
|
FieldTracked
created_attrue
expires_attrue
from_login_typetrue
to_login_typetrue
user_idtrue
| | Group
create, write, delete |
FieldTracked
avatar_urltrue
display_nametrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
sourcefalse
| | GitSSHKey
create |
FieldTracked
created_atfalse
private_keytrue
public_keytrue
updated_atfalse
user_idtrue
| +| HealthSettings
|
FieldTracked
dismissed_healthcheckstrue
idfalse
| | License
create, delete |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| | Template
write, delete |
FieldTracked
active_version_idtrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_ttltrue
nametrue
organization_idfalse
provisionertrue
require_active_versiontrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
user_acltrue
| | TemplateVersion
create, write |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
external_auth_providersfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
template_idtrue
updated_atfalse
| diff --git a/docs/api/debug.md b/docs/api/debug.md index 60576c1c0ac62..de9348e1404e4 100644 --- a/docs/api/debug.md +++ b/docs/api/debug.md @@ -298,6 +298,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 1d1834a621764..6de323c0968fe 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 @@ -4162,6 +4176,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `group` | | `license` | | `convert_login` | +| `health_settings` | | `workspace_proxy` | | `organization` | @@ -5158,6 +5173,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/enterprise/audit/table.go b/enterprise/audit/table.go index 085f4ac581a9e..dcd775cf4c1f3 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -184,6 +184,10 @@ var auditableResourcesTypes = map[any]map[string]Action{ "to_login_type": ActionTrack, "user_id": ActionTrack, }, + &database.HealthSettings{}: { + "id": ActionIgnore, + "dismissed_healthchecks": ActionTrack, + }, // TODO: track an ID here when the below ticket is completed: // https://github.com/coder/coder/pull/6012 &database.License{}: { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 14f4993085215..901f0c213bb02 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[]; @@ -1920,6 +1930,7 @@ export type ResourceType = | "convert_login" | "git_ssh_key" | "group" + | "health_settings" | "license" | "organization" | "template" @@ -1933,6 +1944,7 @@ export const ResourceTypes: ResourceType[] = [ "convert_login", "git_ssh_key", "group", + "health_settings", "license", "organization", "template",