feat(coderd/healthcheck): add access URL error codes and healthcheck doc (#10915)

Relates to #8965

- Added error codes for separate code paths in health checks
- Prefixed errors and warnings with error code prefixes
- Added a docs page with details on each code, cause and solution

Co-authored-by: Muhammad Atif Ali <atif@coder.com>
This commit is contained in:
Cian Johnston
2023-11-30 12:15:40 +00:00
committed by GitHub
co-authored by Muhammad Atif Ali
parent 5b2f43619b
commit 4f9292859d
16 changed files with 479 additions and 72 deletions
+6 -7
View File
@@ -7,8 +7,6 @@ import (
"net/url"
"time"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/healthcheck/health"
"github.com/coder/coder/v2/coderd/util/ptr"
)
@@ -44,7 +42,7 @@ func (r *AccessURLReport) Run(ctx context.Context, opts *AccessURLReportOptions)
r.Dismissed = opts.Dismissed
if opts.AccessURL == nil {
r.Error = ptr.Ref("access URL is nil")
r.Error = ptr.Ref(health.Messagef(health.CodeAccessURLNotSet, "Access URL not set"))
r.Severity = health.SeverityError
return
}
@@ -56,21 +54,21 @@ func (r *AccessURLReport) Run(ctx context.Context, opts *AccessURLReportOptions)
accessURL, err := opts.AccessURL.Parse("/healthz")
if err != nil {
r.Error = convertError(xerrors.Errorf("parse healthz endpoint: %w", err))
r.Error = ptr.Ref(health.Messagef(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 = convertError(xerrors.Errorf("create healthz request: %w", err))
r.Error = ptr.Ref(health.Messagef(health.CodeAccessURLFetch, "create healthz request: %s", err))
r.Severity = health.SeverityError
return
}
res, err := opts.Client.Do(req)
if err != nil {
r.Error = convertError(xerrors.Errorf("get healthz endpoint: %w", err))
r.Error = ptr.Ref(health.Messagef(health.CodeAccessURLFetch, "get healthz endpoint: %s", err))
r.Severity = health.SeverityError
return
}
@@ -78,7 +76,7 @@ func (r *AccessURLReport) Run(ctx context.Context, opts *AccessURLReportOptions)
body, err := io.ReadAll(res.Body)
if err != nil {
r.Error = convertError(xerrors.Errorf("read healthz response: %w", err))
r.Error = ptr.Ref(health.Messagef(health.CodeAccessURLFetch, "read healthz response: %s", err))
r.Severity = health.SeverityError
return
}
@@ -88,6 +86,7 @@ func (r *AccessURLReport) Run(ctx context.Context, opts *AccessURLReportOptions)
r.StatusCode = res.StatusCode
if res.StatusCode != http.StatusOK {
r.Severity = health.SeverityWarning
r.Warnings = append(r.Warnings, health.Messagef(health.CodeAccessURLNotOK, "/healthz did not return 200 OK"))
}
r.HealthzResponse = string(body)
}
+57 -25
View File
@@ -11,7 +11,6 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/healthcheck"
"github.com/coder/coder/v2/coderd/healthcheck/health"
)
@@ -25,12 +24,17 @@ func TestAccessURL(t *testing.T) {
var (
ctx, cancel = context.WithCancel(context.Background())
report healthcheck.AccessURLReport
client = coderdtest.New(t, nil)
resp = []byte("OK")
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write(resp)
}))
)
defer cancel()
report.Run(ctx, &healthcheck.AccessURLReportOptions{
AccessURL: client.URL,
Client: srv.Client(),
AccessURL: mustURL(t, srv.URL),
})
assert.True(t, report.Healthy)
@@ -41,35 +45,27 @@ func TestAccessURL(t *testing.T) {
assert.Nil(t, report.Error)
})
t.Run("404", func(t *testing.T) {
t.Run("NotSet", func(t *testing.T) {
t.Parallel()
var (
ctx, cancel = context.WithCancel(context.Background())
report healthcheck.AccessURLReport
resp = []byte("NOT OK")
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Write(resp)
}))
)
defer cancel()
defer srv.Close()
u, err := url.Parse(srv.URL)
require.NoError(t, err)
report.Run(ctx, &healthcheck.AccessURLReportOptions{
Client: srv.Client(),
AccessURL: u,
Client: nil, // defaults to http.DefaultClient
AccessURL: nil,
})
assert.False(t, report.Healthy)
assert.True(t, report.Reachable)
assert.Equal(t, health.SeverityWarning, report.Severity)
assert.Equal(t, http.StatusNotFound, report.StatusCode)
assert.Equal(t, string(resp), report.HealthzResponse)
assert.Nil(t, report.Error)
assert.False(t, report.Reachable)
assert.Equal(t, health.SeverityError, report.Severity)
assert.Equal(t, 0, report.StatusCode)
assert.Equal(t, "", report.HealthzResponse)
require.NotNil(t, report.Error)
assert.Contains(t, *report.Error, health.CodeAccessURLNotSet)
})
t.Run("ClientErr", func(t *testing.T) {
@@ -81,7 +77,7 @@ func TestAccessURL(t *testing.T) {
resp = []byte("OK")
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write(resp)
_, _ = w.Write(resp)
}))
client = srv.Client()
)
@@ -93,12 +89,9 @@ func TestAccessURL(t *testing.T) {
return nil, expErr
})
u, err := url.Parse(srv.URL)
require.NoError(t, err)
report.Run(ctx, &healthcheck.AccessURLReportOptions{
Client: client,
AccessURL: u,
AccessURL: mustURL(t, srv.URL),
})
assert.False(t, report.Healthy)
@@ -108,6 +101,38 @@ func TestAccessURL(t *testing.T) {
assert.Equal(t, "", report.HealthzResponse)
require.NotNil(t, report.Error)
assert.Contains(t, *report.Error, expErr.Error())
assert.Contains(t, *report.Error, health.CodeAccessURLFetch)
})
t.Run("404", func(t *testing.T) {
t.Parallel()
var (
ctx, cancel = context.WithCancel(context.Background())
report healthcheck.AccessURLReport
resp = []byte("NOT OK")
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write(resp)
}))
)
defer cancel()
defer srv.Close()
report.Run(ctx, &healthcheck.AccessURLReportOptions{
Client: srv.Client(),
AccessURL: mustURL(t, srv.URL),
})
assert.False(t, report.Healthy)
assert.True(t, report.Reachable)
assert.Equal(t, health.SeverityWarning, report.Severity)
assert.Equal(t, http.StatusNotFound, report.StatusCode)
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)
}
})
t.Run("DismissedError", func(t *testing.T) {
@@ -133,3 +158,10 @@ type roundTripFunc func(r *http.Request) (*http.Response, error)
func (rt roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
return rt(r)
}
func mustURL(t testing.TB, s string) *url.URL {
t.Helper()
u, err := url.Parse(s)
require.NoError(t, err)
return u
}
+6 -4
View File
@@ -4,11 +4,11 @@ import (
"context"
"time"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"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 (
@@ -55,8 +55,9 @@ 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 = convertError(xerrors.Errorf("ping: %w", err))
r.Error = ptr.Ref(health.Messagef(health.CodeDatabasePingFailed, "ping database: %s", err))
r.Severity = health.SeverityError
return
}
pings = append(pings, pong)
@@ -69,6 +70,7 @@ func (r *DatabaseReport) Run(ctx context.Context, opts *DatabaseReportOptions) {
r.LatencyMS = latency.Milliseconds()
if r.LatencyMS >= r.ThresholdMS {
r.Severity = health.SeverityWarning
r.Warnings = append(r.Warnings, health.Messagef(health.CodeDatabasePingSlow, "median database ping above threshold"))
}
r.Healthy = true
r.Reachable = true
+6
View File
@@ -65,6 +65,7 @@ func TestDatabase(t *testing.T) {
require.NotNil(t, report.Error)
assert.Equal(t, healthcheck.DatabaseDefaultThreshold.Milliseconds(), report.ThresholdMS)
assert.Contains(t, *report.Error, err.Error())
assert.Contains(t, *report.Error, health.CodeDatabasePingFailed)
})
t.Run("DismissedError", func(t *testing.T) {
@@ -85,6 +86,7 @@ func TestDatabase(t *testing.T) {
assert.Equal(t, health.SeverityError, report.Severity)
assert.True(t, report.Dismissed)
require.NotNil(t, report.Error)
assert.Contains(t, *report.Error, health.CodeDatabasePingFailed)
})
t.Run("Median", func(t *testing.T) {
@@ -112,6 +114,7 @@ func TestDatabase(t *testing.T) {
assert.EqualValues(t, 1, report.LatencyMS)
assert.Equal(t, healthcheck.DatabaseDefaultThreshold.Milliseconds(), report.ThresholdMS)
assert.Nil(t, report.Error)
assert.Empty(t, report.Warnings)
})
t.Run("Threshold", func(t *testing.T) {
@@ -139,5 +142,8 @@ func TestDatabase(t *testing.T) {
assert.EqualValues(t, 1000, report.LatencyMS)
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)
}
})
}
+4 -8
View File
@@ -136,9 +136,7 @@ func (r *Report) Run(ctx context.Context, opts *ReportOptions) {
r.Healthy = false
}
for _, w := range regionReport.Warnings {
r.Warnings = append(r.Warnings, fmt.Sprintf("[%s] %s", regionReport.Region.RegionName, w))
}
r.Warnings = append(r.Warnings, regionReport.Warnings...)
mu.Unlock()
}()
}
@@ -202,9 +200,7 @@ func (r *RegionReport) Run(ctx context.Context) {
unhealthyNodes++
}
for _, w := range nodeReport.Warnings {
r.Warnings = append(r.Warnings, fmt.Sprintf("[%s] %s", nodeReport.Node.Name, w))
}
r.Warnings = append(r.Warnings, nodeReport.Warnings...)
r.mu.Unlock()
}()
}
@@ -228,7 +224,7 @@ func (r *RegionReport) Run(ctx context.Context) {
} else if unhealthyNodes == 1 {
// r.Healthy = true (by default)
r.Severity = health.SeverityWarning
r.Warnings = append(r.Warnings, oneNodeUnhealthy)
r.Warnings = append(r.Warnings, health.Messagef(health.CodeDERPOneNodeUnhealthy, oneNodeUnhealthy))
} else if unhealthyNodes > 1 {
r.Healthy = false
@@ -292,7 +288,7 @@ func (r *NodeReport) Run(ctx context.Context) {
}
if r.UsesWebsocket {
r.Warnings = append(r.Warnings, warningNodeUsesWebsocket)
r.Warnings = append(r.Warnings, health.Messagef(health.CodeDERPNodeUsesWebsocket, warningNodeUsesWebsocket))
r.Severity = health.SeverityWarning
}
}
+6 -1
View File
@@ -129,6 +129,9 @@ func TestDERP(t *testing.T) {
assert.True(t, report.Healthy)
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)
}
for _, region := range report.Regions {
assert.True(t, region.Healthy)
assert.True(t, region.NodeReports[0].Healthy)
@@ -232,7 +235,9 @@ func TestDERP(t *testing.T) {
assert.True(t, report.Healthy)
assert.Equal(t, health.SeverityWarning, report.Severity)
assert.NotEmpty(t, report.Warnings)
if assert.NotEmpty(t, report.Warnings) {
assert.Contains(t, report.Warnings[0], health.CodeDERPNodeUsesWebsocket)
}
for _, region := range report.Regions {
assert.True(t, region.Healthy)
assert.Equal(t, health.SeverityWarning, region.Severity)
+42
View File
@@ -1,9 +1,37 @@
package health
import (
"fmt"
"strings"
)
const (
SeverityOK Severity = "ok"
SeverityWarning Severity = "warning"
SeverityError Severity = "error"
// CodeUnknown is a catch-all health code when something unexpected goes wrong (for example, a panic).
CodeUnknown Code = "EUNKNOWN"
CodeProxyUpdate Code = "EWP01"
CodeProxyFetch Code = "EWP02"
CodeProxyVersionMismatch Code = "EWP03"
CodeProxyUnhealthy Code = "EWP04"
CodeDatabasePingFailed Code = "EDB01"
CodeDatabasePingSlow Code = "EDB02"
CodeWebsocketDial Code = "EWS01"
CodeWebsocketEcho Code = "EWS02"
CodeWebsocketMsg Code = "EWS03"
CodeAccessURLNotSet Code = "EACS01"
CodeAccessURLInvalid Code = "EACS02"
CodeAccessURLFetch Code = "EACS03"
CodeAccessURLNotOK Code = "EACS04"
CodeDERPNodeUsesWebsocket Code = `EDERP01`
CodeDERPOneNodeUnhealthy Code = `EDERP02`
)
// @typescript-generate Severity
@@ -18,3 +46,17 @@ var severityRank = map[Severity]int{
func (s Severity) Value() int {
return severityRank[s]
}
// 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()
}
+5 -6
View File
@@ -2,7 +2,6 @@ package healthcheck
import (
"context"
"fmt"
"sync"
"time"
@@ -104,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(fmt.Sprint(err))
report.DERP.Error = ptr.Ref(health.Messagef(health.CodeUnknown, "derp report panic: %s", err))
}
}()
@@ -116,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(fmt.Sprint(err))
report.AccessURL.Error = ptr.Ref(health.Messagef(health.CodeUnknown, "access url report panic: %s", err))
}
}()
@@ -128,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(fmt.Sprint(err))
report.Websocket.Error = ptr.Ref(health.Messagef(health.CodeUnknown, "websocket report panic: %s", err))
}
}()
@@ -140,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(fmt.Sprint(err))
report.Database.Error = ptr.Ref(health.Messagef(health.CodeUnknown, "database report panic: %s", err))
}
}()
@@ -152,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(fmt.Sprint(err))
report.WorkspaceProxy.Error = ptr.Ref(health.Messagef(health.CodeUnknown, "proxy report panic: %s", err))
}
}()
+6 -4
View File
@@ -13,6 +13,7 @@ import (
"nhooyr.io/websocket"
"github.com/coder/coder/v2/coderd/healthcheck/health"
"github.com/coder/coder/v2/coderd/util/ptr"
)
// @typescript-generate WebsocketReport
@@ -75,6 +76,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.Severity = health.SeverityError
return
}
@@ -84,26 +86,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 = convertError(xerrors.Errorf("write message: %w", err))
r.Error = ptr.Ref(health.Messagef(health.CodeWebsocketEcho, "write message: %s", err))
r.Severity = health.SeverityError
return
}
ty, got, err := c.Read(ctx)
if err != nil {
r.Error = convertError(xerrors.Errorf("read message: %w", err))
r.Error = ptr.Ref(health.Messagef(health.CodeWebsocketEcho, "read message: %s", err))
r.Severity = health.SeverityError
return
}
if ty != websocket.MessageText {
r.Error = convertError(xerrors.Errorf("received incorrect message type: %v", ty))
r.Error = ptr.Ref(health.Messagef(health.CodeWebsocketMsg, "received incorrect message type: %v", ty))
r.Severity = health.SeverityError
return
}
if string(got) != msg {
r.Error = convertError(xerrors.Errorf("received incorrect message: wanted %q, got %q", msg, string(got)))
r.Error = ptr.Ref(health.Messagef(health.CodeWebsocketMsg, "received incorrect message: wanted %q, got %q", msg, string(got)))
r.Severity = health.SeverityError
return
}
+3 -1
View File
@@ -63,7 +63,9 @@ func TestWebsocket(t *testing.T) {
APIKey: "test",
})
require.NotNil(t, wsReport.Error)
if assert.NotNil(t, wsReport.Error) {
assert.Contains(t, *wsReport.Error, health.CodeWebsocketDial)
}
require.Equal(t, health.SeverityError, wsReport.Severity)
assert.Equal(t, wsReport.Body, "test error")
assert.Equal(t, wsReport.Code, http.StatusBadRequest)
+12 -10
View File
@@ -6,12 +6,12 @@ import (
"sort"
"strings"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/coderd/healthcheck/health"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"golang.org/x/xerrors"
)
// @typescript-generate WorkspaceProxyReport
@@ -64,7 +64,7 @@ func (r *WorkspaceProxyReport) Run(ctx context.Context, opts *WorkspaceProxyRepo
// If this fails, just mark it as a warning. It is still updated in the background.
if err := opts.WorkspaceProxiesFetchUpdater.Update(ctx); err != nil {
r.Severity = health.SeverityWarning
r.Warnings = append(r.Warnings, xerrors.Errorf("update proxy health: %w", err).Error())
r.Warnings = append(r.Warnings, health.Messagef(health.CodeProxyUpdate, "update proxy health: %s", err))
return
}
@@ -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(err.Error())
r.Error = ptr.Ref(health.Messagef(health.CodeProxyFetch, "fetch workspace proxies: %s", err))
return
}
@@ -99,11 +99,13 @@ func (r *WorkspaceProxyReport) Run(ctx context.Context, opts *WorkspaceProxyRepo
r.Severity = calculateSeverity(total, healthy)
r.Healthy = r.Severity.Value() < health.SeverityError.Value()
switch r.Severity {
case health.SeverityWarning, health.SeverityOK:
r.Warnings = append(r.Warnings, errs...)
case health.SeverityError:
r.appendError(errs...)
for _, err := range errs {
switch r.Severity {
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))
}
}
// Versions _must_ match. Perform this check last. This will clobber any other severity.
@@ -111,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(fmt.Sprintf("%s: %s", proxy.Name, vErr.Error()))
r.appendError(health.Messagef(health.CodeProxyVersionMismatch, vErr.Error()))
}
}
}
+32 -6
View File
@@ -2,6 +2,7 @@ package healthcheck_test
import (
"context"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -26,6 +27,7 @@ func TestWorkspaceProxies(t *testing.T) {
updateProxyHealth func(context.Context) error
expectedHealthy bool
expectedError string
expectedWarning string
expectedSeverity health.Severity
}{
{
@@ -53,6 +55,7 @@ func TestWorkspaceProxies(t *testing.T) {
updateProxyHealth: fakeUpdateProxyHealth(nil),
expectedHealthy: false,
expectedSeverity: health.SeverityError,
expectedError: string(health.CodeProxyUnhealthy),
},
{
name: "Enabled/OneUnreachable",
@@ -80,7 +83,7 @@ func TestWorkspaceProxies(t *testing.T) {
updateProxyHealth: fakeUpdateProxyHealth(nil),
expectedHealthy: false,
expectedSeverity: health.SeverityError,
expectedError: "connect: connection refused",
expectedError: string(health.CodeProxyUnhealthy),
},
{
name: "Enabled/AllHealthy",
@@ -103,6 +106,7 @@ func TestWorkspaceProxies(t *testing.T) {
updateProxyHealth: fakeUpdateProxyHealth(nil),
expectedHealthy: true,
expectedSeverity: health.SeverityWarning,
expectedWarning: string(health.CodeProxyUnhealthy),
},
{
name: "Enabled/AllUnhealthy",
@@ -113,6 +117,7 @@ func TestWorkspaceProxies(t *testing.T) {
updateProxyHealth: fakeUpdateProxyHealth(nil),
expectedHealthy: false,
expectedSeverity: health.SeverityError,
expectedError: string(health.CodeProxyUnhealthy),
},
{
name: "Enabled/OneOutOfDate",
@@ -150,7 +155,7 @@ func TestWorkspaceProxies(t *testing.T) {
updateProxyHealth: fakeUpdateProxyHealth(nil),
expectedHealthy: false,
expectedSeverity: health.SeverityError,
expectedError: assert.AnError.Error(),
expectedError: string(health.CodeProxyFetch),
},
{
name: "Enabled/ErrUpdateProxyHealth",
@@ -158,6 +163,7 @@ func TestWorkspaceProxies(t *testing.T) {
updateProxyHealth: fakeUpdateProxyHealth(assert.AnError),
expectedHealthy: true,
expectedSeverity: health.SeverityWarning,
expectedWarning: string(health.CodeProxyUpdate),
},
} {
tt := tt
@@ -179,13 +185,22 @@ func TestWorkspaceProxies(t *testing.T) {
assert.Equal(t, tt.expectedHealthy, rpt.Healthy)
assert.Equal(t, tt.expectedSeverity, rpt.Severity)
if tt.expectedError != "" {
assert.NotNil(t, rpt.Error)
if tt.expectedError != "" && assert.NotNil(t, rpt.Error) {
assert.Contains(t, *rpt.Error, tt.expectedError)
} else {
if !assert.Nil(t, rpt.Error) {
assert.Empty(t, *rpt.Error)
assert.Nil(t, rpt.Error)
}
if tt.expectedWarning != "" && assert.NotEmpty(t, rpt.Warnings) {
var found bool
for _, w := range rpt.Warnings {
if strings.Contains(w, tt.expectedWarning) {
found = true
break
}
}
assert.True(t, found, "expected warning %s not found in %v", tt.expectedWarning, rpt.Warnings)
} else {
assert.Empty(t, rpt.Warnings)
}
})
}
@@ -221,13 +236,24 @@ func (u *fakeWorkspaceProxyFetchUpdater) Update(ctx context.Context) error {
return u.updateFunc(ctx)
}
//nolint:revive // yes, this is a control flag, and that is OK in a unit test.
func fakeWorkspaceProxy(name string, healthy bool, version string) codersdk.WorkspaceProxy {
var status codersdk.WorkspaceProxyStatus
if !healthy {
status = codersdk.WorkspaceProxyStatus{
Status: codersdk.ProxyUnreachable,
Report: codersdk.ProxyHealthReport{
Errors: []string{assert.AnError.Error()},
},
}
}
return codersdk.WorkspaceProxy{
Region: codersdk.Region{
Name: name,
Healthy: healthy,
},
Version: version,
Status: status,
}
}