mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +00:00
feat: add dismissed
property to the healthcheck section (#10940)
This commit is contained in:
15
coderd/apidoc/docs.go
generated
15
coderd/apidoc/docs.go
generated
@ -12305,6 +12305,9 @@ const docTemplate = `{
|
||||
"derphealth.Report": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dismissed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -12383,6 +12386,9 @@ const docTemplate = `{
|
||||
"access_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"dismissed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -12422,6 +12428,9 @@ const docTemplate = `{
|
||||
"healthcheck.DatabaseReport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dismissed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -12522,6 +12531,9 @@ const docTemplate = `{
|
||||
"code": {
|
||||
"type": "integer"
|
||||
},
|
||||
"dismissed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -12552,6 +12564,9 @@ const docTemplate = `{
|
||||
"healthcheck.WorkspaceProxyReport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dismissed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
|
15
coderd/apidoc/swagger.json
generated
15
coderd/apidoc/swagger.json
generated
@ -11206,6 +11206,9 @@
|
||||
"derphealth.Report": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dismissed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -11272,6 +11275,9 @@
|
||||
"access_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"dismissed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -11307,6 +11313,9 @@
|
||||
"healthcheck.DatabaseReport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dismissed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -11399,6 +11408,9 @@
|
||||
"code": {
|
||||
"type": "integer"
|
||||
},
|
||||
"dismissed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -11425,6 +11437,9 @@
|
||||
"healthcheck.WorkspaceProxyReport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dismissed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -25,6 +25,7 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
httpSwagger "github.com/swaggo/http-swagger/v2"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/xerrors"
|
||||
"google.golang.org/api/idtoken"
|
||||
"storj.io/drpc/drpcmux"
|
||||
@ -407,24 +408,30 @@ func New(options *Options) *API {
|
||||
|
||||
if options.HealthcheckFunc == nil {
|
||||
options.HealthcheckFunc = func(ctx context.Context, apiKey string) *healthcheck.Report {
|
||||
dismissedHealthchecks := loadDismissedHealthchecks(ctx, options.Database, options.Logger)
|
||||
return healthcheck.Run(ctx, &healthcheck.ReportOptions{
|
||||
Database: healthcheck.DatabaseReportOptions{
|
||||
DB: options.Database,
|
||||
Threshold: options.DeploymentValues.Healthcheck.ThresholdDatabase.Value(),
|
||||
Dismissed: slices.Contains(dismissedHealthchecks, healthcheck.SectionDatabase),
|
||||
},
|
||||
Websocket: healthcheck.WebsocketReportOptions{
|
||||
AccessURL: options.AccessURL,
|
||||
APIKey: apiKey,
|
||||
Dismissed: slices.Contains(dismissedHealthchecks, healthcheck.SectionWebsocket),
|
||||
},
|
||||
AccessURL: healthcheck.AccessURLReportOptions{
|
||||
AccessURL: options.AccessURL,
|
||||
Dismissed: slices.Contains(dismissedHealthchecks, healthcheck.SectionAccessURL),
|
||||
},
|
||||
DerpHealth: derphealth.ReportOptions{
|
||||
DERPMap: api.DERPMap(),
|
||||
DERPMap: api.DERPMap(),
|
||||
Dismissed: slices.Contains(dismissedHealthchecks, healthcheck.SectionDERP),
|
||||
},
|
||||
WorkspaceProxy: healthcheck.WorkspaceProxyReportOptions{
|
||||
CurrentVersion: buildinfo.Version(),
|
||||
WorkspaceProxiesFetchUpdater: *(options.WorkspaceProxiesFetchUpdater).Load(),
|
||||
Dismissed: slices.Contains(dismissedHealthchecks, healthcheck.SectionWorkspaceProxy),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -3,15 +3,17 @@ package coderd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"cdr.dev/slog"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/audit"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
@ -253,3 +255,19 @@ func validateHealthSettings(settings codersdk.HealthSettings) error {
|
||||
// @Router /debug/ws [get]
|
||||
// @x-apidocgen {"skip": true}
|
||||
func _debugws(http.ResponseWriter, *http.Request) {} //nolint:unused
|
||||
|
||||
func loadDismissedHealthchecks(ctx context.Context, db database.Store, logger slog.Logger) []string {
|
||||
dismissedHealthchecks := []string{}
|
||||
settingsJSON, err := db.GetHealthSettings(ctx)
|
||||
if err == nil {
|
||||
var settings codersdk.HealthSettings
|
||||
err = json.Unmarshal([]byte(settingsJSON), &settings)
|
||||
if len(settings.DismissedHealthchecks) > 0 {
|
||||
dismissedHealthchecks = settings.DismissedHealthchecks
|
||||
}
|
||||
}
|
||||
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
|
||||
logger.Error(ctx, "unable to fetch health settings: %w", err)
|
||||
}
|
||||
return dismissedHealthchecks
|
||||
}
|
||||
|
@ -16,9 +16,10 @@ import (
|
||||
// @typescript-generate AccessURLReport
|
||||
type AccessURLReport struct {
|
||||
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
|
||||
Healthy bool `json:"healthy"`
|
||||
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Healthy bool `json:"healthy"`
|
||||
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Dismissed bool `json:"dismissed"`
|
||||
|
||||
AccessURL string `json:"access_url"`
|
||||
Reachable bool `json:"reachable"`
|
||||
@ -30,6 +31,8 @@ type AccessURLReport struct {
|
||||
type AccessURLReportOptions struct {
|
||||
AccessURL *url.URL
|
||||
Client *http.Client
|
||||
|
||||
Dismissed bool
|
||||
}
|
||||
|
||||
func (r *AccessURLReport) Run(ctx context.Context, opts *AccessURLReportOptions) {
|
||||
@ -38,6 +41,8 @@ func (r *AccessURLReport) Run(ctx context.Context, opts *AccessURLReportOptions)
|
||||
|
||||
r.Severity = health.SeverityOK
|
||||
r.Warnings = []string{}
|
||||
r.Dismissed = opts.Dismissed
|
||||
|
||||
if opts.AccessURL == nil {
|
||||
r.Error = ptr.Ref("access URL is nil")
|
||||
r.Severity = health.SeverityError
|
||||
|
@ -109,6 +109,23 @@ func TestAccessURL(t *testing.T) {
|
||||
require.NotNil(t, report.Error)
|
||||
assert.Contains(t, *report.Error, expErr.Error())
|
||||
})
|
||||
|
||||
t.Run("DismissedError", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
report healthcheck.AccessURLReport
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
report.Run(ctx, &healthcheck.AccessURLReportOptions{
|
||||
Dismissed: true,
|
||||
})
|
||||
|
||||
assert.True(t, report.Dismissed)
|
||||
assert.Equal(t, health.SeverityError, report.Severity)
|
||||
})
|
||||
}
|
||||
|
||||
type roundTripFunc func(r *http.Request) (*http.Response, error)
|
||||
|
@ -18,9 +18,10 @@ const (
|
||||
// @typescript-generate DatabaseReport
|
||||
type DatabaseReport struct {
|
||||
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
|
||||
Healthy bool `json:"healthy"`
|
||||
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Healthy bool `json:"healthy"`
|
||||
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Dismissed bool `json:"dismissed"`
|
||||
|
||||
Reachable bool `json:"reachable"`
|
||||
Latency string `json:"latency"`
|
||||
@ -32,11 +33,15 @@ type DatabaseReport struct {
|
||||
type DatabaseReportOptions struct {
|
||||
DB database.Store
|
||||
Threshold time.Duration
|
||||
|
||||
Dismissed bool
|
||||
}
|
||||
|
||||
func (r *DatabaseReport) Run(ctx context.Context, opts *DatabaseReportOptions) {
|
||||
r.Warnings = []string{}
|
||||
r.Severity = health.SeverityOK
|
||||
r.Dismissed = opts.Dismissed
|
||||
|
||||
r.ThresholdMS = opts.Threshold.Milliseconds()
|
||||
if r.ThresholdMS == 0 {
|
||||
r.ThresholdMS = DatabaseDefaultThreshold.Milliseconds()
|
||||
|
@ -67,6 +67,26 @@ func TestDatabase(t *testing.T) {
|
||||
assert.Contains(t, *report.Error, err.Error())
|
||||
})
|
||||
|
||||
t.Run("DismissedError", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
report = healthcheck.DatabaseReport{}
|
||||
db = dbmock.NewMockStore(gomock.NewController(t))
|
||||
err = xerrors.New("ping error")
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
db.EXPECT().Ping(gomock.Any()).Return(time.Duration(0), err)
|
||||
|
||||
report.Run(ctx, &healthcheck.DatabaseReportOptions{DB: db, Dismissed: true})
|
||||
|
||||
assert.Equal(t, health.SeverityError, report.Severity)
|
||||
assert.True(t, report.Dismissed)
|
||||
require.NotNil(t, report.Error)
|
||||
})
|
||||
|
||||
t.Run("Median", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
@ -36,9 +36,10 @@ const (
|
||||
// @typescript-generate Report
|
||||
type Report struct {
|
||||
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
|
||||
Healthy bool `json:"healthy"`
|
||||
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Healthy bool `json:"healthy"`
|
||||
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Dismissed bool `json:"dismissed"`
|
||||
|
||||
Regions map[int]*RegionReport `json:"regions"`
|
||||
|
||||
@ -95,15 +96,18 @@ type StunReport struct {
|
||||
}
|
||||
|
||||
type ReportOptions struct {
|
||||
Dismissed bool
|
||||
|
||||
DERPMap *tailcfg.DERPMap
|
||||
}
|
||||
|
||||
func (r *Report) Run(ctx context.Context, opts *ReportOptions) {
|
||||
r.Healthy = true
|
||||
r.Severity = health.SeverityOK
|
||||
r.Warnings = []string{}
|
||||
r.Dismissed = opts.Dismissed
|
||||
|
||||
r.Regions = map[int]*RegionReport{}
|
||||
r.Warnings = []string{}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
mu := sync.Mutex{}
|
||||
|
@ -120,6 +120,7 @@ func TestDERP(t *testing.T) {
|
||||
}},
|
||||
},
|
||||
}},
|
||||
Dismissed: true, // Let's sneak an extra unit test
|
||||
}
|
||||
)
|
||||
|
||||
@ -127,6 +128,7 @@ func TestDERP(t *testing.T) {
|
||||
|
||||
assert.True(t, report.Healthy)
|
||||
assert.Equal(t, health.SeverityWarning, report.Severity)
|
||||
assert.True(t, report.Dismissed)
|
||||
for _, region := range report.Regions {
|
||||
assert.True(t, region.Healthy)
|
||||
assert.True(t, region.NodeReports[0].Healthy)
|
||||
|
@ -15,30 +15,35 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/healthcheck/health"
|
||||
)
|
||||
|
||||
type WebsocketReportOptions struct {
|
||||
APIKey string
|
||||
AccessURL *url.URL
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// @typescript-generate WebsocketReport
|
||||
type WebsocketReport struct {
|
||||
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
|
||||
Healthy bool `json:"healthy"`
|
||||
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Healthy bool `json:"healthy"`
|
||||
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Dismissed bool `json:"dismissed"`
|
||||
|
||||
Body string `json:"body"`
|
||||
Code int `json:"code"`
|
||||
Error *string `json:"error"`
|
||||
}
|
||||
|
||||
type WebsocketReportOptions struct {
|
||||
APIKey string
|
||||
AccessURL *url.URL
|
||||
HTTPClient *http.Client
|
||||
|
||||
Dismissed bool
|
||||
}
|
||||
|
||||
func (r *WebsocketReport) Run(ctx context.Context, opts *WebsocketReportOptions) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
r.Severity = health.SeverityOK
|
||||
r.Warnings = []string{}
|
||||
r.Dismissed = opts.Dismissed
|
||||
|
||||
u, err := opts.AccessURL.Parse("/api/v2/debug/ws")
|
||||
if err != nil {
|
||||
r.Error = convertError(xerrors.Errorf("parse access url: %w", err))
|
||||
|
@ -68,4 +68,22 @@ func TestWebsocket(t *testing.T) {
|
||||
assert.Equal(t, wsReport.Body, "test error")
|
||||
assert.Equal(t, wsReport.Code, http.StatusBadRequest)
|
||||
})
|
||||
|
||||
t.Run("DismissedError", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
wsReport := healthcheck.WebsocketReport{}
|
||||
wsReport.Run(ctx, &healthcheck.WebsocketReportOptions{
|
||||
AccessURL: &url.URL{Host: "fake"},
|
||||
Dismissed: true,
|
||||
})
|
||||
|
||||
require.True(t, wsReport.Dismissed)
|
||||
require.Equal(t, health.SeverityError, wsReport.Severity)
|
||||
require.NotNil(t, wsReport.Error)
|
||||
require.Equal(t, health.SeverityError, wsReport.Severity)
|
||||
})
|
||||
}
|
||||
|
@ -14,21 +14,24 @@ import (
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// @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"`
|
||||
|
||||
WorkspaceProxies codersdk.RegionsResponse[codersdk.WorkspaceProxy] `json:"workspace_proxies"`
|
||||
}
|
||||
|
||||
type WorkspaceProxyReportOptions struct {
|
||||
// CurrentVersion is the current server version.
|
||||
// We pass this in to make it easier to test.
|
||||
CurrentVersion string
|
||||
WorkspaceProxiesFetchUpdater WorkspaceProxiesFetchUpdater
|
||||
}
|
||||
|
||||
// @typescript-generate WorkspaceProxyReport
|
||||
type WorkspaceProxyReport struct {
|
||||
Healthy bool `json:"healthy"`
|
||||
Severity health.Severity `json:"severity"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Error *string `json:"error"`
|
||||
|
||||
WorkspaceProxies codersdk.RegionsResponse[codersdk.WorkspaceProxy] `json:"workspace_proxies"`
|
||||
Dismissed bool
|
||||
}
|
||||
|
||||
type WorkspaceProxiesFetchUpdater interface {
|
||||
@ -52,6 +55,7 @@ func (r *WorkspaceProxyReport) Run(ctx context.Context, opts *WorkspaceProxyRepo
|
||||
r.Healthy = true
|
||||
r.Severity = health.SeverityOK
|
||||
r.Warnings = []string{}
|
||||
r.Dismissed = opts.Dismissed
|
||||
|
||||
if opts.WorkspaceProxiesFetchUpdater == nil {
|
||||
opts.WorkspaceProxiesFetchUpdater = &AGPLWorkspaceProxiesFetchUpdater{}
|
||||
|
@ -191,6 +191,22 @@ func TestWorkspaceProxies(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkspaceProxy_ErrorDismissed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var report healthcheck.WorkspaceProxyReport
|
||||
report.Run(context.Background(), &healthcheck.WorkspaceProxyReportOptions{
|
||||
WorkspaceProxiesFetchUpdater: &fakeWorkspaceProxyFetchUpdater{
|
||||
fetchFunc: fakeFetchWorkspaceProxiesErr(assert.AnError),
|
||||
updateFunc: fakeUpdateProxyHealth(assert.AnError),
|
||||
},
|
||||
Dismissed: true,
|
||||
})
|
||||
|
||||
assert.True(t, report.Dismissed)
|
||||
assert.Equal(t, health.SeverityWarning, report.Severity)
|
||||
}
|
||||
|
||||
// yet another implementation of the thing
|
||||
type fakeWorkspaceProxyFetchUpdater struct {
|
||||
fetchFunc func(context.Context) (codersdk.RegionsResponse[codersdk.WorkspaceProxy], error)
|
||||
|
Reference in New Issue
Block a user