refactor(coderd/healthcheck): make Warnings an object with { Code, Message } (#10950)

- Adds health.Message { code string, mesasge string }
- Refactors existing warnings []string to be of type []health.Message instead
This commit is contained in:
Cian Johnston 2023-11-30 14:49:50 +00:00 committed by GitHub
parent 4f9292859d
commit 07895006d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 459 additions and 151 deletions

62
coderd/apidoc/docs.go generated
View File

@ -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": {

View File

@ -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": {

View File

@ -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
}

View File

@ -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)
}
})

View File

@ -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

View File

@ -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)
}
})
}

View File

@ -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{}

View File

@ -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)

View File

@ -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())
}

View File

@ -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)
}
}()

View File

@ -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,
},
},

View File

@ -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
}

View File

@ -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()))
}
}
}

View File

@ -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)
}

56
docs/api/debug.md generated
View File

@ -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": [
{

230
docs/api/schemas.md generated
View File

@ -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

View File

@ -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<WorkspaceProxy>;
@ -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<number, DerphealthRegionReport>;
// Named type "tailscale.com/net/netcheck.Report" unknown, using "any"

View File

@ -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" }],
},
},
},