feat(coderd): add provisioner_daemons to /debug/health endpoint (#11393)

Adds a healthcheck for provisioner daemons to /debug/health endpoint.
This commit is contained in:
Cian Johnston 2024-01-08 09:29:04 +00:00 committed by GitHub
parent 31f8fac1b9
commit 04fd96a014
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 791 additions and 94 deletions

45
coderd/apidoc/docs.go generated
View File

@ -9408,14 +9408,16 @@ const docTemplate = `{
"AccessURL",
"Websocket",
"Database",
"WorkspaceProxy"
"WorkspaceProxy",
"ProvisionerDaemons"
],
"x-enum-varnames": [
"HealthSectionDERP",
"HealthSectionAccessURL",
"HealthSectionWebsocket",
"HealthSectionDatabase",
"HealthSectionWorkspaceProxy"
"HealthSectionWorkspaceProxy",
"HealthSectionProvisionerDaemons"
]
},
"codersdk.HealthSettings": {
@ -12957,7 +12959,10 @@ const docTemplate = `{
"EACS03",
"EACS04",
"EDERP01",
"EDERP02"
"EDERP02",
"EPD01",
"EPD02",
"EPD03"
],
"x-enum-varnames": [
"CodeUnknown",
@ -12975,7 +12980,10 @@ const docTemplate = `{
"CodeAccessURLFetch",
"CodeAccessURLNotOK",
"CodeDERPNodeUsesWebsocket",
"CodeDERPOneNodeUnhealthy"
"CodeDERPOneNodeUnhealthy",
"CodeProvisionerDaemonsNoProvisionerDaemons",
"CodeProvisionerDaemonVersionMismatch",
"CodeProvisionerDaemonAPIMajorVersionDeprecated"
]
},
"health.Message": {
@ -13092,6 +13100,32 @@ const docTemplate = `{
}
}
},
"healthcheck.ProvisionerDaemonsReport": {
"type": "object",
"properties": {
"dismissed": {
"type": "boolean"
},
"error": {
"type": "string"
},
"provisioner_daemons": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.ProvisionerDaemon"
}
},
"severity": {
"$ref": "#/definitions/health.Severity"
},
"warnings": {
"type": "array",
"items": {
"$ref": "#/definitions/health.Message"
}
}
}
},
"healthcheck.Report": {
"type": "object",
"properties": {
@ -13119,6 +13153,9 @@ const docTemplate = `{
"description": "Healthy is true if the report returns no errors.\nDeprecated: use ` + "`" + `Severity` + "`" + ` instead",
"type": "boolean"
},
"provisioner_daemons": {
"$ref": "#/definitions/healthcheck.ProvisionerDaemonsReport"
},
"severity": {
"description": "Severity indicates the status of Coder health.",
"enum": [

View File

@ -8440,13 +8440,21 @@
},
"codersdk.HealthSection": {
"type": "string",
"enum": ["DERP", "AccessURL", "Websocket", "Database", "WorkspaceProxy"],
"enum": [
"DERP",
"AccessURL",
"Websocket",
"Database",
"WorkspaceProxy",
"ProvisionerDaemons"
],
"x-enum-varnames": [
"HealthSectionDERP",
"HealthSectionAccessURL",
"HealthSectionWebsocket",
"HealthSectionDatabase",
"HealthSectionWorkspaceProxy"
"HealthSectionWorkspaceProxy",
"HealthSectionProvisionerDaemons"
]
},
"codersdk.HealthSettings": {
@ -11791,7 +11799,10 @@
"EACS03",
"EACS04",
"EDERP01",
"EDERP02"
"EDERP02",
"EPD01",
"EPD02",
"EPD03"
],
"x-enum-varnames": [
"CodeUnknown",
@ -11809,7 +11820,10 @@
"CodeAccessURLFetch",
"CodeAccessURLNotOK",
"CodeDERPNodeUsesWebsocket",
"CodeDERPOneNodeUnhealthy"
"CodeDERPOneNodeUnhealthy",
"CodeProvisionerDaemonsNoProvisionerDaemons",
"CodeProvisionerDaemonVersionMismatch",
"CodeProvisionerDaemonAPIMajorVersionDeprecated"
]
},
"health.Message": {
@ -11910,6 +11924,32 @@
}
}
},
"healthcheck.ProvisionerDaemonsReport": {
"type": "object",
"properties": {
"dismissed": {
"type": "boolean"
},
"error": {
"type": "string"
},
"provisioner_daemons": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.ProvisionerDaemon"
}
},
"severity": {
"$ref": "#/definitions/health.Severity"
},
"warnings": {
"type": "array",
"items": {
"$ref": "#/definitions/health.Message"
}
}
}
},
"healthcheck.Report": {
"type": "object",
"properties": {
@ -11937,6 +11977,9 @@
"description": "Healthy is true if the report returns no errors.\nDeprecated: use `Severity` instead",
"type": "boolean"
},
"provisioner_daemons": {
"$ref": "#/definitions/healthcheck.ProvisionerDaemonsReport"
},
"severity": {
"description": "Severity indicates the status of Coder health.",
"enum": ["ok", "warning", "error"],

View File

@ -440,6 +440,12 @@ func New(options *Options) *API {
CurrentVersion: buildinfo.Version(),
WorkspaceProxiesFetchUpdater: *(options.WorkspaceProxiesFetchUpdater).Load(),
},
ProvisionerDaemons: healthcheck.ProvisionerDaemonsReportDeps{
CurrentVersion: buildinfo.Version(),
CurrentAPIMajorVersion: provisionersdk.CurrentMajor,
Store: options.Database,
// TimeNow and StaleInterval set to defaults, see healthcheck/provisioner.go
},
})
}
}
@ -1188,7 +1194,7 @@ func (api *API) CreateInMemoryProvisionerDaemon(ctx context.Context, name string
Tags: provisionersdk.MutateTags(uuid.Nil, nil),
LastSeenAt: sql.NullTime{Time: dbtime.Now(), Valid: true},
Version: buildinfo.Version(),
APIVersion: provisionersdk.APIVersionCurrent,
APIVersion: provisionersdk.VersionCurrent.String(),
})
if err != nil {
return nil, xerrors.Errorf("failed to create in-memory provisioner daemon: %w", err)

View File

@ -416,3 +416,19 @@ func Apps(dbApps []database.WorkspaceApp, agent database.WorkspaceAgent, ownerNa
}
return apps
}
func ProvisionerDaemon(dbDaemon database.ProvisionerDaemon) codersdk.ProvisionerDaemon {
result := codersdk.ProvisionerDaemon{
ID: dbDaemon.ID,
CreatedAt: dbDaemon.CreatedAt,
LastSeenAt: codersdk.NullTime{NullTime: dbDaemon.LastSeenAt},
Name: dbDaemon.Name,
Tags: dbDaemon.Tags,
Version: dbDaemon.Version,
APIVersion: dbDaemon.APIVersion,
}
for _, provisionerType := range dbDaemon.Provisioners {
result.Provisioners = append(result.Provisioners, codersdk.ProvisionerType(provisionerType))
}
return result
}

View File

@ -218,7 +218,7 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) {
CreatedAt: now.Add(-14 * 24 * time.Hour),
LastSeenAt: sql.NullTime{Valid: true, Time: now.Add(-7 * 24 * time.Hour).Add(time.Minute)},
Version: "1.0.0",
APIVersion: provisionersdk.APIVersionCurrent,
APIVersion: provisionersdk.VersionCurrent.String(),
})
require.NoError(t, err)
_, err = db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{
@ -229,7 +229,7 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) {
CreatedAt: now.Add(-8 * 24 * time.Hour),
LastSeenAt: sql.NullTime{Valid: true, Time: now.Add(-8 * 24 * time.Hour).Add(time.Hour)},
Version: "1.0.0",
APIVersion: provisionersdk.APIVersionCurrent,
APIVersion: provisionersdk.VersionCurrent.String(),
})
require.NoError(t, err)
_, err = db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{
@ -242,7 +242,7 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) {
},
CreatedAt: now.Add(-9 * 24 * time.Hour),
Version: "1.0.0",
APIVersion: provisionersdk.APIVersionCurrent,
APIVersion: provisionersdk.VersionCurrent.String(),
})
require.NoError(t, err)
_, err = db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{
@ -256,7 +256,7 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) {
CreatedAt: now.Add(-6 * 24 * time.Hour),
LastSeenAt: sql.NullTime{Valid: true, Time: now.Add(-6 * 24 * time.Hour)},
Version: "1.0.0",
APIVersion: provisionersdk.APIVersionCurrent,
APIVersion: provisionersdk.VersionCurrent.String(),
})
require.NoError(t, err)

View File

@ -34,6 +34,10 @@ const (
CodeDERPNodeUsesWebsocket Code = `EDERP01`
CodeDERPOneNodeUnhealthy Code = `EDERP02`
CodeProvisionerDaemonsNoProvisionerDaemons Code = `EPD01`
CodeProvisionerDaemonVersionMismatch Code = `EPD02`
CodeProvisionerDaemonAPIMajorVersionDeprecated Code = `EPD03`
)
// @typescript-generate Severity

View File

@ -18,6 +18,7 @@ type Checker interface {
Websocket(ctx context.Context, opts *WebsocketReportOptions) WebsocketReport
Database(ctx context.Context, opts *DatabaseReportOptions) DatabaseReport
WorkspaceProxy(ctx context.Context, opts *WorkspaceProxyReportOptions) WorkspaceProxyReport
ProvisionerDaemons(ctx context.Context, opts *ProvisionerDaemonsReportDeps) ProvisionerDaemonsReport
}
// @typescript-generate Report
@ -32,49 +33,62 @@ type Report struct {
// FailingSections is a list of sections that have failed their healthcheck.
FailingSections []codersdk.HealthSection `json:"failing_sections"`
DERP derphealth.Report `json:"derp"`
AccessURL AccessURLReport `json:"access_url"`
Websocket WebsocketReport `json:"websocket"`
Database DatabaseReport `json:"database"`
WorkspaceProxy WorkspaceProxyReport `json:"workspace_proxy"`
DERP derphealth.Report `json:"derp"`
AccessURL AccessURLReport `json:"access_url"`
Websocket WebsocketReport `json:"websocket"`
Database DatabaseReport `json:"database"`
WorkspaceProxy WorkspaceProxyReport `json:"workspace_proxy"`
ProvisionerDaemons ProvisionerDaemonsReport `json:"provisioner_daemons"`
// The Coder version of the server that the report was generated on.
CoderVersion string `json:"coder_version"`
}
type ReportOptions struct {
AccessURL AccessURLReportOptions
Database DatabaseReportOptions
DerpHealth derphealth.ReportOptions
Websocket WebsocketReportOptions
WorkspaceProxy WorkspaceProxyReportOptions
AccessURL AccessURLReportOptions
Database DatabaseReportOptions
DerpHealth derphealth.ReportOptions
Websocket WebsocketReportOptions
WorkspaceProxy WorkspaceProxyReportOptions
ProvisionerDaemons ProvisionerDaemonsReportDeps
Checker Checker
}
type defaultChecker struct{}
func (defaultChecker) DERP(ctx context.Context, opts *derphealth.ReportOptions) (report derphealth.Report) {
func (defaultChecker) DERP(ctx context.Context, opts *derphealth.ReportOptions) derphealth.Report {
var report derphealth.Report
report.Run(ctx, opts)
return report
}
func (defaultChecker) AccessURL(ctx context.Context, opts *AccessURLReportOptions) (report AccessURLReport) {
func (defaultChecker) AccessURL(ctx context.Context, opts *AccessURLReportOptions) AccessURLReport {
var report AccessURLReport
report.Run(ctx, opts)
return report
}
func (defaultChecker) Websocket(ctx context.Context, opts *WebsocketReportOptions) (report WebsocketReport) {
func (defaultChecker) Websocket(ctx context.Context, opts *WebsocketReportOptions) WebsocketReport {
var report WebsocketReport
report.Run(ctx, opts)
return report
}
func (defaultChecker) Database(ctx context.Context, opts *DatabaseReportOptions) (report DatabaseReport) {
func (defaultChecker) Database(ctx context.Context, opts *DatabaseReportOptions) DatabaseReport {
var report DatabaseReport
report.Run(ctx, opts)
return report
}
func (defaultChecker) WorkspaceProxy(ctx context.Context, opts *WorkspaceProxyReportOptions) (report WorkspaceProxyReport) {
func (defaultChecker) WorkspaceProxy(ctx context.Context, opts *WorkspaceProxyReportOptions) WorkspaceProxyReport {
var report WorkspaceProxyReport
report.Run(ctx, opts)
return report
}
func (defaultChecker) ProvisionerDaemons(ctx context.Context, opts *ProvisionerDaemonsReportDeps) ProvisionerDaemonsReport {
var report ProvisionerDaemonsReport
report.Run(ctx, opts)
return report
}
@ -149,26 +163,41 @@ func Run(ctx context.Context, opts *ReportOptions) *Report {
report.WorkspaceProxy = opts.Checker.WorkspaceProxy(ctx, &opts.WorkspaceProxy)
}()
wg.Add(1)
go func() {
defer wg.Done()
defer func() {
if err := recover(); err != nil {
report.ProvisionerDaemons.Error = health.Errorf(health.CodeUnknown, "provisioner daemon report panic: %s", err)
}
}()
report.ProvisionerDaemons = opts.Checker.ProvisionerDaemons(ctx, &opts.ProvisionerDaemons)
}()
report.CoderVersion = buildinfo.Version()
wg.Wait()
report.Time = time.Now()
report.FailingSections = []codersdk.HealthSection{}
if !report.DERP.Healthy {
if report.DERP.Severity.Value() > health.SeverityWarning.Value() {
report.FailingSections = append(report.FailingSections, codersdk.HealthSectionDERP)
}
if !report.AccessURL.Healthy {
if report.AccessURL.Severity.Value() > health.SeverityOK.Value() {
report.FailingSections = append(report.FailingSections, codersdk.HealthSectionAccessURL)
}
if !report.Websocket.Healthy {
if report.Websocket.Severity.Value() > health.SeverityWarning.Value() {
report.FailingSections = append(report.FailingSections, codersdk.HealthSectionWebsocket)
}
if !report.Database.Healthy {
if report.Database.Severity.Value() > health.SeverityWarning.Value() {
report.FailingSections = append(report.FailingSections, codersdk.HealthSectionDatabase)
}
if !report.WorkspaceProxy.Healthy {
if report.WorkspaceProxy.Severity.Value() > health.SeverityWarning.Value() {
report.FailingSections = append(report.FailingSections, codersdk.HealthSectionWorkspaceProxy)
}
if report.ProvisionerDaemons.Severity.Value() > health.SeverityWarning.Value() {
report.FailingSections = append(report.FailingSections, codersdk.HealthSectionProvisionerDaemons)
}
report.Healthy = len(report.FailingSections) == 0
@ -190,6 +219,9 @@ func Run(ctx context.Context, opts *ReportOptions) *Report {
if report.WorkspaceProxy.Severity.Value() > report.Severity.Value() {
report.Severity = report.WorkspaceProxy.Severity
}
if report.ProvisionerDaemons.Severity.Value() > report.Severity.Value() {
report.Severity = report.ProvisionerDaemons.Severity
}
return &report
}

View File

@ -13,11 +13,12 @@ import (
)
type testChecker struct {
DERPReport derphealth.Report
AccessURLReport healthcheck.AccessURLReport
WebsocketReport healthcheck.WebsocketReport
DatabaseReport healthcheck.DatabaseReport
WorkspaceProxyReport healthcheck.WorkspaceProxyReport
DERPReport derphealth.Report
AccessURLReport healthcheck.AccessURLReport
WebsocketReport healthcheck.WebsocketReport
DatabaseReport healthcheck.DatabaseReport
WorkspaceProxyReport healthcheck.WorkspaceProxyReport
ProvisionerDaemonsReport healthcheck.ProvisionerDaemonsReport
}
func (c *testChecker) DERP(context.Context, *derphealth.ReportOptions) derphealth.Report {
@ -40,6 +41,10 @@ func (c *testChecker) WorkspaceProxy(context.Context, *healthcheck.WorkspaceProx
return c.WorkspaceProxyReport
}
func (c *testChecker) ProvisionerDaemons(context.Context, *healthcheck.ProvisionerDaemonsReportDeps) healthcheck.ProvisionerDaemonsReport {
return c.ProvisionerDaemonsReport
}
func TestHealthcheck(t *testing.T) {
t.Parallel()
@ -72,6 +77,9 @@ func TestHealthcheck(t *testing.T) {
Healthy: true,
Severity: health.SeverityOK,
},
ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{
Severity: health.SeverityOK,
},
},
healthy: true,
severity: health.SeverityOK,
@ -99,6 +107,9 @@ func TestHealthcheck(t *testing.T) {
Healthy: true,
Severity: health.SeverityOK,
},
ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{
Severity: health.SeverityOK,
},
},
healthy: false,
severity: health.SeverityError,
@ -127,6 +138,9 @@ func TestHealthcheck(t *testing.T) {
Healthy: true,
Severity: health.SeverityOK,
},
ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{
Severity: health.SeverityOK,
},
},
healthy: true,
severity: health.SeverityWarning,
@ -154,6 +168,9 @@ func TestHealthcheck(t *testing.T) {
Healthy: true,
Severity: health.SeverityOK,
},
ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{
Severity: health.SeverityOK,
},
},
healthy: false,
severity: health.SeverityWarning,
@ -181,6 +198,9 @@ func TestHealthcheck(t *testing.T) {
Healthy: true,
Severity: health.SeverityOK,
},
ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{
Severity: health.SeverityOK,
},
},
healthy: false,
severity: health.SeverityError,
@ -208,6 +228,9 @@ func TestHealthcheck(t *testing.T) {
Healthy: true,
Severity: health.SeverityOK,
},
ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{
Severity: health.SeverityOK,
},
},
healthy: false,
severity: health.SeverityError,
@ -235,6 +258,9 @@ func TestHealthcheck(t *testing.T) {
Healthy: false,
Severity: health.SeverityError,
},
ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{
Severity: health.SeverityOK,
},
},
severity: health.SeverityError,
healthy: false,
@ -263,6 +289,70 @@ func TestHealthcheck(t *testing.T) {
Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}},
Severity: health.SeverityWarning,
},
ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{
Severity: health.SeverityOK,
},
},
severity: health.SeverityWarning,
healthy: true,
failingSections: []codersdk.HealthSection{},
}, {
name: "ProvisionerDaemonsFail",
checker: &testChecker{
DERPReport: derphealth.Report{
Healthy: true,
Severity: health.SeverityOK,
},
AccessURLReport: healthcheck.AccessURLReport{
Healthy: true,
Severity: health.SeverityOK,
},
WebsocketReport: healthcheck.WebsocketReport{
Healthy: true,
Severity: health.SeverityOK,
},
DatabaseReport: healthcheck.DatabaseReport{
Healthy: true,
Severity: health.SeverityOK,
},
WorkspaceProxyReport: healthcheck.WorkspaceProxyReport{
Healthy: true,
Severity: health.SeverityOK,
},
ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{
Severity: health.SeverityError,
},
},
severity: health.SeverityError,
healthy: false,
failingSections: []codersdk.HealthSection{codersdk.HealthSectionProvisionerDaemons},
}, {
name: "ProvisionerDaemonsWarn",
checker: &testChecker{
DERPReport: derphealth.Report{
Healthy: true,
Severity: health.SeverityOK,
},
AccessURLReport: healthcheck.AccessURLReport{
Healthy: true,
Severity: health.SeverityOK,
},
WebsocketReport: healthcheck.WebsocketReport{
Healthy: true,
Severity: health.SeverityOK,
},
DatabaseReport: healthcheck.DatabaseReport{
Healthy: true,
Severity: health.SeverityOK,
},
WorkspaceProxyReport: healthcheck.WorkspaceProxyReport{
Healthy: true,
Severity: health.SeverityOK,
},
ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{
Severity: health.SeverityWarning,
Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}},
},
},
severity: health.SeverityWarning,
healthy: true,
@ -291,6 +381,9 @@ func TestHealthcheck(t *testing.T) {
Healthy: false,
Severity: health.SeverityError,
},
ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{
Severity: health.SeverityError,
},
},
severity: health.SeverityError,
failingSections: []codersdk.HealthSection{
@ -299,6 +392,7 @@ func TestHealthcheck(t *testing.T) {
codersdk.HealthSectionWebsocket,
codersdk.HealthSectionDatabase,
codersdk.HealthSectionWorkspaceProxy,
codersdk.HealthSectionProvisionerDaemons,
},
}} {
c := c

View File

@ -0,0 +1,136 @@
package healthcheck
import (
"context"
"time"
"golang.org/x/mod/semver"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/healthcheck/health"
"github.com/coder/coder/v2/coderd/provisionerdserver"
"github.com/coder/coder/v2/coderd/util/apiversion"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisionersdk"
)
// @typescript-generate ProvisionerDaemonsReport
type ProvisionerDaemonsReport struct {
Severity health.Severity `json:"severity"`
Warnings []health.Message `json:"warnings"`
Dismissed bool `json:"dismissed"`
Error *string `json:"error"`
ProvisionerDaemons []codersdk.ProvisionerDaemon `json:"provisioner_daemons"`
}
type ProvisionerDaemonsReportDeps struct {
// Required
CurrentVersion string
CurrentAPIMajorVersion int
Store ProvisionerDaemonsStore
// Optional
TimeNow func() time.Time // Defaults to dbtime.Now
StaleInterval time.Duration // Defaults to 3 heartbeats
Dismissed bool
}
type ProvisionerDaemonsStore interface {
GetProvisionerDaemons(ctx context.Context) ([]database.ProvisionerDaemon, error)
}
func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDaemonsReportDeps) {
r.ProvisionerDaemons = make([]codersdk.ProvisionerDaemon, 0)
r.Severity = health.SeverityOK
r.Warnings = make([]health.Message, 0)
r.Dismissed = opts.Dismissed
if opts.TimeNow == nil {
opts.TimeNow = dbtime.Now
}
now := opts.TimeNow()
if opts.StaleInterval == 0 {
opts.StaleInterval = provisionerdserver.DefaultHeartbeatInterval * 3
}
if opts.CurrentVersion == "" {
r.Severity = health.SeverityError
r.Error = ptr.Ref("Developer error: CurrentVersion is empty!")
return
}
if opts.CurrentAPIMajorVersion == 0 {
r.Severity = health.SeverityError
r.Error = ptr.Ref("Developer error: CurrentAPIMajorVersion must be non-zero!")
return
}
if opts.Store == nil {
r.Severity = health.SeverityError
r.Error = ptr.Ref("Developer error: Store is nil!")
return
}
// nolint: gocritic // need an actor to fetch provisioner daemons
daemons, err := opts.Store.GetProvisionerDaemons(dbauthz.AsSystemRestricted(ctx))
if err != nil {
r.Severity = health.SeverityError
r.Error = ptr.Ref("error fetching provisioner daemons: " + err.Error())
return
}
for _, daemon := range daemons {
// Daemon never connected, skip.
if !daemon.LastSeenAt.Valid {
continue
}
// Daemon has gone away, skip.
if now.Sub(daemon.LastSeenAt.Time) > (opts.StaleInterval) {
continue
}
r.ProvisionerDaemons = append(r.ProvisionerDaemons, db2sdk.ProvisionerDaemon(daemon))
// For release versions, just check MAJOR.MINOR and ignore patch.
if !semver.IsValid(daemon.Version) {
if r.Severity.Value() < health.SeverityError.Value() {
r.Severity = health.SeverityError
}
r.Warnings = append(r.Warnings, health.Messagef(health.CodeUnknown, "Provisioner daemon %q reports invalid version %q", opts.CurrentVersion, daemon.Version))
} else if !buildinfo.VersionsMatch(opts.CurrentVersion, daemon.Version) {
if r.Severity.Value() < health.SeverityWarning.Value() {
r.Severity = health.SeverityWarning
}
r.Warnings = append(r.Warnings, health.Messagef(health.CodeProvisionerDaemonVersionMismatch, "Provisioner daemon %q has outdated version %q", daemon.Name, daemon.Version))
}
// Provisioner daemon API version follows different rules; we just want to check the major API version and
// warn about potential later deprecations.
// When we check API versions of connecting provisioner daemons, all active provisioner daemons
// will, by necessity, have a compatible API version.
if maj, _, err := apiversion.Parse(daemon.APIVersion); err != nil {
if r.Severity.Value() < health.SeverityError.Value() {
r.Severity = health.SeverityError
}
r.Warnings = append(r.Warnings, health.Messagef(health.CodeUnknown, "Provisioner daemon %q reports invalid API version: %s", daemon.Name, err.Error()))
} else if maj != opts.CurrentAPIMajorVersion {
if r.Severity.Value() < health.SeverityWarning.Value() {
r.Severity = health.SeverityWarning
}
r.Warnings = append(r.Warnings, health.Messagef(health.CodeProvisionerDaemonAPIMajorVersionDeprecated, "Provisioner daemon %q reports deprecated major API version %d. Consider upgrading!", daemon.Name, provisionersdk.CurrentMajor))
}
}
if len(r.ProvisionerDaemons) == 0 {
r.Severity = health.SeverityError
r.Error = ptr.Ref("No active provisioner daemons found!")
return
}
}

View File

@ -0,0 +1,191 @@
package healthcheck_test
import (
"context"
"database/sql"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbmock"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/healthcheck"
"github.com/coder/coder/v2/coderd/healthcheck/health"
"github.com/coder/coder/v2/provisionersdk"
gomock "go.uber.org/mock/gomock"
)
func TestProvisionerDaemonReport(t *testing.T) {
t.Parallel()
for _, tt := range []struct {
name string
currentVersion string
currentAPIMajorVersion int
provisionerDaemons []database.ProvisionerDaemon
provisionerDaemonsErr error
expectedSeverity health.Severity
expectedWarningCode health.Code
expectedError string
}{
{
name: "current version empty",
currentVersion: "",
expectedSeverity: health.SeverityError,
expectedError: "Developer error: CurrentVersion is empty",
},
{
name: "no daemons",
currentVersion: "v1.2.3",
currentAPIMajorVersion: provisionersdk.CurrentMajor,
expectedSeverity: health.SeverityError,
expectedError: "No active provisioner daemons found!",
},
{
name: "error fetching daemons",
currentVersion: "v1.2.3",
currentAPIMajorVersion: provisionersdk.CurrentMajor,
provisionerDaemonsErr: assert.AnError,
expectedSeverity: health.SeverityError,
expectedError: assert.AnError.Error(),
},
{
name: "one daemon up to date",
currentVersion: "v1.2.3",
currentAPIMajorVersion: provisionersdk.CurrentMajor,
expectedSeverity: health.SeverityOK,
provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "1.0")},
},
{
name: "one daemon out of date",
currentVersion: "v1.2.3",
currentAPIMajorVersion: provisionersdk.CurrentMajor,
expectedSeverity: health.SeverityWarning,
expectedWarningCode: health.CodeProvisionerDaemonVersionMismatch,
provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-old", "v1.1.2", "1.0")},
},
{
name: "invalid daemon version",
currentVersion: "v1.2.3",
currentAPIMajorVersion: provisionersdk.CurrentMajor,
expectedSeverity: health.SeverityError,
expectedWarningCode: health.CodeUnknown,
provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-invalid-version", "invalid", "1.0")},
},
{
name: "invalid daemon api version",
currentVersion: "v1.2.3",
currentAPIMajorVersion: provisionersdk.CurrentMajor,
expectedSeverity: health.SeverityError,
expectedWarningCode: health.CodeUnknown,
provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-new-minor", "v1.2.3", "invalid")},
},
{
name: "api version backward compat",
currentVersion: "v2.3.4",
currentAPIMajorVersion: 2,
expectedSeverity: health.SeverityWarning,
expectedWarningCode: health.CodeProvisionerDaemonAPIMajorVersionDeprecated,
provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-old-api", "v2.3.4", "1.0")},
},
{
name: "one up to date, one out of date",
currentVersion: "v1.2.3",
currentAPIMajorVersion: provisionersdk.CurrentMajor,
expectedSeverity: health.SeverityWarning,
expectedWarningCode: health.CodeProvisionerDaemonVersionMismatch,
provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "1.0"), fakeProvisionerDaemon(t, "pd-old", "v1.1.2", "1.0")},
},
{
name: "one up to date, one newer",
currentVersion: "v1.2.3",
currentAPIMajorVersion: provisionersdk.CurrentMajor,
expectedSeverity: health.SeverityWarning,
expectedWarningCode: health.CodeProvisionerDaemonVersionMismatch,
provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "1.0"), fakeProvisionerDaemon(t, "pd-new", "v2.3.4", "1.0")},
},
{
name: "one up to date, one stale older",
currentVersion: "v2.3.4",
currentAPIMajorVersion: provisionersdk.CurrentMajor,
expectedSeverity: health.SeverityOK,
provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemonStale(t, "pd-ok", "v1.2.3", "0.9", dbtime.Now().Add(-5*time.Minute)), fakeProvisionerDaemon(t, "pd-new", "v2.3.4", "1.0")},
},
{
name: "one stale",
currentVersion: "v2.3.4",
currentAPIMajorVersion: provisionersdk.CurrentMajor,
expectedSeverity: health.SeverityError,
expectedError: "No active provisioner daemons found!",
provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemonStale(t, "pd-ok", "v1.2.3", "0.9", dbtime.Now().Add(-5*time.Minute))},
},
} {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var rpt healthcheck.ProvisionerDaemonsReport
var deps healthcheck.ProvisionerDaemonsReportDeps
deps.CurrentVersion = tt.currentVersion
deps.CurrentAPIMajorVersion = tt.currentAPIMajorVersion
if tt.currentAPIMajorVersion == 0 {
deps.CurrentAPIMajorVersion = provisionersdk.CurrentMajor
}
now := dbtime.Now()
deps.TimeNow = func() time.Time {
return now
}
ctrl := gomock.NewController(t)
mDB := dbmock.NewMockStore(ctrl)
mDB.EXPECT().GetProvisionerDaemons(gomock.Any()).AnyTimes().Return(tt.provisionerDaemons, tt.provisionerDaemonsErr)
deps.Store = mDB
rpt.Run(context.Background(), &deps)
assert.Equal(t, tt.expectedSeverity, rpt.Severity)
if tt.expectedWarningCode != "" && assert.NotEmpty(t, rpt.Warnings) {
var found bool
for _, w := range rpt.Warnings {
if w.Code == tt.expectedWarningCode {
found = true
break
}
}
assert.True(t, found, "expected warning %s not found in %v", tt.expectedWarningCode, rpt.Warnings)
} else {
assert.Empty(t, rpt.Warnings)
}
if tt.expectedError != "" && assert.NotNil(t, rpt.Error) {
assert.Contains(t, *rpt.Error, tt.expectedError)
}
})
}
}
func fakeProvisionerDaemon(t *testing.T, name, version, apiVersion string) database.ProvisionerDaemon {
t.Helper()
return database.ProvisionerDaemon{
ID: uuid.New(),
Name: name,
CreatedAt: dbtime.Now(),
LastSeenAt: sql.NullTime{Time: dbtime.Now(), Valid: true},
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho, database.ProvisionerTypeTerraform},
ReplicaID: uuid.NullUUID{},
Tags: map[string]string{},
Version: version,
APIVersion: apiVersion,
}
}
func fakeProvisionerDaemonStale(t *testing.T, name, version, apiVersion string, lastSeenAt time.Time) database.ProvisionerDaemon {
t.Helper()
d := fakeProvisionerDaemon(t, name, version, apiVersion)
d.LastSeenAt.Valid = true
d.LastSeenAt.Time = lastSeenAt
return d
}

View File

@ -1786,7 +1786,7 @@ func setup(t *testing.T, ignoreLogErrors bool, ov *overrides) (proto.DRPCProvisi
Tags: database.StringMap{},
LastSeenAt: sql.NullTime{},
Version: buildinfo.Version(),
APIVersion: provisionersdk.APIVersionCurrent,
APIVersion: provisionersdk.VersionCurrent.String(),
})
require.NoError(t, err)

View File

@ -1,6 +1,7 @@
package apiversion
import (
"fmt"
"strconv"
"strings"
@ -41,6 +42,10 @@ func (v *APIVersion) WithBackwardCompat(majs ...int) *APIVersion {
// - 1.x is supported,
// - 2.0, 2.1, and 2.2 are supported,
// - 2.3+ is not supported.
func (v *APIVersion) String() string {
return fmt.Sprintf("%d.%d", v.supportedMajor, v.supportedMinor)
}
func (v *APIVersion) Validate(version string) error {
major, minor, err := Parse(version)
if err != nil {

View File

@ -12,11 +12,12 @@ type HealthSection string
// If you add another const below, make sure to add it to HealthSections!
const (
HealthSectionDERP HealthSection = "DERP"
HealthSectionAccessURL HealthSection = "AccessURL"
HealthSectionWebsocket HealthSection = "Websocket"
HealthSectionDatabase HealthSection = "Database"
HealthSectionWorkspaceProxy HealthSection = "WorkspaceProxy"
HealthSectionDERP HealthSection = "DERP"
HealthSectionAccessURL HealthSection = "AccessURL"
HealthSectionWebsocket HealthSection = "Websocket"
HealthSectionDatabase HealthSection = "Database"
HealthSectionWorkspaceProxy HealthSection = "WorkspaceProxy"
HealthSectionProvisionerDaemons HealthSection = "ProvisionerDaemons"
)
var HealthSections = []HealthSection{

26
docs/api/debug.md generated
View File

@ -282,6 +282,32 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \
},
"failing_sections": ["DERP"],
"healthy": true,
"provisioner_daemons": {
"dismissed": true,
"error": "string",
"provisioner_daemons": [
{
"api_version": "string",
"created_at": "2019-08-24T14:15:22Z",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"name": "string",
"provisioners": ["string"],
"tags": {
"property1": "string",
"property2": "string"
},
"version": "string"
}
],
"severity": "ok",
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
},
"severity": "ok",
"time": "string",
"websocket": {

110
docs/api/schemas.md generated
View File

@ -3220,13 +3220,14 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
#### Enumerated Values
| Value |
| ---------------- |
| `DERP` |
| `AccessURL` |
| `Websocket` |
| `Database` |
| `WorkspaceProxy` |
| Value |
| -------------------- |
| `DERP` |
| `AccessURL` |
| `Websocket` |
| `Database` |
| `WorkspaceProxy` |
| `ProvisionerDaemons` |
## codersdk.HealthSettings
@ -7771,6 +7772,9 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| `EACS04` |
| `EDERP01` |
| `EDERP02` |
| `EPD01` |
| `EPD02` |
| `EPD03` |
## health.Message
@ -7890,6 +7894,47 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| `severity` | `warning` |
| `severity` | `error` |
## healthcheck.ProvisionerDaemonsReport
```json
{
"dismissed": true,
"error": "string",
"provisioner_daemons": [
{
"api_version": "string",
"created_at": "2019-08-24T14:15:22Z",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"name": "string",
"provisioners": ["string"],
"tags": {
"property1": "string",
"property2": "string"
},
"version": "string"
}
],
"severity": "ok",
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| --------------------- | ----------------------------------------------------------------- | -------- | ------------ | ----------- |
| `dismissed` | boolean | false | | |
| `error` | string | false | | |
| `provisioner_daemons` | array of [codersdk.ProvisionerDaemon](#codersdkprovisionerdaemon) | false | | |
| `severity` | [health.Severity](#healthseverity) | false | | |
| `warnings` | array of [health.Message](#healthmessage) | false | | |
## healthcheck.Report
```json
@ -8131,6 +8176,32 @@ If the schedule is empty, the user will be updated to use the default schedule.|
},
"failing_sections": ["DERP"],
"healthy": true,
"provisioner_daemons": {
"dismissed": true,
"error": "string",
"provisioner_daemons": [
{
"api_version": "string",
"created_at": "2019-08-24T14:15:22Z",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
"name": "string",
"provisioners": ["string"],
"tags": {
"property1": "string",
"property2": "string"
},
"version": "string"
}
],
"severity": "ok",
"warnings": [
{
"code": "EUNKNOWN",
"message": "string"
}
]
},
"severity": "ok",
"time": "string",
"websocket": {
@ -8186,18 +8257,19 @@ If the schedule is empty, the user will be updated to use the default schedule.|
### Properties
| Name | Type | Required | Restrictions | Description |
| ------------------ | -------------------------------------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------- |
| `access_url` | [healthcheck.AccessURLReport](#healthcheckaccessurlreport) | false | | |
| `coder_version` | string | false | | The Coder version of the server that the report was generated on. |
| `database` | [healthcheck.DatabaseReport](#healthcheckdatabasereport) | false | | |
| `derp` | [derphealth.Report](#derphealthreport) | false | | |
| `failing_sections` | array of [codersdk.HealthSection](#codersdkhealthsection) | false | | Failing sections is a list of sections that have failed their healthcheck. |
| `healthy` | boolean | false | | Healthy is true if the report returns no errors. Deprecated: use `Severity` instead |
| `severity` | [health.Severity](#healthseverity) | false | | Severity indicates the status of Coder health. |
| `time` | string | false | | Time is the time the report was generated at. |
| `websocket` | [healthcheck.WebsocketReport](#healthcheckwebsocketreport) | false | | |
| `workspace_proxy` | [healthcheck.WorkspaceProxyReport](#healthcheckworkspaceproxyreport) | false | | |
| Name | Type | Required | Restrictions | Description |
| --------------------- | ---------------------------------------------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------- |
| `access_url` | [healthcheck.AccessURLReport](#healthcheckaccessurlreport) | false | | |
| `coder_version` | string | false | | The Coder version of the server that the report was generated on. |
| `database` | [healthcheck.DatabaseReport](#healthcheckdatabasereport) | false | | |
| `derp` | [derphealth.Report](#derphealthreport) | false | | |
| `failing_sections` | array of [codersdk.HealthSection](#codersdkhealthsection) | false | | Failing sections is a list of sections that have failed their healthcheck. |
| `healthy` | boolean | false | | Healthy is true if the report returns no errors. Deprecated: use `Severity` instead |
| `provisioner_daemons` | [healthcheck.ProvisionerDaemonsReport](#healthcheckprovisionerdaemonsreport) | false | | |
| `severity` | [health.Severity](#healthseverity) | false | | Severity indicates the status of Coder health. |
| `time` | string | false | | Time is the time the report was generated at. |
| `websocket` | [healthcheck.WebsocketReport](#healthcheckwebsocketreport) | false | | |
| `workspace_proxy` | [healthcheck.WorkspaceProxyReport](#healthcheckworkspaceproxyreport) | false | | |
#### Enumerated Values

View File

@ -51,7 +51,7 @@ func TestProvisionerDaemon_PSK(t *testing.T) {
require.Equal(t, "matt-daemon", daemons[0].Name)
require.Equal(t, provisionersdk.ScopeOrganization, daemons[0].Tags[provisionersdk.TagScope])
require.Equal(t, buildinfo.Version(), daemons[0].Version)
require.Equal(t, provisionersdk.APIVersionCurrent, daemons[0].APIVersion)
require.Equal(t, provisionersdk.VersionCurrent.String(), daemons[0].APIVersion)
}
func TestProvisionerDaemon_SessionToken(t *testing.T) {
@ -88,7 +88,7 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) {
assert.Equal(t, provisionersdk.ScopeUser, daemons[0].Tags[provisionersdk.TagScope])
assert.Equal(t, anotherUser.ID.String(), daemons[0].Tags[provisionersdk.TagOwner])
assert.Equal(t, buildinfo.Version(), daemons[0].Version)
assert.Equal(t, provisionersdk.APIVersionCurrent, daemons[0].APIVersion)
assert.Equal(t, provisionersdk.VersionCurrent.String(), daemons[0].APIVersion)
})
t.Run("ScopeAnotherUser", func(t *testing.T) {
@ -124,7 +124,7 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) {
// This should get clobbered to the user who started the daemon.
assert.Equal(t, anotherUser.ID.String(), daemons[0].Tags[provisionersdk.TagOwner])
assert.Equal(t, buildinfo.Version(), daemons[0].Version)
assert.Equal(t, provisionersdk.APIVersionCurrent, daemons[0].APIVersion)
assert.Equal(t, provisionersdk.VersionCurrent.String(), daemons[0].APIVersion)
})
t.Run("ScopeOrg", func(t *testing.T) {
@ -158,6 +158,6 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) {
assert.Equal(t, "org-daemon", daemons[0].Name)
assert.Equal(t, provisionersdk.ScopeOrganization, daemons[0].Tags[provisionersdk.TagScope])
assert.Equal(t, buildinfo.Version(), daemons[0].Version)
assert.Equal(t, provisionersdk.APIVersionCurrent, daemons[0].APIVersion)
assert.Equal(t, provisionersdk.VersionCurrent.String(), daemons[0].APIVersion)
})
}

View File

@ -26,6 +26,7 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/httpapi"
@ -89,7 +90,7 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
}
apiDaemons := make([]codersdk.ProvisionerDaemon, 0)
for _, daemon := range daemons {
apiDaemons = append(apiDaemons, convertProvisionerDaemon(daemon))
apiDaemons = append(apiDaemons, db2sdk.ProvisionerDaemon(daemon))
}
httpapi.Write(ctx, rw, http.StatusOK, apiDaemons)
}
@ -235,6 +236,11 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request)
versionHdrVal := r.Header.Get(codersdk.BuildVersionHeader)
apiVersion := "1.0"
if qv := r.URL.Query().Get("version"); qv != "" {
apiVersion = qv
}
// Create the daemon in the database.
now := dbtime.Now()
daemon, err := api.Database.UpsertProvisionerDaemon(authCtx, database.UpsertProvisionerDaemonParams{
@ -244,7 +250,7 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request)
CreatedAt: now,
LastSeenAt: sql.NullTime{Time: now, Valid: true},
Version: versionHdrVal,
APIVersion: provisionersdk.APIVersionCurrent,
APIVersion: apiVersion,
})
if err != nil {
if !xerrors.Is(err, context.Canceled) {
@ -355,22 +361,6 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request)
_ = conn.Close(websocket.StatusGoingAway, "")
}
func convertProvisionerDaemon(daemon database.ProvisionerDaemon) codersdk.ProvisionerDaemon {
result := codersdk.ProvisionerDaemon{
ID: daemon.ID,
CreatedAt: daemon.CreatedAt,
LastSeenAt: codersdk.NullTime{NullTime: daemon.LastSeenAt},
Name: daemon.Name,
Tags: daemon.Tags,
Version: daemon.Version,
APIVersion: daemon.APIVersion,
}
for _, provisionerType := range daemon.Provisioners {
result.Provisioners = append(result.Provisioners, codersdk.ProvisionerType(provisionerType))
}
return result
}
// wsNetConn wraps net.Conn created by websocket.NetConn(). Cancel func
// is called if a read or write error is encountered.
type wsNetConn struct {

View File

@ -59,7 +59,7 @@ func TestProvisionerDaemonServe(t *testing.T) {
if assert.Len(t, daemons, 1) {
assert.Equal(t, daemonName, daemons[0].Name)
assert.Equal(t, buildinfo.Version(), daemons[0].Version)
assert.Equal(t, provisionersdk.APIVersionCurrent, daemons[0].APIVersion)
assert.Equal(t, provisionersdk.VersionCurrent.String(), daemons[0].APIVersion)
}
})

View File

@ -3,7 +3,6 @@ package provisionersdk
import (
"context"
"errors"
"fmt"
"io"
"net"
"os"
@ -18,6 +17,7 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/tracing"
"github.com/coder/coder/v2/coderd/util/apiversion"
"github.com/coder/coder/v2/provisionersdk/proto"
)
@ -26,13 +26,10 @@ const (
CurrentMinor = 0
)
var (
SupportedMajors = []int{1}
// APIVersionCurrent is the current provisionerd API version.
// Breaking changes to the provisionerd API **MUST** increment
// CurrentMajor above.
APIVersionCurrent = fmt.Sprintf("%d.%d", CurrentMajor, CurrentMinor)
)
// VersionCurrent is the current provisionerd API version.
// Breaking changes to the provisionerd API **MUST** increment
// CurrentMajor above.
var VersionCurrent = apiversion.New(CurrentMajor, CurrentMinor)
// ServeOptions are configurations to serve a provisioner.
type ServeOptions struct {

View File

@ -877,6 +877,8 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) {
return TypescriptType{ValueType: "HealthSeverity"}, nil
case "github.com/coder/coder/v2/codersdk.HealthSection":
return TypescriptType{ValueType: "HealthSection"}, nil
case "github.com/coder/coder/v2/codersdk.ProvisionerDaemon":
return TypescriptType{ValueType: "ProvisionerDaemon"}, nil
}
// Some hard codes are a bit trickier.

View File

@ -1865,12 +1865,14 @@ export type HealthSection =
| "AccessURL"
| "DERP"
| "Database"
| "ProvisionerDaemons"
| "Websocket"
| "WorkspaceProxy";
export const HealthSections: HealthSection[] = [
"AccessURL",
"DERP",
"Database",
"ProvisionerDaemons",
"Websocket",
"WorkspaceProxy",
];
@ -2203,6 +2205,15 @@ export interface HealthcheckDatabaseReport {
readonly error?: string;
}
// From healthcheck/provisioner.go
export interface HealthcheckProvisionerDaemonsReport {
readonly severity: HealthSeverity;
readonly warnings: HealthMessage[];
readonly dismissed: boolean;
readonly error?: string;
readonly provisioner_daemons: ProvisionerDaemon[];
}
// From healthcheck/healthcheck.go
export interface HealthcheckReport {
readonly time: string;
@ -2214,6 +2225,7 @@ export interface HealthcheckReport {
readonly websocket: HealthcheckWebsocketReport;
readonly database: HealthcheckDatabaseReport;
readonly workspace_proxy: HealthcheckWorkspaceProxyReport;
readonly provisioner_daemons: HealthcheckProvisionerDaemonsReport;
readonly coder_version: string;
}
@ -2301,6 +2313,9 @@ export type HealthCode =
| "EDB02"
| "EDERP01"
| "EDERP02"
| "EPD01"
| "EPD02"
| "EPD03"
| "EUNKNOWN"
| "EWP01"
| "EWP02"
@ -2318,6 +2333,9 @@ export const HealthCodes: HealthCode[] = [
"EDB02",
"EDERP01",
"EDERP02",
"EPD01",
"EPD02",
"EPD03",
"EUNKNOWN",
"EWP01",
"EWP02",

View File

@ -3101,6 +3101,26 @@ export const MockHealth: TypesGen.HealthcheckReport = {
],
},
},
provisioner_daemons: {
severity: "ok",
warnings: [],
dismissed: false,
provisioner_daemons: [
{
id: "e455b582-ac04-4323-9ad6-ab71301fa006",
created_at: "2024-01-04T15:53:03.21563Z",
last_seen_at: "2024-01-04T16:05:03.967551Z",
name: "vvuurrkk-2",
version: "v2.6.0-devel+965ad5e96",
api_version: "1.0",
provisioners: ["echo", "terraform"],
tags: {
owner: "",
scope: "organization",
},
},
],
},
coder_version: "v2.5.0-devel+5fad61102",
};
@ -3189,6 +3209,13 @@ export const DeploymentHealthUnhealthy: TypesGen.HealthcheckReport = {
],
},
},
provisioner_daemons: {
severity: "error",
error: "something went wrong lol",
warnings: [],
dismissed: false,
provisioner_daemons: [],
},
};
export const MockHealthSettings: TypesGen.HealthSettings = {