diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index a0c0abba77..c88eb6fb2b 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -12258,7 +12258,7 @@ const docTemplate = `{ "warnings": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/health.Message" } } } @@ -12297,7 +12297,7 @@ const docTemplate = `{ "warnings": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/health.Message" } } } @@ -12348,7 +12348,7 @@ const docTemplate = `{ "warnings": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/health.Message" } } } @@ -12367,6 +12367,56 @@ const docTemplate = `{ } } }, + "health.Code": { + "type": "string", + "enum": [ + "EUNKNOWN", + "EWP01", + "EWP02", + "EWP03", + "EWP04", + "EDB01", + "EDB02", + "EWS01", + "EWS02", + "EWS03", + "EACS01", + "EACS02", + "EACS03", + "EACS04", + "EDERP01", + "EDERP02" + ], + "x-enum-varnames": [ + "CodeUnknown", + "CodeProxyUpdate", + "CodeProxyFetch", + "CodeProxyVersionMismatch", + "CodeProxyUnhealthy", + "CodeDatabasePingFailed", + "CodeDatabasePingSlow", + "CodeWebsocketDial", + "CodeWebsocketEcho", + "CodeWebsocketMsg", + "CodeAccessURLNotSet", + "CodeAccessURLInvalid", + "CodeAccessURLFetch", + "CodeAccessURLNotOK", + "CodeDERPNodeUsesWebsocket", + "CodeDERPOneNodeUnhealthy" + ] + }, + "health.Message": { + "type": "object", + "properties": { + "code": { + "$ref": "#/definitions/health.Code" + }, + "message": { + "type": "string" + } + } + }, "health.Severity": { "type": "string", "enum": [ @@ -12420,7 +12470,7 @@ const docTemplate = `{ "warnings": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/health.Message" } } } @@ -12465,7 +12515,7 @@ const docTemplate = `{ "warnings": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/health.Message" } } } @@ -12579,7 +12629,7 @@ const docTemplate = `{ "warnings": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/health.Message" } }, "workspace_proxies": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index f2c0226755..ed7fa177e6 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -11163,7 +11163,7 @@ "warnings": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/health.Message" } } } @@ -11198,7 +11198,7 @@ "warnings": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/health.Message" } } } @@ -11245,7 +11245,7 @@ "warnings": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/health.Message" } } } @@ -11264,6 +11264,56 @@ } } }, + "health.Code": { + "type": "string", + "enum": [ + "EUNKNOWN", + "EWP01", + "EWP02", + "EWP03", + "EWP04", + "EDB01", + "EDB02", + "EWS01", + "EWS02", + "EWS03", + "EACS01", + "EACS02", + "EACS03", + "EACS04", + "EDERP01", + "EDERP02" + ], + "x-enum-varnames": [ + "CodeUnknown", + "CodeProxyUpdate", + "CodeProxyFetch", + "CodeProxyVersionMismatch", + "CodeProxyUnhealthy", + "CodeDatabasePingFailed", + "CodeDatabasePingSlow", + "CodeWebsocketDial", + "CodeWebsocketEcho", + "CodeWebsocketMsg", + "CodeAccessURLNotSet", + "CodeAccessURLInvalid", + "CodeAccessURLFetch", + "CodeAccessURLNotOK", + "CodeDERPNodeUsesWebsocket", + "CodeDERPOneNodeUnhealthy" + ] + }, + "health.Message": { + "type": "object", + "properties": { + "code": { + "$ref": "#/definitions/health.Code" + }, + "message": { + "type": "string" + } + } + }, "health.Severity": { "type": "string", "enum": ["ok", "warning", "error"], @@ -11305,7 +11355,7 @@ "warnings": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/health.Message" } } } @@ -11346,7 +11396,7 @@ "warnings": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/health.Message" } } } @@ -11452,7 +11502,7 @@ "warnings": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/health.Message" } }, "workspace_proxies": { diff --git a/coderd/healthcheck/accessurl.go b/coderd/healthcheck/accessurl.go index cfcc0ac006..2c1d27f164 100644 --- a/coderd/healthcheck/accessurl.go +++ b/coderd/healthcheck/accessurl.go @@ -8,16 +8,15 @@ import ( "time" "github.com/coder/coder/v2/coderd/healthcheck/health" - "github.com/coder/coder/v2/coderd/util/ptr" ) // @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"` - Dismissed bool `json:"dismissed"` + Healthy bool `json:"healthy"` + Severity health.Severity `json:"severity" enums:"ok,warning,error"` + Warnings []health.Message `json:"warnings"` + Dismissed bool `json:"dismissed"` AccessURL string `json:"access_url"` Reachable bool `json:"reachable"` @@ -38,11 +37,11 @@ func (r *AccessURLReport) Run(ctx context.Context, opts *AccessURLReportOptions) defer cancel() r.Severity = health.SeverityOK - r.Warnings = []string{} + r.Warnings = []health.Message{} r.Dismissed = opts.Dismissed if opts.AccessURL == nil { - r.Error = ptr.Ref(health.Messagef(health.CodeAccessURLNotSet, "Access URL not set")) + r.Error = health.Errorf(health.CodeAccessURLNotSet, "Access URL not set") r.Severity = health.SeverityError return } @@ -54,21 +53,21 @@ func (r *AccessURLReport) Run(ctx context.Context, opts *AccessURLReportOptions) accessURL, err := opts.AccessURL.Parse("/healthz") if err != nil { - r.Error = ptr.Ref(health.Messagef(health.CodeAccessURLInvalid, "parse healthz endpoint: %s", err)) + r.Error = health.Errorf(health.CodeAccessURLInvalid, "parse healthz endpoint: %s", err) r.Severity = health.SeverityError return } req, err := http.NewRequestWithContext(ctx, "GET", accessURL.String(), nil) if err != nil { - r.Error = ptr.Ref(health.Messagef(health.CodeAccessURLFetch, "create healthz request: %s", err)) + r.Error = health.Errorf(health.CodeAccessURLFetch, "create healthz request: %s", err) r.Severity = health.SeverityError return } res, err := opts.Client.Do(req) if err != nil { - r.Error = ptr.Ref(health.Messagef(health.CodeAccessURLFetch, "get healthz endpoint: %s", err)) + r.Error = health.Errorf(health.CodeAccessURLFetch, "get healthz endpoint: %s", err) r.Severity = health.SeverityError return } @@ -76,7 +75,7 @@ func (r *AccessURLReport) Run(ctx context.Context, opts *AccessURLReportOptions) body, err := io.ReadAll(res.Body) if err != nil { - r.Error = ptr.Ref(health.Messagef(health.CodeAccessURLFetch, "read healthz response: %s", err)) + r.Error = health.Errorf(health.CodeAccessURLFetch, "read healthz response: %s", err) r.Severity = health.SeverityError return } diff --git a/coderd/healthcheck/accessurl_test.go b/coderd/healthcheck/accessurl_test.go index 788fd41481..29bf008346 100644 --- a/coderd/healthcheck/accessurl_test.go +++ b/coderd/healthcheck/accessurl_test.go @@ -131,7 +131,7 @@ func TestAccessURL(t *testing.T) { assert.Equal(t, string(resp), report.HealthzResponse) assert.Nil(t, report.Error) if assert.NotEmpty(t, report.Warnings) { - assert.Contains(t, report.Warnings[0], health.CodeAccessURLNotOK) + assert.Equal(t, report.Warnings[0].Code, health.CodeAccessURLNotOK) } }) diff --git a/coderd/healthcheck/database.go b/coderd/healthcheck/database.go index aa2896f65c..ac36ec0d53 100644 --- a/coderd/healthcheck/database.go +++ b/coderd/healthcheck/database.go @@ -4,11 +4,10 @@ import ( "context" "time" + "golang.org/x/exp/slices" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/healthcheck/health" - "github.com/coder/coder/v2/coderd/util/ptr" - - "golang.org/x/exp/slices" ) const ( @@ -18,10 +17,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"` - Dismissed bool `json:"dismissed"` + Healthy bool `json:"healthy"` + Severity health.Severity `json:"severity" enums:"ok,warning,error"` + Warnings []health.Message `json:"warnings"` + Dismissed bool `json:"dismissed"` Reachable bool `json:"reachable"` Latency string `json:"latency"` @@ -38,7 +37,7 @@ type DatabaseReportOptions struct { } func (r *DatabaseReport) Run(ctx context.Context, opts *DatabaseReportOptions) { - r.Warnings = []string{} + r.Warnings = []health.Message{} r.Severity = health.SeverityOK r.Dismissed = opts.Dismissed @@ -55,7 +54,7 @@ func (r *DatabaseReport) Run(ctx context.Context, opts *DatabaseReportOptions) { for i := 0; i < pingCount; i++ { pong, err := opts.DB.Ping(ctx) if err != nil { - r.Error = ptr.Ref(health.Messagef(health.CodeDatabasePingFailed, "ping database: %s", err)) + r.Error = health.Errorf(health.CodeDatabasePingFailed, "ping database: %s", err) r.Severity = health.SeverityError return diff --git a/coderd/healthcheck/database_test.go b/coderd/healthcheck/database_test.go index afa518f270..f3f032356a 100644 --- a/coderd/healthcheck/database_test.go +++ b/coderd/healthcheck/database_test.go @@ -143,7 +143,7 @@ func TestDatabase(t *testing.T) { assert.Equal(t, time.Second.Milliseconds(), report.ThresholdMS) assert.Nil(t, report.Error) if assert.NotEmpty(t, report.Warnings) { - assert.Contains(t, report.Warnings[0], health.CodeDatabasePingSlow) + assert.Equal(t, report.Warnings[0].Code, health.CodeDatabasePingSlow) } }) } diff --git a/coderd/healthcheck/derphealth/derp.go b/coderd/healthcheck/derphealth/derp.go index 9051cc6e52..de8899fcf1 100644 --- a/coderd/healthcheck/derphealth/derp.go +++ b/coderd/healthcheck/derphealth/derp.go @@ -36,10 +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"` - Dismissed bool `json:"dismissed"` + Healthy bool `json:"healthy"` + Severity health.Severity `json:"severity" enums:"ok,warning,error"` + Warnings []health.Message `json:"warnings"` + Dismissed bool `json:"dismissed"` Regions map[int]*RegionReport `json:"regions"` @@ -55,9 +55,9 @@ type RegionReport struct { mu sync.Mutex // 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 []health.Message `json:"warnings"` Region *tailcfg.DERPRegion `json:"region"` NodeReports []*NodeReport `json:"node_reports"` @@ -70,9 +70,9 @@ type NodeReport struct { clientCounter int // 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 []health.Message `json:"warnings"` Node *tailcfg.DERPNode `json:"node"` @@ -104,7 +104,7 @@ type ReportOptions struct { func (r *Report) Run(ctx context.Context, opts *ReportOptions) { r.Healthy = true r.Severity = health.SeverityOK - r.Warnings = []string{} + r.Warnings = []health.Message{} r.Dismissed = opts.Dismissed r.Regions = map[int]*RegionReport{} @@ -168,7 +168,7 @@ func (r *RegionReport) Run(ctx context.Context) { r.Healthy = true r.Severity = health.SeverityOK r.NodeReports = []*NodeReport{} - r.Warnings = []string{} + r.Warnings = []health.Message{} wg := &sync.WaitGroup{} var unhealthyNodes int // atomic.Int64 is not mandatory as we depend on RegionReport mutex. @@ -263,7 +263,7 @@ func (r *NodeReport) Run(ctx context.Context) { r.Severity = health.SeverityOK r.ClientLogs = [][]string{} r.ClientErrs = [][]string{} - r.Warnings = []string{} + r.Warnings = []health.Message{} wg := &sync.WaitGroup{} diff --git a/coderd/healthcheck/derphealth/derp_test.go b/coderd/healthcheck/derphealth/derp_test.go index 8a2bf99e76..a7011f05e5 100644 --- a/coderd/healthcheck/derphealth/derp_test.go +++ b/coderd/healthcheck/derphealth/derp_test.go @@ -130,7 +130,7 @@ func TestDERP(t *testing.T) { assert.Equal(t, health.SeverityWarning, report.Severity) assert.True(t, report.Dismissed) if assert.NotEmpty(t, report.Warnings) { - assert.Contains(t, report.Warnings[0], health.CodeDERPOneNodeUnhealthy) + assert.Contains(t, report.Warnings[0].Code, health.CodeDERPOneNodeUnhealthy) } for _, region := range report.Regions { assert.True(t, region.Healthy) @@ -236,7 +236,7 @@ func TestDERP(t *testing.T) { assert.True(t, report.Healthy) assert.Equal(t, health.SeverityWarning, report.Severity) if assert.NotEmpty(t, report.Warnings) { - assert.Contains(t, report.Warnings[0], health.CodeDERPNodeUsesWebsocket) + assert.Equal(t, report.Warnings[0].Code, health.CodeDERPNodeUsesWebsocket) } for _, region := range report.Regions { assert.True(t, region.Healthy) diff --git a/coderd/healthcheck/health/model.go b/coderd/healthcheck/health/model.go index 27ff9eae1f..707969e404 100644 --- a/coderd/healthcheck/health/model.go +++ b/coderd/healthcheck/health/model.go @@ -3,6 +3,8 @@ package health import ( "fmt" "strings" + + "github.com/coder/coder/v2/coderd/util/ptr" ) const ( @@ -47,16 +49,34 @@ func (s Severity) Value() int { return severityRank[s] } +// @typescript-generate Message +type Message struct { + Code Code `json:"code"` + Message string `json:"message"` +} + +func (m Message) String() string { + var sb strings.Builder + _, _ = sb.WriteString(string(m.Code)) + _, _ = sb.WriteRune(':') + _, _ = sb.WriteRune(' ') + _, _ = sb.WriteString(m.Message) + return sb.String() +} + // Code is a stable identifier used to link to documentation. // @typescript-generate Code type Code string -// Messagef is a convenience function for formatting a healthcheck error message. -func Messagef(code Code, msg string, args ...any) string { - var sb strings.Builder - _, _ = sb.WriteString(string(code)) - _, _ = sb.WriteRune(':') - _, _ = sb.WriteRune(' ') - _, _ = sb.WriteString(fmt.Sprintf(msg, args...)) - return sb.String() +// Messagef is a convenience function for returning a health.Message +func Messagef(code Code, msg string, args ...any) Message { + return Message{ + Code: code, + Message: fmt.Sprintf(msg, args...), + } +} + +// Errorf is a convenience function for returning a stringly-typed version of a Message. +func Errorf(code Code, msg string, args ...any) *string { + return ptr.Ref(Messagef(code, msg, args...).String()) } diff --git a/coderd/healthcheck/healthcheck.go b/coderd/healthcheck/healthcheck.go index f1d399b889..72e96b4ae6 100644 --- a/coderd/healthcheck/healthcheck.go +++ b/coderd/healthcheck/healthcheck.go @@ -103,7 +103,7 @@ func Run(ctx context.Context, opts *ReportOptions) *Report { defer wg.Done() defer func() { if err := recover(); err != nil { - report.DERP.Error = ptr.Ref(health.Messagef(health.CodeUnknown, "derp report panic: %s", err)) + report.DERP.Error = health.Errorf(health.CodeUnknown, "derp report panic: %s", err) } }() @@ -115,7 +115,7 @@ func Run(ctx context.Context, opts *ReportOptions) *Report { defer wg.Done() defer func() { if err := recover(); err != nil { - report.AccessURL.Error = ptr.Ref(health.Messagef(health.CodeUnknown, "access url report panic: %s", err)) + report.AccessURL.Error = health.Errorf(health.CodeUnknown, "access url report panic: %s", err) } }() @@ -127,7 +127,7 @@ func Run(ctx context.Context, opts *ReportOptions) *Report { defer wg.Done() defer func() { if err := recover(); err != nil { - report.Websocket.Error = ptr.Ref(health.Messagef(health.CodeUnknown, "websocket report panic: %s", err)) + report.Websocket.Error = health.Errorf(health.CodeUnknown, "websocket report panic: %s", err) } }() @@ -139,7 +139,7 @@ func Run(ctx context.Context, opts *ReportOptions) *Report { defer wg.Done() defer func() { if err := recover(); err != nil { - report.Database.Error = ptr.Ref(health.Messagef(health.CodeUnknown, "database report panic: %s", err)) + report.Database.Error = health.Errorf(health.CodeUnknown, "database report panic: %s", err) } }() @@ -151,7 +151,7 @@ func Run(ctx context.Context, opts *ReportOptions) *Report { defer wg.Done() defer func() { if err := recover(); err != nil { - report.WorkspaceProxy.Error = ptr.Ref(health.Messagef(health.CodeUnknown, "proxy report panic: %s", err)) + report.WorkspaceProxy.Error = health.Errorf(health.CodeUnknown, "proxy report panic: %s", err) } }() diff --git a/coderd/healthcheck/healthcheck_test.go b/coderd/healthcheck/healthcheck_test.go index 7f7d22017c..9550cc5cc4 100644 --- a/coderd/healthcheck/healthcheck_test.go +++ b/coderd/healthcheck/healthcheck_test.go @@ -107,7 +107,7 @@ func TestHealthcheck(t *testing.T) { checker: &testChecker{ DERPReport: derphealth.Report{ Healthy: true, - Warnings: []string{"foobar"}, + Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}}, Severity: health.SeverityWarning, }, AccessURLReport: healthcheck.AccessURLReport{ @@ -259,7 +259,7 @@ func TestHealthcheck(t *testing.T) { }, WorkspaceProxyReport: healthcheck.WorkspaceProxyReport{ Healthy: true, - Warnings: []string{"foobar"}, + Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}}, Severity: health.SeverityWarning, }, }, diff --git a/coderd/healthcheck/websocket.go b/coderd/healthcheck/websocket.go index 372a322bc8..215465fe5c 100644 --- a/coderd/healthcheck/websocket.go +++ b/coderd/healthcheck/websocket.go @@ -13,7 +13,6 @@ import ( "nhooyr.io/websocket" "github.com/coder/coder/v2/coderd/healthcheck/health" - "github.com/coder/coder/v2/coderd/util/ptr" ) // @typescript-generate WebsocketReport @@ -76,7 +75,7 @@ func (r *WebsocketReport) Run(ctx context.Context, opts *WebsocketReportOptions) } if err != nil { r.Error = convertError(xerrors.Errorf("websocket dial: %w", err)) - r.Error = ptr.Ref(health.Messagef(health.CodeWebsocketDial, "websocket dial: %s", err)) + r.Error = health.Errorf(health.CodeWebsocketDial, "websocket dial: %s", err) r.Severity = health.SeverityError return } @@ -86,26 +85,26 @@ func (r *WebsocketReport) Run(ctx context.Context, opts *WebsocketReportOptions) msg := strconv.Itoa(i) err := c.Write(ctx, websocket.MessageText, []byte(msg)) if err != nil { - r.Error = ptr.Ref(health.Messagef(health.CodeWebsocketEcho, "write message: %s", err)) + r.Error = health.Errorf(health.CodeWebsocketEcho, "write message: %s", err) r.Severity = health.SeverityError return } ty, got, err := c.Read(ctx) if err != nil { - r.Error = ptr.Ref(health.Messagef(health.CodeWebsocketEcho, "read message: %s", err)) + r.Error = health.Errorf(health.CodeWebsocketEcho, "read message: %s", err) r.Severity = health.SeverityError return } if ty != websocket.MessageText { - r.Error = ptr.Ref(health.Messagef(health.CodeWebsocketMsg, "received incorrect message type: %v", ty)) + r.Error = health.Errorf(health.CodeWebsocketMsg, "received incorrect message type: %v", ty) r.Severity = health.SeverityError return } if string(got) != msg { - r.Error = ptr.Ref(health.Messagef(health.CodeWebsocketMsg, "received incorrect message: wanted %q, got %q", msg, string(got))) + r.Error = health.Errorf(health.CodeWebsocketMsg, "received incorrect message: wanted %q, got %q", msg, string(got)) r.Severity = health.SeverityError return } diff --git a/coderd/healthcheck/workspaceproxy.go b/coderd/healthcheck/workspaceproxy.go index bfb1b892d9..e99f46a3c7 100644 --- a/coderd/healthcheck/workspaceproxy.go +++ b/coderd/healthcheck/workspaceproxy.go @@ -16,11 +16,11 @@ import ( // @typescript-generate WorkspaceProxyReport type WorkspaceProxyReport struct { - Healthy bool `json:"healthy"` - Severity health.Severity `json:"severity"` - Warnings []string `json:"warnings"` - Dismissed bool `json:"dismissed"` - Error *string `json:"error"` + Healthy bool `json:"healthy"` + Severity health.Severity `json:"severity"` + Warnings []health.Message `json:"warnings"` + Dismissed bool `json:"dismissed"` + Error *string `json:"error"` WorkspaceProxies codersdk.RegionsResponse[codersdk.WorkspaceProxy] `json:"workspace_proxies"` } @@ -54,7 +54,7 @@ func (*AGPLWorkspaceProxiesFetchUpdater) Update(context.Context) error { func (r *WorkspaceProxyReport) Run(ctx context.Context, opts *WorkspaceProxyReportOptions) { r.Healthy = true r.Severity = health.SeverityOK - r.Warnings = []string{} + r.Warnings = []health.Message{} r.Dismissed = opts.Dismissed if opts.WorkspaceProxiesFetchUpdater == nil { @@ -72,7 +72,7 @@ func (r *WorkspaceProxyReport) Run(ctx context.Context, opts *WorkspaceProxyRepo if err != nil { r.Healthy = false r.Severity = health.SeverityError - r.Error = ptr.Ref(health.Messagef(health.CodeProxyFetch, "fetch workspace proxies: %s", err)) + r.Error = health.Errorf(health.CodeProxyFetch, "fetch workspace proxies: %s", err) return } @@ -104,7 +104,7 @@ func (r *WorkspaceProxyReport) Run(ctx context.Context, opts *WorkspaceProxyRepo case health.SeverityWarning, health.SeverityOK: r.Warnings = append(r.Warnings, health.Messagef(health.CodeProxyUnhealthy, err)) case health.SeverityError: - r.appendError(health.Messagef(health.CodeProxyUnhealthy, err)) + r.appendError(*health.Errorf(health.CodeProxyUnhealthy, err)) } } @@ -113,7 +113,7 @@ func (r *WorkspaceProxyReport) Run(ctx context.Context, opts *WorkspaceProxyRepo if vErr := checkVersion(proxy, opts.CurrentVersion); vErr != nil { r.Healthy = false r.Severity = health.SeverityError - r.appendError(health.Messagef(health.CodeProxyVersionMismatch, vErr.Error())) + r.appendError(*health.Errorf(health.CodeProxyVersionMismatch, vErr.Error())) } } } diff --git a/coderd/healthcheck/workspaceproxy_test.go b/coderd/healthcheck/workspaceproxy_test.go index 98a75eb237..7044268366 100644 --- a/coderd/healthcheck/workspaceproxy_test.go +++ b/coderd/healthcheck/workspaceproxy_test.go @@ -2,7 +2,6 @@ package healthcheck_test import ( "context" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -27,7 +26,7 @@ func TestWorkspaceProxies(t *testing.T) { updateProxyHealth func(context.Context) error expectedHealthy bool expectedError string - expectedWarning string + expectedWarningCode health.Code expectedSeverity health.Severity }{ { @@ -103,10 +102,10 @@ func TestWorkspaceProxies(t *testing.T) { fakeWorkspaceProxy("alpha", false, currentVersion), fakeWorkspaceProxy("beta", true, currentVersion), ), - updateProxyHealth: fakeUpdateProxyHealth(nil), - expectedHealthy: true, - expectedSeverity: health.SeverityWarning, - expectedWarning: string(health.CodeProxyUnhealthy), + updateProxyHealth: fakeUpdateProxyHealth(nil), + expectedHealthy: true, + expectedSeverity: health.SeverityWarning, + expectedWarningCode: health.CodeProxyUnhealthy, }, { name: "Enabled/AllUnhealthy", @@ -163,7 +162,7 @@ func TestWorkspaceProxies(t *testing.T) { updateProxyHealth: fakeUpdateProxyHealth(assert.AnError), expectedHealthy: true, expectedSeverity: health.SeverityWarning, - expectedWarning: string(health.CodeProxyUpdate), + expectedWarningCode: health.CodeProxyUpdate, }, } { tt := tt @@ -190,15 +189,15 @@ func TestWorkspaceProxies(t *testing.T) { } else { assert.Nil(t, rpt.Error) } - if tt.expectedWarning != "" && assert.NotEmpty(t, rpt.Warnings) { + if tt.expectedWarningCode != "" && assert.NotEmpty(t, rpt.Warnings) { var found bool for _, w := range rpt.Warnings { - if strings.Contains(w, tt.expectedWarning) { + if w.Code == tt.expectedWarningCode { found = true break } } - assert.True(t, found, "expected warning %s not found in %v", tt.expectedWarning, rpt.Warnings) + assert.True(t, found, "expected warning %s not found in %v", tt.expectedWarningCode, rpt.Warnings) } else { assert.Empty(t, rpt.Warnings) } diff --git a/docs/api/debug.md b/docs/api/debug.md index 826ef3e138..019ca30ee8 100644 --- a/docs/api/debug.md +++ b/docs/api/debug.md @@ -54,7 +54,12 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "reachable": true, "severity": "ok", "status_code": 0, - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] }, "coder_version": "string", "database": { @@ -66,7 +71,12 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "reachable": true, "severity": "ok", "threshold_ms": 0, - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] }, "derp": { "dismissed": true, @@ -143,7 +153,12 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "error": "string" }, "uses_websocket": true, - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] } ], "region": { @@ -171,7 +186,12 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "regionName": "string" }, "severity": "ok", - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] }, "property2": { "error": "string", @@ -211,7 +231,12 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "error": "string" }, "uses_websocket": true, - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] } ], "region": { @@ -239,11 +264,21 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "regionName": "string" }, "severity": "ok", - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] } }, "severity": "ok", - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] }, "failing_sections": ["string"], "healthy": true, @@ -263,7 +298,12 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "error": "string", "healthy": true, "severity": "ok", - "warnings": ["string"], + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ], "workspace_proxies": { "regions": [ { diff --git a/docs/api/schemas.md b/docs/api/schemas.md index d00c1a7390..b2d344fbd9 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -7176,7 +7176,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "error": "string" }, "uses_websocket": true, - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] } ``` @@ -7196,7 +7201,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `severity` | [health.Severity](#healthseverity) | false | | | | `stun` | [derphealth.StunReport](#derphealthstunreport) | false | | | | `uses_websocket` | boolean | false | | | -| `warnings` | array of string | false | | | +| `warnings` | array of [health.Message](#healthmessage) | false | | | #### Enumerated Values @@ -7247,7 +7252,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "error": "string" }, "uses_websocket": true, - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] } ], "region": { @@ -7275,7 +7285,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "regionName": "string" }, "severity": "ok", - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] } ``` @@ -7288,7 +7303,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `node_reports` | array of [derphealth.NodeReport](#derphealthnodereport) | false | | | | `region` | [tailcfg.DERPRegion](#tailcfgderpregion) | false | | | | `severity` | [health.Severity](#healthseverity) | false | | | -| `warnings` | array of string | false | | | +| `warnings` | array of [health.Message](#healthmessage) | false | | | #### Enumerated Values @@ -7376,7 +7391,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "error": "string" }, "uses_websocket": true, - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] } ], "region": { @@ -7404,7 +7424,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "regionName": "string" }, "severity": "ok", - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] }, "property2": { "error": "string", @@ -7444,7 +7469,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "error": "string" }, "uses_websocket": true, - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] } ], "region": { @@ -7472,11 +7502,21 @@ If the schedule is empty, the user will be updated to use the default schedule.| "regionName": "string" }, "severity": "ok", - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] } }, "severity": "ok", - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] } ``` @@ -7493,7 +7533,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `regions` | object | false | | | | ยป `[any property]` | [derphealth.RegionReport](#derphealthregionreport) | false | | | | `severity` | [health.Severity](#healthseverity) | false | | | -| `warnings` | array of string | false | | | +| `warnings` | array of [health.Message](#healthmessage) | false | | | #### Enumerated Values @@ -7521,6 +7561,51 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `enabled` | boolean | false | | | | `error` | string | false | | | +## health.Code + +```json +"EUNKNOWN" +``` + +### Properties + +#### Enumerated Values + +| Value | +| ---------- | +| `EUNKNOWN` | +| `EWP01` | +| `EWP02` | +| `EWP03` | +| `EWP04` | +| `EDB01` | +| `EDB02` | +| `EWS01` | +| `EWS02` | +| `EWS03` | +| `EACS01` | +| `EACS02` | +| `EACS03` | +| `EACS04` | +| `EDERP01` | +| `EDERP02` | + +## health.Message + +```json +{ + "code": "EUNKNOWN", + "message": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| --------- | -------------------------- | -------- | ------------ | ----------- | +| `code` | [health.Code](#healthcode) | false | | | +| `message` | string | false | | | + ## health.Severity ```json @@ -7549,23 +7634,28 @@ If the schedule is empty, the user will be updated to use the default schedule.| "reachable": true, "severity": "ok", "status_code": 0, - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] } ``` ### Properties -| 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 | | | -| `reachable` | boolean | false | | | -| `severity` | [health.Severity](#healthseverity) | false | | | -| `status_code` | integer | false | | | -| `warnings` | array of string | false | | | +| 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 | | | +| `reachable` | boolean | false | | | +| `severity` | [health.Severity](#healthseverity) | false | | | +| `status_code` | integer | false | | | +| `warnings` | array of [health.Message](#healthmessage) | false | | | #### Enumerated Values @@ -7587,23 +7677,28 @@ If the schedule is empty, the user will be updated to use the default schedule.| "reachable": true, "severity": "ok", "threshold_ms": 0, - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] } ``` ### Properties -| 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 | | | -| `latency_ms` | integer | false | | | -| `reachable` | boolean | false | | | -| `severity` | [health.Severity](#healthseverity) | false | | | -| `threshold_ms` | integer | false | | | -| `warnings` | array of string | false | | | +| 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 | | | +| `latency_ms` | integer | false | | | +| `reachable` | boolean | false | | | +| `severity` | [health.Severity](#healthseverity) | false | | | +| `threshold_ms` | integer | false | | | +| `warnings` | array of [health.Message](#healthmessage) | false | | | #### Enumerated Values @@ -7626,7 +7721,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "reachable": true, "severity": "ok", "status_code": 0, - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] }, "coder_version": "string", "database": { @@ -7638,7 +7738,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "reachable": true, "severity": "ok", "threshold_ms": 0, - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] }, "derp": { "dismissed": true, @@ -7715,7 +7820,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "error": "string" }, "uses_websocket": true, - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] } ], "region": { @@ -7743,7 +7853,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "regionName": "string" }, "severity": "ok", - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] }, "property2": { "error": "string", @@ -7783,7 +7898,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "error": "string" }, "uses_websocket": true, - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] } ], "region": { @@ -7811,11 +7931,21 @@ If the schedule is empty, the user will be updated to use the default schedule.| "regionName": "string" }, "severity": "ok", - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] } }, "severity": "ok", - "warnings": ["string"] + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ] }, "failing_sections": ["string"], "healthy": true, @@ -7835,7 +7965,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "error": "string", "healthy": true, "severity": "ok", - "warnings": ["string"], + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ], "workspace_proxies": { "regions": [ { @@ -7932,7 +8067,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "error": "string", "healthy": true, "severity": "ok", - "warnings": ["string"], + "warnings": [ + { + "code": "EUNKNOWN", + "message": "string" + } + ], "workspace_proxies": { "regions": [ { @@ -7971,7 +8111,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `error` | string | false | | | | `healthy` | boolean | false | | | | `severity` | [health.Severity](#healthseverity) | false | | | -| `warnings` | array of string | false | | | +| `warnings` | array of [health.Message](#healthmessage) | false | | | | `workspace_proxies` | [codersdk.RegionsResponse-codersdk_WorkspaceProxy](#codersdkregionsresponse-codersdk_workspaceproxy) | false | | | ## netcheck.Report diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 21c70c6bb1..3f4763e583 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2099,7 +2099,7 @@ export type RegionTypes = Region | WorkspaceProxy; export interface HealthcheckAccessURLReport { readonly healthy: boolean; readonly severity: HealthSeverity; - readonly warnings: string[]; + readonly warnings: HealthMessage[]; readonly dismissed: boolean; readonly access_url: string; readonly reachable: boolean; @@ -2112,7 +2112,7 @@ export interface HealthcheckAccessURLReport { export interface HealthcheckDatabaseReport { readonly healthy: boolean; readonly severity: HealthSeverity; - readonly warnings: string[]; + readonly warnings: HealthMessage[]; readonly dismissed: boolean; readonly reachable: boolean; readonly latency: string; @@ -2150,7 +2150,7 @@ export interface HealthcheckWebsocketReport { export interface HealthcheckWorkspaceProxyReport { readonly healthy: boolean; readonly severity: HealthSeverity; - readonly warnings: string[]; + readonly warnings: HealthMessage[]; readonly dismissed: boolean; readonly error?: string; readonly workspace_proxies: RegionsResponse; @@ -2203,6 +2203,12 @@ export const ClibaseValueSources: ClibaseValueSource[] = [ // The code below is generated from coderd/healthcheck/health. +// From health/model.go +export interface HealthMessage { + readonly code: HealthCode; + readonly message: string; +} + // From health/model.go export type HealthCode = | "EACS01" @@ -2251,7 +2257,9 @@ export interface DerphealthNodeReport { readonly healthy: boolean; // This is likely an enum in an external package ("github.com/coder/coder/v2/coderd/healthcheck/health.Severity") readonly severity: string; - readonly warnings: string[]; + // Named type "github.com/coder/coder/v2/coderd/healthcheck/health.Message" unknown, using "any" + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type + readonly warnings: any[]; // Named type "tailscale.com/tailcfg.DERPNode" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type readonly node?: any; @@ -2273,7 +2281,9 @@ export interface DerphealthRegionReport { readonly healthy: boolean; // This is likely an enum in an external package ("github.com/coder/coder/v2/coderd/healthcheck/health.Severity") readonly severity: string; - readonly warnings: string[]; + // Named type "github.com/coder/coder/v2/coderd/healthcheck/health.Message" unknown, using "any" + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type + readonly warnings: any[]; // Named type "tailscale.com/tailcfg.DERPRegion" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type readonly region?: any; @@ -2286,7 +2296,9 @@ export interface DerphealthReport { readonly healthy: boolean; // This is likely an enum in an external package ("github.com/coder/coder/v2/coderd/healthcheck/health.Severity") readonly severity: string; - readonly warnings: string[]; + // Named type "github.com/coder/coder/v2/coderd/healthcheck/health.Message" unknown, using "any" + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type + readonly warnings: any[]; readonly dismissed: boolean; readonly regions: Record; // Named type "tailscale.com/net/netcheck.Report" unknown, using "any" diff --git a/site/src/pages/HealthPage/HealthPage.stories.tsx b/site/src/pages/HealthPage/HealthPage.stories.tsx index 9849edb539..dcccfa99f4 100644 --- a/site/src/pages/HealthPage/HealthPage.stories.tsx +++ b/site/src/pages/HealthPage/HealthPage.stories.tsx @@ -43,7 +43,7 @@ export const AccessURLWarning: Story = { access_url: { ...MockHealth.access_url, healthy: true, - warnings: ["foobar"], + warnings: [{ code: "EUNKNOWN", message: "foobar" }], }, }, }, @@ -73,7 +73,7 @@ export const DatabaseWarning: Story = { database: { ...MockHealth.database, healthy: true, - warnings: ["foobar"], + warnings: [{ code: "EUNKNOWN", message: "foobar" }], }, }, }, @@ -159,7 +159,7 @@ export const ProxyWarning: Story = { severity: "warning", workspace_proxy: { ...MockHealth.workspace_proxy, - warnings: ["foobar"], + warnings: [{ code: "EUNKNOWN", message: "foobar" }], }, }, },