Skip to content
Merged
15 changes: 15 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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),
},
})
Comment on lines +411 to 436
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-blocking: this anonymous function is getting a bit heavy-weight and could probably use a refactor of some sort.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack

}
Expand Down
20 changes: 19 additions & 1 deletion coderd/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
11 changes: 8 additions & 3 deletions coderd/healthcheck/accessurl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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) {
Expand All @@ -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
Expand Down
17 changes: 17 additions & 0 deletions coderd/healthcheck/accessurl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 8 additions & 3 deletions coderd/healthcheck/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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()
Expand Down
20 changes: 20 additions & 0 deletions coderd/healthcheck/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
12 changes: 8 additions & 4 deletions coderd/healthcheck/derphealth/derp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Expand Down Expand Up @@ -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{}
Expand Down
2 changes: 2 additions & 0 deletions coderd/healthcheck/derphealth/derp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,15 @@ func TestDERP(t *testing.T) {
}},
},
}},
Dismissed: true, // Let's sneak an extra unit test
}
)

report.Run(ctx, opts)

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)
Expand Down
23 changes: 14 additions & 9 deletions coderd/healthcheck/websocket.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading