coder/coderd/healthcheck/provisioner.go

145 lines
5.2 KiB
Go

package healthcheck
import (
"context"
"sort"
"time"
"golang.org/x/mod/semver"
"github.com/coder/coder/v2/apiversion"
"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/ptr"
"github.com/coder/coder/v2/codersdk/healthsdk"
"github.com/coder/coder/v2/provisionerd/proto"
)
type ProvisionerDaemonsReport healthsdk.ProvisionerDaemonsReport
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.Items = make([]healthsdk.ProvisionerDaemonsReportItem, 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
}
// Ensure stable order for display and for tests
sort.Slice(daemons, func(i, j int) bool {
return daemons[i].Name < daemons[j].Name
})
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
}
it := healthsdk.ProvisionerDaemonsReportItem{
ProvisionerDaemon: db2sdk.ProvisionerDaemon(daemon),
Warnings: make([]health.Message, 0),
}
// 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, "Some provisioner daemons report invalid version information."))
it.Warnings = append(it.Warnings, health.Messagef(health.CodeUnknown, "Invalid version %q", 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, "Some provisioner daemons report mismatched versions."))
it.Warnings = append(it.Warnings, health.Messagef(health.CodeProvisionerDaemonVersionMismatch, "Mismatched version %q", 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, "Some provisioner daemons report invalid API version information."))
it.Warnings = append(it.Warnings, health.Messagef(health.CodeUnknown, "Invalid API version: %s", err.Error())) // contains version string
} 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, "Some provisioner daemons report deprecated major API versions. Consider upgrading!"))
it.Warnings = append(it.Warnings, health.Messagef(health.CodeProvisionerDaemonAPIMajorVersionDeprecated, "Deprecated major API version %d.", proto.CurrentMajor))
}
r.Items = append(r.Items, it)
}
if len(r.Items) == 0 {
r.Severity = health.SeverityError
r.Warnings = append(r.Warnings, health.Messagef(health.CodeProvisionerDaemonsNoProvisionerDaemons, "No active provisioner daemons found!"))
return
}
}