From ffe19ea48f6c265688dcc10eb3c156cebb46e6fb Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 3 Jan 2024 17:34:55 +0000 Subject: [PATCH 01/18] WIP: add version healthcheck for provisioner daemons --- coderd/healthcheck/health/model.go | 5 + coderd/healthcheck/provisioner.go | 93 ++++++++++++++ coderd/healthcheck/provisioner_test.go | 165 +++++++++++++++++++++++++ 3 files changed, 263 insertions(+) create mode 100644 coderd/healthcheck/provisioner.go create mode 100644 coderd/healthcheck/provisioner_test.go diff --git a/coderd/healthcheck/health/model.go b/coderd/healthcheck/health/model.go index 707969e404886..9539de706a31e 100644 --- a/coderd/healthcheck/health/model.go +++ b/coderd/healthcheck/health/model.go @@ -34,6 +34,11 @@ const ( CodeDERPNodeUsesWebsocket Code = `EDERP01` CodeDERPOneNodeUnhealthy Code = `EDERP02` + + CodeProvisionerDaemonsNoProvisionerDaemons Code = `EPD01` + CodeProvisionerDaemonVersionOutOfDate Code = `EPD02` + CodeProvisionerDaemonAPIMajorVersionNotAvailable Code = `EPD03` + CodeProvisionerDaemonAPIMinorVersionNotAvailable Code = `EPD04` ) // @typescript-generate Severity diff --git a/coderd/healthcheck/provisioner.go b/coderd/healthcheck/provisioner.go new file mode 100644 index 0000000000000..b5098b66580f1 --- /dev/null +++ b/coderd/healthcheck/provisioner.go @@ -0,0 +1,93 @@ +package healthcheck + +import ( + "context" + + "golang.org/x/mod/semver" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/healthcheck/health" + "github.com/coder/coder/v2/codersdk" +) + +// @typescript-generate ProvisionerDaemonReport +type ProvisionerDaemonReport struct { + Severity health.Severity `json:"severity"` + Warnings []health.Message `json:"warnings"` + Dismissed bool `json:"dismissed"` + Error *string + + Provisioners []codersdk.ProvisionerDaemon +} + +// @typescript-generate ProvisionerDaemonReportOptions +type ProvisionerDaemonReportOptions struct { + CurrentVersion string + CurrentAPIVersion string + + // ProvisionerDaemonsFn is a function that returns ProvisionerDaemons. + // Satisfied by database.Store.ProvisionerDaemons + ProvisionerDaemonsFn func(context.Context) ([]database.ProvisionerDaemon, error) + + Dismissed bool +} + +func (r *ProvisionerDaemonReport) Run(ctx context.Context, opts *ProvisionerDaemonReportOptions) { + r.Severity = health.SeverityOK + r.Warnings = make([]health.Message, 0) + r.Dismissed = opts.Dismissed + + if opts.CurrentVersion == "" { + r.Severity = health.SeverityError + r.Warnings = append(r.Warnings, health.Messagef(health.CodeUnknown, "Developer error: CurrentVersion is empty!")) + return + } + + if opts.CurrentAPIVersion == "" { + r.Severity = health.SeverityError + r.Warnings = append(r.Warnings, health.Messagef(health.CodeUnknown, "Developer error: CurrentAPIVersion is empty!")) + return + } + + if opts.ProvisionerDaemonsFn == nil { + r.Severity = health.SeverityError + r.Warnings = append(r.Warnings, health.Messagef(health.CodeUnknown, "Developer error: ProvisionerDaemonsFn is nil!")) + return + } + + daemons, err := opts.ProvisionerDaemonsFn(ctx) + if err != nil { + r.Severity = health.SeverityError + r.Warnings = append(r.Warnings, health.Messagef(health.CodeUnknown, "Unable to fetch provisioner daemons: %s", err.Error())) + return + } + + if len(daemons) == 0 { + r.Severity = health.SeverityError + r.Warnings = append(r.Warnings, health.Messagef(health.CodeProvisionerDaemonsNoProvisionerDaemons, "No provisioner daemons found!")) + } + + for _, daemon := range daemons { + // For release versions, just check MAJOR.MINOR and ignore patch. + if !semver.IsValid(daemon.Version) { + r.Severity = health.SeverityWarning + r.Warnings = append(r.Warnings, health.Messagef(health.CodeUnknown, "Provisioner daemon %q reports invalid version %q", opts.CurrentVersion, daemon.Version)) + } else if semver.Compare(semver.MajorMinor(opts.CurrentVersion), semver.MajorMinor(daemon.Version)) > 1 { + r.Severity = health.SeverityWarning + r.Warnings = append(r.Warnings, health.Messagef(health.CodeUnknown, "Provisioner daemon %q has outdated version %q", daemon.Name, daemon.Version)) + } + + // Provisioner daemon API version follows different rules. + // 1) Coderd must support the requested API major version. + // 2) The requested API minor version must be less than or equal to that of Coderd. + ourMaj := semver.Major(opts.CurrentVersion) + theirMaj := semver.Major(daemon.APIVersion) + if semver.Compare(ourMaj, theirMaj) != 0 { + r.Severity = health.SeverityError + r.Warnings = append(r.Warnings, health.Messagef("Provisioner daemon %q requested major API version %s but only %s is available", daemon.Name, theirMaj, ourMaj)) + } else if semver.Compare(semver.MajorMinor(opts.CurrentAPIVersion), semver.MajorMinor(daemon.APIVersion)) > 1 { + r.Severity = health.SeverityWarning + r.Warnings = append(r.Warnings, health.Messagef(health.CodeUnknown, "Provisioner daemon %q requested API version %q but only %q is available", daemon.Name, daemon.Version, opts.CurrentAPIVersion)) + } + } +} diff --git a/coderd/healthcheck/provisioner_test.go b/coderd/healthcheck/provisioner_test.go new file mode 100644 index 0000000000000..30a738c63cc74 --- /dev/null +++ b/coderd/healthcheck/provisioner_test.go @@ -0,0 +1,165 @@ +package healthcheck_test + +import ( + "context" + "database/sql" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/healthcheck" + "github.com/coder/coder/v2/coderd/healthcheck/health" +) + +func TestProvisionerDaemonReport(t *testing.T) { + t.Parallel() + + var () + + for _, tt := range []struct { + name string + currentVersion string + currentAPIVersion string + provisionerDaemonsFn func(context.Context) ([]database.ProvisionerDaemon, error) + expectedSeverity health.Severity + expectedWarningCode health.Code + }{ + { + name: "current version empty", + currentVersion: "", + expectedSeverity: health.SeverityError, + expectedWarningCode: health.CodeUnknown, + }, + { + name: "current api version empty", + currentVersion: "v1.2.3", + currentAPIVersion: "", + expectedSeverity: health.SeverityError, + expectedWarningCode: health.CodeUnknown, + }, + { + name: "provisionerdaemonsfn nil", + currentVersion: "v1.2.3", + currentAPIVersion: "v1.0", + expectedSeverity: health.SeverityError, + expectedWarningCode: health.CodeUnknown, + }, + { + name: "no daemons", + currentVersion: "v1.2.3", + currentAPIVersion: "v1.0", + expectedSeverity: health.SeverityError, + expectedWarningCode: health.CodeProvisionerDaemonsNoProvisionerDaemons, + provisionerDaemonsFn: fakeProvisionerDaemonsFn(), + }, + { + name: "one daemon up to date", + currentVersion: "v1.2.3", + currentAPIVersion: "v1.0", + expectedSeverity: health.SeverityOK, + provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "v1.0")), + }, + { + name: "one daemon out of date", + currentVersion: "v1.2.3", + currentAPIVersion: "v1.0", + expectedSeverity: health.SeverityWarning, + expectedWarningCode: health.CodeProvisionerDaemonVersionOutOfDate, + provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-old", "v1.1.2", "v1.0")), + }, + { + name: "major api version not available", + currentVersion: "v1.2.3", + currentAPIVersion: "v1.0", + expectedSeverity: health.SeverityError, + expectedWarningCode: health.CodeProvisionerDaemonAPIMajorVersionNotAvailable, + provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-new-major", "v1.2.3", "v2.0")), + }, + { + name: "minor api version not available", + currentVersion: "v1.2.3", + currentAPIVersion: "v1.0", + expectedSeverity: health.SeverityWarning, + expectedWarningCode: health.CodeProvisionerDaemonAPIMinorVersionNotAvailable, + provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-new-minor", "v1.2.3", "v1.1")), + }, + { + name: "one up to date, one out of date", + currentVersion: "v1.2.3", + currentAPIVersion: "v1.0", + expectedSeverity: health.SeverityWarning, + expectedWarningCode: health.CodeProvisionerDaemonVersionOutOfDate, + provisionerDaemonsFn: fakeProvisionerDaemonsFn( + fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "v1.0"), + fakeProvisionerDaemon(t, "pd-old", "v1.1.2", "v1.0")), + }, + { + name: "one up to date, one newer", + currentVersion: "v1.2.3", + currentAPIVersion: "v1.0", + expectedSeverity: health.SeverityOK, + provisionerDaemonsFn: fakeProvisionerDaemonsFn( + fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "v1.0"), + fakeProvisionerDaemon(t, "pd-new", "v2.3.4", "v1.0")), + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var rpt healthcheck.ProvisionerDaemonReport + var opts healthcheck.ProvisionerDaemonReportOptions + opts.CurrentVersion = tt.currentVersion + opts.CurrentAPIVersion = tt.currentAPIVersion + if tt.provisionerDaemonsFn != nil { + opts.ProvisionerDaemonsFn = tt.provisionerDaemonsFn + } + + rpt.Run(context.Background(), &opts) + + 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) + } + }) + } +} + +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 fakeProvisionerDaemonsFn(pds ...database.ProvisionerDaemon) func(context.Context) ([]database.ProvisionerDaemon, error) { + return func(context.Context) ([]database.ProvisionerDaemon, error) { + return pds, nil + } +} + +func fakeProvisionerDaemonsFnErr(err error) func(context.Context) ([]database.ProvisionerDaemon, error) { + return func(context.Context) ([]database.ProvisionerDaemon, error) { + return nil, err + } +} From 764e5aea1d9a74473d92bd87e27548bb44bb6c8a Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 4 Jan 2024 15:10:49 +0000 Subject: [PATCH 02/18] fix unit tests --- coderd/coderd.go | 2 +- coderd/database/dbpurge/dbpurge_test.go | 8 ++++---- .../provisionerdserver_test.go | 2 +- coderd/util/apiversion/apiversion.go | 5 +++++ enterprise/cli/provisionerdaemons_test.go | 8 ++++---- enterprise/coderd/provisionerdaemons.go | 7 ++++++- enterprise/coderd/provisionerdaemons_test.go | 2 +- helm/provisioner/charts/libcoder-0.1.0.tgz | Bin 3013 -> 3013 bytes provisionersdk/serve.go | 7 +++---- 9 files changed, 25 insertions(+), 16 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 7de4e3207135f..ea6eef6044db0 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1188,7 +1188,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) diff --git a/coderd/database/dbpurge/dbpurge_test.go b/coderd/database/dbpurge/dbpurge_test.go index 8fe9953d6aaab..c244bca5d4683 100644 --- a/coderd/database/dbpurge/dbpurge_test.go +++ b/coderd/database/dbpurge/dbpurge_test.go @@ -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) diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index d89ade60b6d87..915b50a31dc02 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -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) diff --git a/coderd/util/apiversion/apiversion.go b/coderd/util/apiversion/apiversion.go index f9a1d0d539b88..7decaeab325c7 100644 --- a/coderd/util/apiversion/apiversion.go +++ b/coderd/util/apiversion/apiversion.go @@ -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 { diff --git a/enterprise/cli/provisionerdaemons_test.go b/enterprise/cli/provisionerdaemons_test.go index 3c0d377214f9f..3651971e8f9c5 100644 --- a/enterprise/cli/provisionerdaemons_test.go +++ b/enterprise/cli/provisionerdaemons_test.go @@ -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) }) } diff --git a/enterprise/coderd/provisionerdaemons.go b/enterprise/coderd/provisionerdaemons.go index ffd3af57acd3e..c06d64925386f 100644 --- a/enterprise/coderd/provisionerdaemons.go +++ b/enterprise/coderd/provisionerdaemons.go @@ -235,6 +235,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 +249,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) { diff --git a/enterprise/coderd/provisionerdaemons_test.go b/enterprise/coderd/provisionerdaemons_test.go index 1cce042447147..ac48e21cdd14f 100644 --- a/enterprise/coderd/provisionerdaemons_test.go +++ b/enterprise/coderd/provisionerdaemons_test.go @@ -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) } }) diff --git a/helm/provisioner/charts/libcoder-0.1.0.tgz b/helm/provisioner/charts/libcoder-0.1.0.tgz index d04a06b78e2c55e69b7b6dc3e42ac1cc6e035de7..ce216fcde677866c1d4d1379c9fe2762356c2d05 100644 GIT binary patch delta 2916 zcmV-q3!C)C7sVHlOn-T`g@Famn`FDiW}CuEcTu!^1uczjUMNu|DJQ}*Xun!JT!lMyx0bFlneC_1EX;~fP`prcA`rRrwM0#LQy_}mq{3S0GVLrbOmNNqD~wY{6w2-gtJM2T1$V^%(f+|fBmR%}o*q8L z|F?Km$Doze$!4!W>Vz!zLCXxL~l7uUwbDcD-=ZLi#DnM0&i-jjs47D%Qa%FZc~VW_yUT8N1Xr)L$lv??aYIcrhSQ%g3FXGgYT82zx?3>z!|d8yYmCP;a7 z*091FOMkS`EbO75s!hB$TFto!t@e2iImz5g<{ib1hBydAZID;F%hqYufs6ygC-|6g zg(aH;bs|x(Iiumc4+C~h=F(fMCmNFp25H9F1%?7uMPKIA>+(x~y*EFv`+H4)3Pns4 zqmLJYg!mc-<{i7qkB1XXkG zYIvhom$?RDlg_eotwa5$bENUAO+2Y)%&&2#Wv|%OpImk)F1xobc#5^oMXeOHuXjGP zcx<%hXi!Zp76U#u?`D4|@OHb6gi!6{D#?OmOhI7x5%o&C zNMRcCe1<~MNasE!vLuA32j-Q=7uk4BZ@};P)g)xob$8c07D$xi#d4=vbsITAB!6TQ zBN)v~j%@;5$+v+i8BYaG)EN99WakH+6|4N)cUJ}Alj-Y7h~WRf5B>-Lvt=n(nl=Y} z-v8+bPR`-g`6UcaPGIorE&O&qJcWzXv(w|t^WjARFE8Qc1q|S);pyPlm#<#H@h^i{ zFHXTAuadIaA=-+T23Oe#MS==tK!17njvgIak!T_nNkUibhRBdev>IzQAdh37yHw4G+Ly{Hwlgi@*c z6(ucGiAh2x7*P5lgc#PbnK$DL3F0)(zDNbwj~gZV=+dPi2?rwS?nS@ zq5?z3<}emKh8I)?{_={Ym3e_FhsC15;5Ihd@Z6u>EtH| z=O?Gb50_^bA5LElemXn#!1`I=!L|SnPELlW7Z(-CUSR(9pC29`w0~h7pAIiS{QR;; z{q`0FNhS!7X>{`h9+yUe{*QMXO{bx;!G*=*$?dJvX%|)4m6CNs6iT`?mD}4I`Ng8E z35G(IwrJz}*VDIuIi|00US+!COe_2dM>5e+ z&gh?G?n;blTAd|(`erj*57^$b{AlV}6}VOk+cHV%mlkW8J>y!AL7Vjo?n2VhUn?Bi zwtG_+mzwk%k6ObM1=|dh21{;7Z3lWeXlpO;z*Y;t>=_r6FpVLRUzbvY#2v0nFw9^t|^N`A|jY_sf-oPVS`>d z7oViT+j(k2m_j= znR435*YcJTopX0JZo(B1U^p@0Rvs-z0DmAPh!H3b0)JyZ!wS0TQmznFJ!WYYl5Xv8 z1!l;O19V{P{rlZ5vv9poL?~(Xv$eZb0eSb&;7XK*C=ZH{re!;-SL-Bw2TFV>x%n;zx()hZ{fFl^3S?SqMSs7Led z-@tn#$prf?8!N()xO9L%Y(qpt1->Cz`b0|9G*CQHQ?x@G-y9+N@YuA_D-6=4-)NE5 zJOag+le-Bwe}iYu{r|&<`=8(FX+7vO4p1C~wq~@(f#fw8SEkc_jUu+aVy9C(LNzo= zrlwrbf14~GTs^nDrupIOkTd+!Bh|&CQ}ED9H4~o5m_1|f07>Z!U5@o4%lGZ2i1HX7 z-uWqyq#K{17`fBa4ut0P!TpGkVdF>08N*OZ{r~cR0&eC<@A77%e~1L@%-L;U0p_M*qT4xp-;4>|kz2eGzfN84hu*Y3HV2BccaT z$p>GXe+B-gb^E@aw)$^wZ+yG@-+T6Kzj^-OJL)~u|8Mc=nCR-Jx#jMT?yecwS9Q(d z?clewqEV2h8|RtQ&A#ru1j>vl-FBMZOpz-C14IZix4o%AbRAxv!Sj~QEw-zpZl2nW z1nzDTNZ5{0D%TF~+=VtG5@Fj`mYwDHqxITSR=)Citl8`T2xBUws_fxBE_u;k1&>XV zFSd`ib*8_80QPnajh*T_qiLLWDs!al4l4V1Q*mWx+ZwFGpC6!Hi~_h?Fl3g#qckM8^F^?JQ$2M6YFuh(n-?LB>d_-OCo z@SxXwet7V#_o%nG|NPmrN6@=>PFkK!Dk2{BzIv{vaQ`I_Nq_0@C?w@cA7=X=Nz>wI zFW3uuUW76fG*xEkNV^I!Iub<8A=ED8O?ixcm@1XZez!ZJYMPCLkjGu?SLS~&CNxom zCMaaz>j1`tvOdTxO}SA2Ffba&14xJ_XD7PEaGG$&CluuqI8DO91IPp`rzPR z{C|t*_O=5Nj%kA6=e`g`m@+<(F;Rg^nGc=C!mB067EJNCBxbA0U=rD%BuTg;I@d|VdX89|p#oGTxL9~H#ZU_)Fl96(mPQ@f zeLCVy&}yF-kdw@C`!HbFWG=n6dZICzV31~vU0^6sRrF;(y)M7>*L(Aey1&=-r%=Q+ zG5UBZNQkddP##@i$dgF+;aP7zlVm)m303ooL4VxHSMaoAF2xxNXStI9a`66VOn8JB zE|^N->S#IVJx!@~cf|A6P{ri6@h_h?ENQ^IKUe3F8E09HZOJ1i*>Ue{>VsbZ71Rr; zOi>oZC?@E`Hc3-WGFV+@b_!DY-0hQ>@}(G-^~shSyXZ5~(^h-T2P<(AzD20zp}Q3S zWq+Jn5RDb`I3`J?&9pKXIhcxi_cf|O$ukk6T#H;lHPIOzV>k~Pu0=1Pn&=Y4Oi(q~ zu7)>ib(w1bHt8%Y*E-a1I!79>+QgG;#{3#*TK0-f{mEr_;<9_|f~Q#PT+~WI`+Day zi^oQ5js}$#OAz>~Mb(|gs%z?Ntkt6PWq)&h@1^no#M9RQO_8N2q&CaF%(1@z+uJ|v z^_uy6*0JM*@j*yjbout8OC)h<}7k zVg#dk$+1m0zMWTsRBne%y8zMs@(Q2&KfINl$`zj}=OeP{1({uTa?L5#9_>S83ZVgFB^wI;OVqV}I+ySCI+$ z{*L{=B$aQrQj78pV?r7DZ+QlBCY5Qn>DWbQgXm1prDvb>Y)c>*$6|~o69fJWve-p* zLSp;9@AyH}K#?79YNxf7TXm&4T1srsC!fKnyB-Xx< z36t39Bdd%x5K8doQgNk1W>oM*w|Fokf@<02diBiwO4#8?*lxsTRfoY2_*z7R(#cN_ z&W}%qA1==>KAgN7{B(BWf%UV#gKYsE93Kx)E-osNy}a@{2`R z6AXnaZPCW{uP1N+a!g<0yvlUNnO68!9;+k_QA&6-JX_^Y4su`s-we;b$g@mAQYw{R zEgvr^j%1>t zoY6nW+?5#9v^q=n^v!0r9j9<_!i3bq+04VK)F+79$`(AHkwfvpyP*%>6vNq@Y0Nn32?l|G!!4CgGBARDW(1GZlxd1&$@MCVHxft3t>T*)Wc1G7-A+T~ii?L_{#0`l&k!Jh`mb?#I`54eDs3%i>@LWM#;fvt|^ zp^Ch{<<`-LK14hwG=Y$$Dia73!Lzi42F*`kF)pW=L(3`8SOg;ki7638VPyFvuoZ0C ze#dHHWPdSg6ayz93{k3l+s48L6-IY*Mj!8K2{q8xR)9AoP8kLvkF8X>Zx~Y#t)&Ny zf&c#fu2IzX`}aYxyW=mNXc-Fz*A@2xfA{h2-okJ9;x=prD>$fbCy^-x}6S$~}cP|)%GxR}Oeg6^(&ygc=C?Xc~w zcH|M7J*dh=cQk2G?UY*qsI18UM3ZmjL7J8sNfb2qo0c>_#3Vu?fjE)Wl&JiuP><%@ zzk&Bgk_q-(Hdcfoap?ek*oKIP3VcJb^of+HX`pzZrf7#WzBxkj;jw9;>3le-Bwf2Yrz`~L?I_dmbU(|XWn9H2M|ZOv$n1IcSHu1u%<8bxe-#ZISoglcG# zOij6<|2A1XxO!oCP4mOmA!qodN2-fOr{JNJY9>69F?+_|0g}>}x*Y38mhan35#=#F zyz^5YNjE-2F>mK!}yv2Te5`Xb_jGaTYr)6Px5Mnn&w zk`KN%e+&Fg>-K#;ZS~*W-uQO)zxVw4e)IglchGyN|KH-#G11jcbIaWw-CZ-Vuj-n^ z+re*VMWY~1H_kJqn|EIZ5XN9(nvR($32ShLsv5yn(VRoTONT=Js73Lcvz zUu++3>r8(G0qpG<8avf Date: Thu, 4 Jan 2024 15:12:11 +0000 Subject: [PATCH 03/18] implement provisioner daemon healthcheck --- coderd/healthcheck/health/model.go | 7 +- coderd/healthcheck/provisioner.go | 81 ++++++++++----- coderd/healthcheck/provisioner_test.go | 130 ++++++++++++++++--------- 3 files changed, 142 insertions(+), 76 deletions(-) diff --git a/coderd/healthcheck/health/model.go b/coderd/healthcheck/health/model.go index 9539de706a31e..da6e1d7b36522 100644 --- a/coderd/healthcheck/health/model.go +++ b/coderd/healthcheck/health/model.go @@ -35,10 +35,9 @@ const ( CodeDERPNodeUsesWebsocket Code = `EDERP01` CodeDERPOneNodeUnhealthy Code = `EDERP02` - CodeProvisionerDaemonsNoProvisionerDaemons Code = `EPD01` - CodeProvisionerDaemonVersionOutOfDate Code = `EPD02` - CodeProvisionerDaemonAPIMajorVersionNotAvailable Code = `EPD03` - CodeProvisionerDaemonAPIMinorVersionNotAvailable Code = `EPD04` + CodeProvisionerDaemonsNoProvisionerDaemons Code = `EPD01` + CodeProvisionerDaemonVersionMismatch Code = `EPD02` + CodeProvisionerDaemonAPIVersionIncompatible Code = `EPD03` ) // @typescript-generate Severity diff --git a/coderd/healthcheck/provisioner.go b/coderd/healthcheck/provisioner.go index b5098b66580f1..f08d28305e457 100644 --- a/coderd/healthcheck/provisioner.go +++ b/coderd/healthcheck/provisioner.go @@ -2,16 +2,22 @@ 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/dbauthz" "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" ) -// @typescript-generate ProvisionerDaemonReport -type ProvisionerDaemonReport struct { +// @typescript-generate ProvisionerDaemonsReport +type ProvisionerDaemonsReport struct { Severity health.Severity `json:"severity"` Warnings []health.Message `json:"warnings"` Dismissed bool `json:"dismissed"` @@ -20,74 +26,95 @@ type ProvisionerDaemonReport struct { Provisioners []codersdk.ProvisionerDaemon } -// @typescript-generate ProvisionerDaemonReportOptions -type ProvisionerDaemonReportOptions struct { +// @typescript-generate ProvisionerDaemonsReportOptions +type ProvisionerDaemonsReportOptions struct { CurrentVersion string - CurrentAPIVersion string + CurrentAPIVersion *apiversion.APIVersion // ProvisionerDaemonsFn is a function that returns ProvisionerDaemons. // Satisfied by database.Store.ProvisionerDaemons ProvisionerDaemonsFn func(context.Context) ([]database.ProvisionerDaemon, error) + TimeNowFn func() time.Time + StaleInterval time.Duration + Dismissed bool } -func (r *ProvisionerDaemonReport) Run(ctx context.Context, opts *ProvisionerDaemonReportOptions) { +func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDaemonsReportOptions) { r.Severity = health.SeverityOK r.Warnings = make([]health.Message, 0) r.Dismissed = opts.Dismissed + now := opts.TimeNowFn() + if opts.StaleInterval == 0 { + opts.StaleInterval = provisionerdserver.DefaultHeartbeatInterval * 3 + } if opts.CurrentVersion == "" { r.Severity = health.SeverityError - r.Warnings = append(r.Warnings, health.Messagef(health.CodeUnknown, "Developer error: CurrentVersion is empty!")) + r.Error = ptr.Ref("Developer error: CurrentVersion is empty!") return } - if opts.CurrentAPIVersion == "" { + if opts.CurrentAPIVersion == nil { r.Severity = health.SeverityError - r.Warnings = append(r.Warnings, health.Messagef(health.CodeUnknown, "Developer error: CurrentAPIVersion is empty!")) + r.Error = ptr.Ref("Developer error: CurrentAPIVersion is nil!") return } if opts.ProvisionerDaemonsFn == nil { r.Severity = health.SeverityError - r.Warnings = append(r.Warnings, health.Messagef(health.CodeUnknown, "Developer error: ProvisionerDaemonsFn is nil!")) + r.Error = ptr.Ref("Developer error: ProvisionerDaemonsFn is nil!") return } - daemons, err := opts.ProvisionerDaemonsFn(ctx) + // nolint: gocritic // need an actor to fetch provisioner daemons + daemons, err := opts.ProvisionerDaemonsFn(dbauthz.AsSystemRestricted(ctx)) if err != nil { r.Severity = health.SeverityError - r.Warnings = append(r.Warnings, health.Messagef(health.CodeUnknown, "Unable to fetch provisioner daemons: %s", err.Error())) + r.Error = ptr.Ref("error fetching provisioner daemons: " + err.Error()) return } if len(daemons) == 0 { r.Severity = health.SeverityError - r.Warnings = append(r.Warnings, health.Messagef(health.CodeProvisionerDaemonsNoProvisionerDaemons, "No provisioner daemons found!")) + r.Error = ptr.Ref("No provisioner daemons found!") + 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 + } // For release versions, just check MAJOR.MINOR and ignore patch. if !semver.IsValid(daemon.Version) { - r.Severity = health.SeverityWarning + if r.Severity.Value() < health.SeverityWarning.Value() { + r.Severity = health.SeverityWarning + } r.Warnings = append(r.Warnings, health.Messagef(health.CodeUnknown, "Provisioner daemon %q reports invalid version %q", opts.CurrentVersion, daemon.Version)) - } else if semver.Compare(semver.MajorMinor(opts.CurrentVersion), semver.MajorMinor(daemon.Version)) > 1 { - r.Severity = health.SeverityWarning - r.Warnings = append(r.Warnings, health.Messagef(health.CodeUnknown, "Provisioner daemon %q has outdated version %q", daemon.Name, 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. - // 1) Coderd must support the requested API major version. - // 2) The requested API minor version must be less than or equal to that of Coderd. - ourMaj := semver.Major(opts.CurrentVersion) - theirMaj := semver.Major(daemon.APIVersion) - if semver.Compare(ourMaj, theirMaj) != 0 { - r.Severity = health.SeverityError - r.Warnings = append(r.Warnings, health.Messagef("Provisioner daemon %q requested major API version %s but only %s is available", daemon.Name, theirMaj, ourMaj)) - } else if semver.Compare(semver.MajorMinor(opts.CurrentAPIVersion), semver.MajorMinor(daemon.APIVersion)) > 1 { - r.Severity = health.SeverityWarning - r.Warnings = append(r.Warnings, health.Messagef(health.CodeUnknown, "Provisioner daemon %q requested API version %q but only %q is available", daemon.Name, daemon.Version, opts.CurrentAPIVersion)) + if _, _, 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 err := opts.CurrentAPIVersion.Validate(daemon.APIVersion); err != nil { + if r.Severity.Value() < health.SeverityError.Value() { + r.Severity = health.SeverityError + } + r.Warnings = append(r.Warnings, health.Messagef(health.CodeProvisionerDaemonAPIVersionIncompatible, "Provisioner daemon %q reports incompatible API version: %s", daemon.Name, err.Error())) } } } diff --git a/coderd/healthcheck/provisioner_test.go b/coderd/healthcheck/provisioner_test.go index 30a738c63cc74..c530c64d8e88b 100644 --- a/coderd/healthcheck/provisioner_test.go +++ b/coderd/healthcheck/provisioner_test.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "testing" + "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -12,111 +13,139 @@ import ( "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/coderd/util/apiversion" + "github.com/coder/coder/v2/provisionersdk" ) func TestProvisionerDaemonReport(t *testing.T) { t.Parallel() - var () - for _, tt := range []struct { name string currentVersion string - currentAPIVersion string + currentAPIVersion *apiversion.APIVersion provisionerDaemonsFn func(context.Context) ([]database.ProvisionerDaemon, error) expectedSeverity health.Severity expectedWarningCode health.Code + expectedError string }{ { - name: "current version empty", - currentVersion: "", - expectedSeverity: health.SeverityError, - expectedWarningCode: health.CodeUnknown, + name: "current version empty", + currentVersion: "", + expectedSeverity: health.SeverityError, + expectedError: "Developer error: CurrentVersion is empty", }, { - name: "current api version empty", - currentVersion: "v1.2.3", - currentAPIVersion: "", - expectedSeverity: health.SeverityError, - expectedWarningCode: health.CodeUnknown, + name: "provisionerdaemonsfn nil", + currentVersion: "v1.2.3", + currentAPIVersion: provisionersdk.VersionCurrent, + expectedSeverity: health.SeverityError, + expectedError: "Developer error: ProvisionerDaemonsFn is nil", }, { - name: "provisionerdaemonsfn nil", - currentVersion: "v1.2.3", - currentAPIVersion: "v1.0", - expectedSeverity: health.SeverityError, - expectedWarningCode: health.CodeUnknown, + name: "no daemons", + currentVersion: "v1.2.3", + currentAPIVersion: provisionersdk.VersionCurrent, + provisionerDaemonsFn: fakeProvisionerDaemonsFn(), + expectedSeverity: health.SeverityError, + expectedError: "No provisioner daemons found!", }, { - name: "no daemons", + name: "error fetching daemons", currentVersion: "v1.2.3", - currentAPIVersion: "v1.0", + currentAPIVersion: provisionersdk.VersionCurrent, + provisionerDaemonsFn: fakeProvisionerDaemonsFnErr(assert.AnError), expectedSeverity: health.SeverityError, - expectedWarningCode: health.CodeProvisionerDaemonsNoProvisionerDaemons, - provisionerDaemonsFn: fakeProvisionerDaemonsFn(), + expectedError: assert.AnError.Error(), }, { name: "one daemon up to date", currentVersion: "v1.2.3", - currentAPIVersion: "v1.0", + currentAPIVersion: provisionersdk.VersionCurrent, expectedSeverity: health.SeverityOK, - provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "v1.0")), + provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "1.0")), }, { name: "one daemon out of date", currentVersion: "v1.2.3", - currentAPIVersion: "v1.0", + currentAPIVersion: provisionersdk.VersionCurrent, expectedSeverity: health.SeverityWarning, - expectedWarningCode: health.CodeProvisionerDaemonVersionOutOfDate, - provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-old", "v1.1.2", "v1.0")), + expectedWarningCode: health.CodeProvisionerDaemonVersionMismatch, + provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-old", "v1.1.2", "1.0")), }, { name: "major api version not available", currentVersion: "v1.2.3", - currentAPIVersion: "v1.0", + currentAPIVersion: provisionersdk.VersionCurrent, expectedSeverity: health.SeverityError, - expectedWarningCode: health.CodeProvisionerDaemonAPIMajorVersionNotAvailable, - provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-new-major", "v1.2.3", "v2.0")), + expectedWarningCode: health.CodeProvisionerDaemonAPIVersionIncompatible, + provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-new-major", "v1.2.3", "2.0")), }, { name: "minor api version not available", currentVersion: "v1.2.3", - currentAPIVersion: "v1.0", - expectedSeverity: health.SeverityWarning, - expectedWarningCode: health.CodeProvisionerDaemonAPIMinorVersionNotAvailable, - provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-new-minor", "v1.2.3", "v1.1")), + currentAPIVersion: provisionersdk.VersionCurrent, + expectedSeverity: health.SeverityError, + expectedWarningCode: health.CodeProvisionerDaemonAPIVersionIncompatible, + provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-new-minor", "v1.2.3", "1.1")), + }, + { + name: "api version backward compat", + currentVersion: "v2.3.4", + currentAPIVersion: apiversion.New([]int{2, 1}, 0), + expectedSeverity: health.SeverityOK, + provisionerDaemonsFn: fakeProvisionerDaemonsFn( + fakeProvisionerDaemon(t, "pd-old-api", "v2.3.4", "1.0")), }, { name: "one up to date, one out of date", currentVersion: "v1.2.3", - currentAPIVersion: "v1.0", + currentAPIVersion: provisionersdk.VersionCurrent, expectedSeverity: health.SeverityWarning, - expectedWarningCode: health.CodeProvisionerDaemonVersionOutOfDate, + expectedWarningCode: health.CodeProvisionerDaemonVersionMismatch, provisionerDaemonsFn: fakeProvisionerDaemonsFn( - fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "v1.0"), - fakeProvisionerDaemon(t, "pd-old", "v1.1.2", "v1.0")), + 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", - currentAPIVersion: "v1.0", + name: "one up to date, one newer", + currentVersion: "v1.2.3", + currentAPIVersion: provisionersdk.VersionCurrent, + expectedSeverity: health.SeverityWarning, + expectedWarningCode: health.CodeProvisionerDaemonVersionMismatch, + provisionerDaemonsFn: fakeProvisionerDaemonsFn( + 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", + currentAPIVersion: provisionersdk.VersionCurrent, expectedSeverity: health.SeverityOK, provisionerDaemonsFn: fakeProvisionerDaemonsFn( - fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "v1.0"), - fakeProvisionerDaemon(t, "pd-new", "v2.3.4", "v1.0")), + fakeProvisionerDaemonStale(t, "pd-ok", "v1.2.3", "0.9", dbtime.Now().Add(-5*time.Minute)), + fakeProvisionerDaemon(t, "pd-new", "v2.3.4", "1.0")), }, } { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - var rpt healthcheck.ProvisionerDaemonReport - var opts healthcheck.ProvisionerDaemonReportOptions + var rpt healthcheck.ProvisionerDaemonsReport + var opts healthcheck.ProvisionerDaemonsReportOptions opts.CurrentVersion = tt.currentVersion - opts.CurrentAPIVersion = tt.currentAPIVersion + if tt.currentAPIVersion == nil { + opts.CurrentAPIVersion = provisionersdk.VersionCurrent + } else { + opts.CurrentAPIVersion = tt.currentAPIVersion + } if tt.provisionerDaemonsFn != nil { opts.ProvisionerDaemonsFn = tt.provisionerDaemonsFn } + now := dbtime.Now() + opts.TimeNowFn = func() time.Time { + return now + } rpt.Run(context.Background(), &opts) @@ -133,6 +162,9 @@ func TestProvisionerDaemonReport(t *testing.T) { } else { assert.Empty(t, rpt.Warnings) } + if tt.expectedError != "" && assert.NotNil(t, rpt.Error) { + assert.Contains(t, *rpt.Error, tt.expectedError) + } }) } } @@ -163,3 +195,11 @@ func fakeProvisionerDaemonsFnErr(err error) func(context.Context) ([]database.Pr return nil, err } } + +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 +} From 23b78696cbc28cf22f3a13f7a1ddebc0c7d14c2d Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 4 Jan 2024 15:17:16 +0000 Subject: [PATCH 04/18] wire up provisioner daemons healthcheck --- coderd/coderd.go | 7 ++ coderd/healthcheck/healthcheck.go | 56 +++++++++---- coderd/healthcheck/healthcheck_test.go | 104 +++++++++++++++++++++++-- codersdk/health.go | 11 +-- 4 files changed, 153 insertions(+), 25 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index ea6eef6044db0..210fa56537150 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -440,6 +440,13 @@ func New(options *Options) *API { CurrentVersion: buildinfo.Version(), WorkspaceProxiesFetchUpdater: *(options.WorkspaceProxiesFetchUpdater).Load(), }, + ProvisionerDaemons: healthcheck.ProvisionerDaemonsReportOptions{ + CurrentVersion: buildinfo.Version(), + CurrentAPIVersion: provisionersdk.VersionCurrent, + ProvisionerDaemonsFn: options.Database.GetProvisionerDaemons, + TimeNowFn: dbtime.Now, + StaleInterval: provisionerdserver.DefaultHeartbeatInterval * 3, + }, }) } } diff --git a/coderd/healthcheck/healthcheck.go b/coderd/healthcheck/healthcheck.go index 7c634201234bc..632a334eb7b47 100644 --- a/coderd/healthcheck/healthcheck.go +++ b/coderd/healthcheck/healthcheck.go @@ -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 *ProvisionerDaemonsReportOptions) ProvisionerDaemonsReport } // @typescript-generate Report @@ -32,22 +33,24 @@ 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_daemon"` // 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 ProvisionerDaemonsReportOptions Checker Checker } @@ -79,6 +82,11 @@ func (defaultChecker) WorkspaceProxy(ctx context.Context, opts *WorkspaceProxyRe return report } +func (defaultChecker) ProvisionerDaemons(ctx context.Context, opts *ProvisionerDaemonsReportOptions) (report ProvisionerDaemonsReport) { + report.Run(ctx, opts) + return report +} + func Run(ctx context.Context, opts *ReportOptions) *Report { var ( wg sync.WaitGroup @@ -149,26 +157,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 +213,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 } diff --git a/coderd/healthcheck/healthcheck_test.go b/coderd/healthcheck/healthcheck_test.go index e8089f36eb3ea..aacf80adcad2f 100644 --- a/coderd/healthcheck/healthcheck_test.go +++ b/coderd/healthcheck/healthcheck_test.go @@ -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.ProvisionerDaemonsReportOptions) 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 diff --git a/codersdk/health.go b/codersdk/health.go index 495ce8bb8e1a3..a53ca73192ef9 100644 --- a/codersdk/health.go +++ b/codersdk/health.go @@ -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{ From 1c449ebbb16d94e4372b12de1b765cda1b68865e Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 4 Jan 2024 15:22:56 +0000 Subject: [PATCH 05/18] do not ts generate ProvisionerDaemonsReportOptions --- coderd/healthcheck/provisioner.go | 1 - 1 file changed, 1 deletion(-) diff --git a/coderd/healthcheck/provisioner.go b/coderd/healthcheck/provisioner.go index f08d28305e457..3209af3e409c9 100644 --- a/coderd/healthcheck/provisioner.go +++ b/coderd/healthcheck/provisioner.go @@ -26,7 +26,6 @@ type ProvisionerDaemonsReport struct { Provisioners []codersdk.ProvisionerDaemon } -// @typescript-generate ProvisionerDaemonsReportOptions type ProvisionerDaemonsReportOptions struct { CurrentVersion string CurrentAPIVersion *apiversion.APIVersion From a9978e7e96507bfc7440e002520408abaeab9559 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 4 Jan 2024 16:32:38 +0000 Subject: [PATCH 06/18] typescript fixings --- coderd/healthcheck/healthcheck.go | 2 +- coderd/healthcheck/provisioner.go | 28 +++++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/coderd/healthcheck/healthcheck.go b/coderd/healthcheck/healthcheck.go index 632a334eb7b47..15d84dcf79dfd 100644 --- a/coderd/healthcheck/healthcheck.go +++ b/coderd/healthcheck/healthcheck.go @@ -38,7 +38,7 @@ type Report struct { Websocket WebsocketReport `json:"websocket"` Database DatabaseReport `json:"database"` WorkspaceProxy WorkspaceProxyReport `json:"workspace_proxy"` - ProvisionerDaemons ProvisionerDaemonsReport `json:"provisioner_daemon"` + ProvisionerDaemons ProvisionerDaemonsReport `json:"provisioner_daemons"` // The Coder version of the server that the report was generated on. CoderVersion string `json:"coder_version"` diff --git a/coderd/healthcheck/provisioner.go b/coderd/healthcheck/provisioner.go index 3209af3e409c9..2f18f5829d772 100644 --- a/coderd/healthcheck/provisioner.go +++ b/coderd/healthcheck/provisioner.go @@ -21,9 +21,9 @@ type ProvisionerDaemonsReport struct { Severity health.Severity `json:"severity"` Warnings []health.Message `json:"warnings"` Dismissed bool `json:"dismissed"` - Error *string + Error *string `json:"error"` - Provisioners []codersdk.ProvisionerDaemon + ProvisionerDaemons []codersdk.ProvisionerDaemon `json:"provisioner_daemons"` } type ProvisionerDaemonsReportOptions struct { @@ -41,6 +41,7 @@ type ProvisionerDaemonsReportOptions struct { } func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDaemonsReportOptions) { + r.ProvisionerDaemons = make([]codersdk.ProvisionerDaemon, 0) r.Severity = health.SeverityOK r.Warnings = make([]health.Message, 0) r.Dismissed = opts.Dismissed @@ -75,7 +76,11 @@ func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDae return } - if len(daemons) == 0 { + for _, daemon := range daemons { + r.ProvisionerDaemons = append(r.ProvisionerDaemons, convertProvisionerDaemon(daemon)) + } + + if len(r.ProvisionerDaemons) == 0 { r.Severity = health.SeverityError r.Error = ptr.Ref("No provisioner daemons found!") return @@ -117,3 +122,20 @@ func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDae } } } + +// XXX: duplicated from enterprise/coderd +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 +} From 6131b6b22691e8a8c55dc191711462618814c78e Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 4 Jan 2024 16:32:55 +0000 Subject: [PATCH 07/18] hack once again for apitypings --- scripts/apitypings/main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 36b2829a8dfcd..5840afd3d6ab6 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -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. From a785764e740e7cc965871f34a6acfa5fcaa48e5f Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 4 Jan 2024 16:33:15 +0000 Subject: [PATCH 08/18] make gen lint fmt --- coderd/apidoc/docs.go | 45 +++++++++++-- coderd/apidoc/swagger.json | 51 ++++++++++++-- docs/api/debug.md | 26 ++++++++ docs/api/schemas.md | 110 +++++++++++++++++++++++++------ site/src/api/typesGenerated.ts | 18 +++++ site/src/testHelpers/entities.ts | 27 ++++++++ 6 files changed, 250 insertions(+), 27 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index a1f2553e25407..f1966d29878b3 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -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", + "CodeProvisionerDaemonAPIVersionIncompatible" ] }, "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": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index d4fe6ffd558db..1e94363f4ae82 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -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", + "CodeProvisionerDaemonAPIVersionIncompatible" ] }, "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"], diff --git a/docs/api/debug.md b/docs/api/debug.md index 8ea63c39a3e91..3668a886c3a0d 100644 --- a/docs/api/debug.md +++ b/docs/api/debug.md @@ -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": { diff --git a/docs/api/schemas.md b/docs/api/schemas.md index c8ccc1fba5be7..8b653c7286d28 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -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 diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index b38c1b48298eb..37c671cbfa2ab 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -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", diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 3edf538eab4b1..5ef64845cb430 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -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 = { From e191f14228c0b1cba020081841ffc8d16ca5fc6f Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 5 Jan 2024 11:26:39 +0000 Subject: [PATCH 09/18] fix after rebase --- coderd/healthcheck/provisioner_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/healthcheck/provisioner_test.go b/coderd/healthcheck/provisioner_test.go index c530c64d8e88b..6cbf540aca412 100644 --- a/coderd/healthcheck/provisioner_test.go +++ b/coderd/healthcheck/provisioner_test.go @@ -92,7 +92,7 @@ func TestProvisionerDaemonReport(t *testing.T) { { name: "api version backward compat", currentVersion: "v2.3.4", - currentAPIVersion: apiversion.New([]int{2, 1}, 0), + currentAPIVersion: apiversion.New(2, 0).WithBackwardCompat(1), expectedSeverity: health.SeverityOK, provisionerDaemonsFn: fakeProvisionerDaemonsFn( fakeProvisionerDaemon(t, "pd-old-api", "v2.3.4", "1.0")), From a002fc1cb8c9656f74f79b8c250bc869521df051 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 5 Jan 2024 12:22:05 +0000 Subject: [PATCH 10/18] make fmt --- provisionersdk/serve.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/provisionersdk/serve.go b/provisionersdk/serve.go index cbac69b6c86a7..0b2e10234f017 100644 --- a/provisionersdk/serve.go +++ b/provisionersdk/serve.go @@ -26,12 +26,10 @@ const ( CurrentMinor = 0 ) -var ( - // VersionCurrent is the current provisionerd API version. - // Breaking changes to the provisionerd API **MUST** increment - // CurrentMajor above. - VersionCurrent = apiversion.New(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 { From fa673705ce162dd2cfcc3f7334df568c63b2644c Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 5 Jan 2024 13:54:47 +0000 Subject: [PATCH 11/18] only check provisioner major api version mismatch --- coderd/healthcheck/health/model.go | 6 +- coderd/healthcheck/provisioner.go | 28 ++--- coderd/healthcheck/provisioner_test.go | 139 ++++++++++++------------- 3 files changed, 88 insertions(+), 85 deletions(-) diff --git a/coderd/healthcheck/health/model.go b/coderd/healthcheck/health/model.go index da6e1d7b36522..9eae390aa0b08 100644 --- a/coderd/healthcheck/health/model.go +++ b/coderd/healthcheck/health/model.go @@ -35,9 +35,9 @@ const ( CodeDERPNodeUsesWebsocket Code = `EDERP01` CodeDERPOneNodeUnhealthy Code = `EDERP02` - CodeProvisionerDaemonsNoProvisionerDaemons Code = `EPD01` - CodeProvisionerDaemonVersionMismatch Code = `EPD02` - CodeProvisionerDaemonAPIVersionIncompatible Code = `EPD03` + CodeProvisionerDaemonsNoProvisionerDaemons Code = `EPD01` + CodeProvisionerDaemonVersionMismatch Code = `EPD02` + CodeProvisionerDaemonAPIMajorVersionDeprecated Code = `EPD03` ) // @typescript-generate Severity diff --git a/coderd/healthcheck/provisioner.go b/coderd/healthcheck/provisioner.go index 2f18f5829d772..1be1540b3cffc 100644 --- a/coderd/healthcheck/provisioner.go +++ b/coderd/healthcheck/provisioner.go @@ -14,6 +14,7 @@ import ( "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 @@ -27,8 +28,8 @@ type ProvisionerDaemonsReport struct { } type ProvisionerDaemonsReportOptions struct { - CurrentVersion string - CurrentAPIVersion *apiversion.APIVersion + CurrentVersion string + CurrentAPIMajorVersion int // ProvisionerDaemonsFn is a function that returns ProvisionerDaemons. // Satisfied by database.Store.ProvisionerDaemons @@ -56,9 +57,9 @@ func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDae return } - if opts.CurrentAPIVersion == nil { + if opts.CurrentAPIMajorVersion == 0 { r.Severity = health.SeverityError - r.Error = ptr.Ref("Developer error: CurrentAPIVersion is nil!") + r.Error = ptr.Ref("Developer error: CurrentAPIVersion must be non-zero!") return } @@ -97,8 +98,8 @@ func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDae } // For release versions, just check MAJOR.MINOR and ignore patch. if !semver.IsValid(daemon.Version) { - if r.Severity.Value() < health.SeverityWarning.Value() { - r.Severity = health.SeverityWarning + 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) { @@ -108,17 +109,20 @@ func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDae 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. - if _, _, err := apiversion.Parse(daemon.APIVersion); err != nil { + // 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 neccessity, 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 err := opts.CurrentAPIVersion.Validate(daemon.APIVersion); err != nil { - if r.Severity.Value() < health.SeverityError.Value() { - r.Severity = health.SeverityError + } else if maj != opts.CurrentAPIMajorVersion { + if r.Severity.Value() < health.SeverityWarning.Value() { + r.Severity = health.SeverityWarning } - r.Warnings = append(r.Warnings, health.Messagef(health.CodeProvisionerDaemonAPIVersionIncompatible, "Provisioner daemon %q reports incompatible API version: %s", daemon.Name, err.Error())) + r.Warnings = append(r.Warnings, health.Messagef(health.CodeProvisionerDaemonAPIMajorVersionDeprecated, "Provisioner daemon %q reports deprecated major API version %d. Consider upgrading!", daemon.Name, provisionersdk.CurrentMajor)) } } } diff --git a/coderd/healthcheck/provisioner_test.go b/coderd/healthcheck/provisioner_test.go index 6cbf540aca412..c1eae35cb26db 100644 --- a/coderd/healthcheck/provisioner_test.go +++ b/coderd/healthcheck/provisioner_test.go @@ -13,7 +13,6 @@ import ( "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/coderd/util/apiversion" "github.com/coder/coder/v2/provisionersdk" ) @@ -21,13 +20,13 @@ func TestProvisionerDaemonReport(t *testing.T) { t.Parallel() for _, tt := range []struct { - name string - currentVersion string - currentAPIVersion *apiversion.APIVersion - provisionerDaemonsFn func(context.Context) ([]database.ProvisionerDaemon, error) - expectedSeverity health.Severity - expectedWarningCode health.Code - expectedError string + name string + currentVersion string + currentAPIMajorVersion int + provisionerDaemonsFn func(context.Context) ([]database.ProvisionerDaemon, error) + expectedSeverity health.Severity + expectedWarningCode health.Code + expectedError string }{ { name: "current version empty", @@ -36,92 +35,93 @@ func TestProvisionerDaemonReport(t *testing.T) { expectedError: "Developer error: CurrentVersion is empty", }, { - name: "provisionerdaemonsfn nil", - currentVersion: "v1.2.3", - currentAPIVersion: provisionersdk.VersionCurrent, - expectedSeverity: health.SeverityError, - expectedError: "Developer error: ProvisionerDaemonsFn is nil", + name: "provisionerdaemonsfn nil", + currentVersion: "v1.2.3", + currentAPIMajorVersion: 1, + expectedSeverity: health.SeverityError, + expectedError: "Developer error: ProvisionerDaemonsFn is nil", }, { - name: "no daemons", - currentVersion: "v1.2.3", - currentAPIVersion: provisionersdk.VersionCurrent, - provisionerDaemonsFn: fakeProvisionerDaemonsFn(), - expectedSeverity: health.SeverityError, - expectedError: "No provisioner daemons found!", + name: "no daemons", + currentVersion: "v1.2.3", + currentAPIMajorVersion: provisionersdk.CurrentMajor, + provisionerDaemonsFn: fakeProvisionerDaemonsFn(), + expectedSeverity: health.SeverityError, + expectedError: "No provisioner daemons found!", }, { - name: "error fetching daemons", - currentVersion: "v1.2.3", - currentAPIVersion: provisionersdk.VersionCurrent, - provisionerDaemonsFn: fakeProvisionerDaemonsFnErr(assert.AnError), - expectedSeverity: health.SeverityError, - expectedError: assert.AnError.Error(), + name: "error fetching daemons", + currentVersion: "v1.2.3", + currentAPIMajorVersion: provisionersdk.CurrentMajor, + provisionerDaemonsFn: fakeProvisionerDaemonsFnErr(assert.AnError), + expectedSeverity: health.SeverityError, + expectedError: assert.AnError.Error(), }, { - name: "one daemon up to date", - currentVersion: "v1.2.3", - currentAPIVersion: provisionersdk.VersionCurrent, - expectedSeverity: health.SeverityOK, - provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "1.0")), + name: "one daemon up to date", + currentVersion: "v1.2.3", + currentAPIMajorVersion: provisionersdk.CurrentMajor, + expectedSeverity: health.SeverityOK, + provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "1.0")), }, { - name: "one daemon out of date", - currentVersion: "v1.2.3", - currentAPIVersion: provisionersdk.VersionCurrent, - expectedSeverity: health.SeverityWarning, - expectedWarningCode: health.CodeProvisionerDaemonVersionMismatch, - provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-old", "v1.1.2", "1.0")), + name: "one daemon out of date", + currentVersion: "v1.2.3", + currentAPIMajorVersion: provisionersdk.CurrentMajor, + expectedSeverity: health.SeverityWarning, + expectedWarningCode: health.CodeProvisionerDaemonVersionMismatch, + provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-old", "v1.1.2", "1.0")), }, { - name: "major api version not available", - currentVersion: "v1.2.3", - currentAPIVersion: provisionersdk.VersionCurrent, - expectedSeverity: health.SeverityError, - expectedWarningCode: health.CodeProvisionerDaemonAPIVersionIncompatible, - provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-new-major", "v1.2.3", "2.0")), + name: "invalid daemon version", + currentVersion: "v1.2.3", + currentAPIMajorVersion: provisionersdk.CurrentMajor, + expectedSeverity: health.SeverityError, + expectedWarningCode: health.CodeUnknown, + provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-invalid-version", "invalid", "1.0")), }, { - name: "minor api version not available", - currentVersion: "v1.2.3", - currentAPIVersion: provisionersdk.VersionCurrent, - expectedSeverity: health.SeverityError, - expectedWarningCode: health.CodeProvisionerDaemonAPIVersionIncompatible, - provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-new-minor", "v1.2.3", "1.1")), + name: "invalid daemon api version", + currentVersion: "v1.2.3", + currentAPIMajorVersion: provisionersdk.CurrentMajor, + expectedSeverity: health.SeverityError, + expectedWarningCode: health.CodeUnknown, + provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-new-minor", "v1.2.3", "invalid")), }, { - name: "api version backward compat", - currentVersion: "v2.3.4", - currentAPIVersion: apiversion.New(2, 0).WithBackwardCompat(1), - expectedSeverity: health.SeverityOK, + name: "api version backward compat", + currentVersion: "v2.3.4", + currentAPIMajorVersion: 2, + expectedSeverity: health.SeverityWarning, + expectedWarningCode: health.CodeProvisionerDaemonAPIMajorVersionDeprecated, provisionerDaemonsFn: fakeProvisionerDaemonsFn( fakeProvisionerDaemon(t, "pd-old-api", "v2.3.4", "1.0")), }, { - name: "one up to date, one out of date", - currentVersion: "v1.2.3", - currentAPIVersion: provisionersdk.VersionCurrent, - expectedSeverity: health.SeverityWarning, - expectedWarningCode: health.CodeProvisionerDaemonVersionMismatch, + name: "one up to date, one out of date", + currentVersion: "v1.2.3", + currentAPIMajorVersion: provisionersdk.CurrentMajor, + expectedSeverity: health.SeverityWarning, + expectedWarningCode: health.CodeProvisionerDaemonVersionMismatch, provisionerDaemonsFn: fakeProvisionerDaemonsFn( 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", - currentAPIVersion: provisionersdk.VersionCurrent, - expectedSeverity: health.SeverityWarning, - expectedWarningCode: health.CodeProvisionerDaemonVersionMismatch, + name: "one up to date, one newer", + currentVersion: "v1.2.3", + currentAPIMajorVersion: provisionersdk.CurrentMajor, + expectedSeverity: health.SeverityWarning, + expectedWarningCode: health.CodeProvisionerDaemonVersionMismatch, provisionerDaemonsFn: fakeProvisionerDaemonsFn( 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", - currentAPIVersion: provisionersdk.VersionCurrent, - expectedSeverity: health.SeverityOK, + name: "one up to date, one stale older", + currentVersion: "v2.3.4", + currentAPIMajorVersion: provisionersdk.CurrentMajor, + expectedSeverity: health.SeverityOK, provisionerDaemonsFn: fakeProvisionerDaemonsFn( fakeProvisionerDaemonStale(t, "pd-ok", "v1.2.3", "0.9", dbtime.Now().Add(-5*time.Minute)), fakeProvisionerDaemon(t, "pd-new", "v2.3.4", "1.0")), @@ -134,10 +134,9 @@ func TestProvisionerDaemonReport(t *testing.T) { var rpt healthcheck.ProvisionerDaemonsReport var opts healthcheck.ProvisionerDaemonsReportOptions opts.CurrentVersion = tt.currentVersion - if tt.currentAPIVersion == nil { - opts.CurrentAPIVersion = provisionersdk.VersionCurrent - } else { - opts.CurrentAPIVersion = tt.currentAPIVersion + opts.CurrentAPIMajorVersion = tt.currentAPIMajorVersion + if tt.currentAPIMajorVersion == 0 { + opts.CurrentAPIMajorVersion = provisionersdk.CurrentMajor } if tt.provisionerDaemonsFn != nil { opts.ProvisionerDaemonsFn = tt.provisionerDaemonsFn From eefdfc9d60b527dcba46d4131d5c515080d8b16d Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 5 Jan 2024 13:58:09 +0000 Subject: [PATCH 12/18] move convertProvisionerDaemon to db2sdk --- coderd/database/db2sdk/db2sdk.go | 16 ++++++++++++++++ coderd/healthcheck/provisioner.go | 20 ++------------------ enterprise/coderd/provisionerdaemons.go | 19 ++----------------- 3 files changed, 20 insertions(+), 35 deletions(-) diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 329f593ba9d4c..6707b72a89e4b 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -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 +} diff --git a/coderd/healthcheck/provisioner.go b/coderd/healthcheck/provisioner.go index 1be1540b3cffc..e0ee6131be78c 100644 --- a/coderd/healthcheck/provisioner.go +++ b/coderd/healthcheck/provisioner.go @@ -8,6 +8,7 @@ import ( "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/healthcheck/health" "github.com/coder/coder/v2/coderd/provisionerdserver" @@ -78,7 +79,7 @@ func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDae } for _, daemon := range daemons { - r.ProvisionerDaemons = append(r.ProvisionerDaemons, convertProvisionerDaemon(daemon)) + r.ProvisionerDaemons = append(r.ProvisionerDaemons, db2sdk.ProvisionerDaemon(daemon)) } if len(r.ProvisionerDaemons) == 0 { @@ -126,20 +127,3 @@ func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDae } } } - -// XXX: duplicated from enterprise/coderd -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 -} diff --git a/enterprise/coderd/provisionerdaemons.go b/enterprise/coderd/provisionerdaemons.go index c06d64925386f..92f034e35202c 100644 --- a/enterprise/coderd/provisionerdaemons.go +++ b/enterprise/coderd/provisionerdaemons.go @@ -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) } @@ -360,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 { From a61d34835959855fe0ae7d449a3e36b06f0d98c0 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 5 Jan 2024 14:22:30 +0000 Subject: [PATCH 13/18] use store instead of fn --- coderd/coderd.go | 10 ++--- coderd/healthcheck/provisioner.go | 16 ++++--- coderd/healthcheck/provisioner_test.go | 59 +++++++++----------------- 3 files changed, 33 insertions(+), 52 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 210fa56537150..99289445ae62b 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -441,11 +441,11 @@ func New(options *Options) *API { WorkspaceProxiesFetchUpdater: *(options.WorkspaceProxiesFetchUpdater).Load(), }, ProvisionerDaemons: healthcheck.ProvisionerDaemonsReportOptions{ - CurrentVersion: buildinfo.Version(), - CurrentAPIVersion: provisionersdk.VersionCurrent, - ProvisionerDaemonsFn: options.Database.GetProvisionerDaemons, - TimeNowFn: dbtime.Now, - StaleInterval: provisionerdserver.DefaultHeartbeatInterval * 3, + CurrentVersion: buildinfo.Version(), + CurrentAPIMajorVersion: provisionersdk.CurrentMajor, + Store: options.Database, + TimeNowFn: dbtime.Now, + StaleInterval: provisionerdserver.DefaultHeartbeatInterval * 3, }, }) } diff --git a/coderd/healthcheck/provisioner.go b/coderd/healthcheck/provisioner.go index e0ee6131be78c..c5e79d8ccb7f3 100644 --- a/coderd/healthcheck/provisioner.go +++ b/coderd/healthcheck/provisioner.go @@ -32,9 +32,7 @@ type ProvisionerDaemonsReportOptions struct { CurrentVersion string CurrentAPIMajorVersion int - // ProvisionerDaemonsFn is a function that returns ProvisionerDaemons. - // Satisfied by database.Store.ProvisionerDaemons - ProvisionerDaemonsFn func(context.Context) ([]database.ProvisionerDaemon, error) + Store ProvisionerDaemonsStore TimeNowFn func() time.Time StaleInterval time.Duration @@ -42,6 +40,10 @@ type ProvisionerDaemonsReportOptions struct { Dismissed bool } +type ProvisionerDaemonsStore interface { + GetProvisionerDaemons(ctx context.Context) ([]database.ProvisionerDaemon, error) +} + func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDaemonsReportOptions) { r.ProvisionerDaemons = make([]codersdk.ProvisionerDaemon, 0) r.Severity = health.SeverityOK @@ -60,18 +62,18 @@ func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDae if opts.CurrentAPIMajorVersion == 0 { r.Severity = health.SeverityError - r.Error = ptr.Ref("Developer error: CurrentAPIVersion must be non-zero!") + r.Error = ptr.Ref("Developer error: CurrentAPIMajorVersion must be non-zero!") return } - if opts.ProvisionerDaemonsFn == nil { + if opts.Store == nil { r.Severity = health.SeverityError - r.Error = ptr.Ref("Developer error: ProvisionerDaemonsFn is nil!") + r.Error = ptr.Ref("Developer error: Store is nil!") return } // nolint: gocritic // need an actor to fetch provisioner daemons - daemons, err := opts.ProvisionerDaemonsFn(dbauthz.AsSystemRestricted(ctx)) + 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()) diff --git a/coderd/healthcheck/provisioner_test.go b/coderd/healthcheck/provisioner_test.go index c1eae35cb26db..be17eb3a2491c 100644 --- a/coderd/healthcheck/provisioner_test.go +++ b/coderd/healthcheck/provisioner_test.go @@ -10,10 +10,13 @@ import ( "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) { @@ -23,7 +26,8 @@ func TestProvisionerDaemonReport(t *testing.T) { name string currentVersion string currentAPIMajorVersion int - provisionerDaemonsFn func(context.Context) ([]database.ProvisionerDaemon, error) + provisionerDaemons []database.ProvisionerDaemon + provisionerDaemonsErr error expectedSeverity health.Severity expectedWarningCode health.Code expectedError string @@ -34,18 +38,10 @@ func TestProvisionerDaemonReport(t *testing.T) { expectedSeverity: health.SeverityError, expectedError: "Developer error: CurrentVersion is empty", }, - { - name: "provisionerdaemonsfn nil", - currentVersion: "v1.2.3", - currentAPIMajorVersion: 1, - expectedSeverity: health.SeverityError, - expectedError: "Developer error: ProvisionerDaemonsFn is nil", - }, { name: "no daemons", currentVersion: "v1.2.3", currentAPIMajorVersion: provisionersdk.CurrentMajor, - provisionerDaemonsFn: fakeProvisionerDaemonsFn(), expectedSeverity: health.SeverityError, expectedError: "No provisioner daemons found!", }, @@ -53,7 +49,7 @@ func TestProvisionerDaemonReport(t *testing.T) { name: "error fetching daemons", currentVersion: "v1.2.3", currentAPIMajorVersion: provisionersdk.CurrentMajor, - provisionerDaemonsFn: fakeProvisionerDaemonsFnErr(assert.AnError), + provisionerDaemonsErr: assert.AnError, expectedSeverity: health.SeverityError, expectedError: assert.AnError.Error(), }, @@ -62,7 +58,7 @@ func TestProvisionerDaemonReport(t *testing.T) { currentVersion: "v1.2.3", currentAPIMajorVersion: provisionersdk.CurrentMajor, expectedSeverity: health.SeverityOK, - provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "1.0")), + provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "1.0")}, }, { name: "one daemon out of date", @@ -70,7 +66,7 @@ func TestProvisionerDaemonReport(t *testing.T) { currentAPIMajorVersion: provisionersdk.CurrentMajor, expectedSeverity: health.SeverityWarning, expectedWarningCode: health.CodeProvisionerDaemonVersionMismatch, - provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-old", "v1.1.2", "1.0")), + provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-old", "v1.1.2", "1.0")}, }, { name: "invalid daemon version", @@ -78,7 +74,7 @@ func TestProvisionerDaemonReport(t *testing.T) { currentAPIMajorVersion: provisionersdk.CurrentMajor, expectedSeverity: health.SeverityError, expectedWarningCode: health.CodeUnknown, - provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-invalid-version", "invalid", "1.0")), + provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-invalid-version", "invalid", "1.0")}, }, { name: "invalid daemon api version", @@ -86,7 +82,7 @@ func TestProvisionerDaemonReport(t *testing.T) { currentAPIMajorVersion: provisionersdk.CurrentMajor, expectedSeverity: health.SeverityError, expectedWarningCode: health.CodeUnknown, - provisionerDaemonsFn: fakeProvisionerDaemonsFn(fakeProvisionerDaemon(t, "pd-new-minor", "v1.2.3", "invalid")), + provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-new-minor", "v1.2.3", "invalid")}, }, { name: "api version backward compat", @@ -94,8 +90,7 @@ func TestProvisionerDaemonReport(t *testing.T) { currentAPIMajorVersion: 2, expectedSeverity: health.SeverityWarning, expectedWarningCode: health.CodeProvisionerDaemonAPIMajorVersionDeprecated, - provisionerDaemonsFn: fakeProvisionerDaemonsFn( - fakeProvisionerDaemon(t, "pd-old-api", "v2.3.4", "1.0")), + provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-old-api", "v2.3.4", "1.0")}, }, { name: "one up to date, one out of date", @@ -103,9 +98,7 @@ func TestProvisionerDaemonReport(t *testing.T) { currentAPIMajorVersion: provisionersdk.CurrentMajor, expectedSeverity: health.SeverityWarning, expectedWarningCode: health.CodeProvisionerDaemonVersionMismatch, - provisionerDaemonsFn: fakeProvisionerDaemonsFn( - fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "1.0"), - fakeProvisionerDaemon(t, "pd-old", "v1.1.2", "1.0")), + 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", @@ -113,18 +106,14 @@ func TestProvisionerDaemonReport(t *testing.T) { currentAPIMajorVersion: provisionersdk.CurrentMajor, expectedSeverity: health.SeverityWarning, expectedWarningCode: health.CodeProvisionerDaemonVersionMismatch, - provisionerDaemonsFn: fakeProvisionerDaemonsFn( - fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "1.0"), - fakeProvisionerDaemon(t, "pd-new", "v2.3.4", "1.0")), + 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, - provisionerDaemonsFn: fakeProvisionerDaemonsFn( - fakeProvisionerDaemonStale(t, "pd-ok", "v1.2.3", "0.9", dbtime.Now().Add(-5*time.Minute)), - fakeProvisionerDaemon(t, "pd-new", "v2.3.4", "1.0")), + 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")}, }, } { tt := tt @@ -138,14 +127,16 @@ func TestProvisionerDaemonReport(t *testing.T) { if tt.currentAPIMajorVersion == 0 { opts.CurrentAPIMajorVersion = provisionersdk.CurrentMajor } - if tt.provisionerDaemonsFn != nil { - opts.ProvisionerDaemonsFn = tt.provisionerDaemonsFn - } now := dbtime.Now() opts.TimeNowFn = func() time.Time { return now } + ctrl := gomock.NewController(t) + mDB := dbmock.NewMockStore(ctrl) + mDB.EXPECT().GetProvisionerDaemons(gomock.Any()).AnyTimes().Return(tt.provisionerDaemons, tt.provisionerDaemonsErr) + opts.Store = mDB + rpt.Run(context.Background(), &opts) assert.Equal(t, tt.expectedSeverity, rpt.Severity) @@ -183,18 +174,6 @@ func fakeProvisionerDaemon(t *testing.T, name, version, apiVersion string) datab } } -func fakeProvisionerDaemonsFn(pds ...database.ProvisionerDaemon) func(context.Context) ([]database.ProvisionerDaemon, error) { - return func(context.Context) ([]database.ProvisionerDaemon, error) { - return pds, nil - } -} - -func fakeProvisionerDaemonsFnErr(err error) func(context.Context) ([]database.ProvisionerDaemon, error) { - return func(context.Context) ([]database.ProvisionerDaemon, error) { - return nil, err - } -} - func fakeProvisionerDaemonStale(t *testing.T, name, version, apiVersion string, lastSeenAt time.Time) database.ProvisionerDaemon { t.Helper() d := fakeProvisionerDaemon(t, name, version, apiVersion) From c2361cdb1114e43b8cb2eb7a705a69eac23969ae Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 5 Jan 2024 14:27:30 +0000 Subject: [PATCH 14/18] rename ProvisionerDaemonsReportOptions -> ProvisionerDaemonsReportDeps --- coderd/coderd.go | 2 +- coderd/healthcheck/healthcheck.go | 6 +++--- coderd/healthcheck/healthcheck_test.go | 2 +- coderd/healthcheck/provisioner.go | 4 ++-- coderd/healthcheck/provisioner_test.go | 14 +++++++------- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 99289445ae62b..bfa96b35bb028 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -440,7 +440,7 @@ func New(options *Options) *API { CurrentVersion: buildinfo.Version(), WorkspaceProxiesFetchUpdater: *(options.WorkspaceProxiesFetchUpdater).Load(), }, - ProvisionerDaemons: healthcheck.ProvisionerDaemonsReportOptions{ + ProvisionerDaemons: healthcheck.ProvisionerDaemonsReportDeps{ CurrentVersion: buildinfo.Version(), CurrentAPIMajorVersion: provisionersdk.CurrentMajor, Store: options.Database, diff --git a/coderd/healthcheck/healthcheck.go b/coderd/healthcheck/healthcheck.go index 15d84dcf79dfd..5f10c182df51d 100644 --- a/coderd/healthcheck/healthcheck.go +++ b/coderd/healthcheck/healthcheck.go @@ -18,7 +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 *ProvisionerDaemonsReportOptions) ProvisionerDaemonsReport + ProvisionerDaemons(ctx context.Context, opts *ProvisionerDaemonsReportDeps) ProvisionerDaemonsReport } // @typescript-generate Report @@ -50,7 +50,7 @@ type ReportOptions struct { DerpHealth derphealth.ReportOptions Websocket WebsocketReportOptions WorkspaceProxy WorkspaceProxyReportOptions - ProvisionerDaemons ProvisionerDaemonsReportOptions + ProvisionerDaemons ProvisionerDaemonsReportDeps Checker Checker } @@ -82,7 +82,7 @@ func (defaultChecker) WorkspaceProxy(ctx context.Context, opts *WorkspaceProxyRe return report } -func (defaultChecker) ProvisionerDaemons(ctx context.Context, opts *ProvisionerDaemonsReportOptions) (report ProvisionerDaemonsReport) { +func (defaultChecker) ProvisionerDaemons(ctx context.Context, opts *ProvisionerDaemonsReportDeps) (report ProvisionerDaemonsReport) { report.Run(ctx, opts) return report } diff --git a/coderd/healthcheck/healthcheck_test.go b/coderd/healthcheck/healthcheck_test.go index aacf80adcad2f..1dc155623a2df 100644 --- a/coderd/healthcheck/healthcheck_test.go +++ b/coderd/healthcheck/healthcheck_test.go @@ -41,7 +41,7 @@ func (c *testChecker) WorkspaceProxy(context.Context, *healthcheck.WorkspaceProx return c.WorkspaceProxyReport } -func (c *testChecker) ProvisionerDaemons(context.Context, *healthcheck.ProvisionerDaemonsReportOptions) healthcheck.ProvisionerDaemonsReport { +func (c *testChecker) ProvisionerDaemons(context.Context, *healthcheck.ProvisionerDaemonsReportDeps) healthcheck.ProvisionerDaemonsReport { return c.ProvisionerDaemonsReport } diff --git a/coderd/healthcheck/provisioner.go b/coderd/healthcheck/provisioner.go index c5e79d8ccb7f3..84463a376e95f 100644 --- a/coderd/healthcheck/provisioner.go +++ b/coderd/healthcheck/provisioner.go @@ -28,7 +28,7 @@ type ProvisionerDaemonsReport struct { ProvisionerDaemons []codersdk.ProvisionerDaemon `json:"provisioner_daemons"` } -type ProvisionerDaemonsReportOptions struct { +type ProvisionerDaemonsReportDeps struct { CurrentVersion string CurrentAPIMajorVersion int @@ -44,7 +44,7 @@ type ProvisionerDaemonsStore interface { GetProvisionerDaemons(ctx context.Context) ([]database.ProvisionerDaemon, error) } -func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDaemonsReportOptions) { +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) diff --git a/coderd/healthcheck/provisioner_test.go b/coderd/healthcheck/provisioner_test.go index be17eb3a2491c..c76050ae6a881 100644 --- a/coderd/healthcheck/provisioner_test.go +++ b/coderd/healthcheck/provisioner_test.go @@ -121,23 +121,23 @@ func TestProvisionerDaemonReport(t *testing.T) { t.Parallel() var rpt healthcheck.ProvisionerDaemonsReport - var opts healthcheck.ProvisionerDaemonsReportOptions - opts.CurrentVersion = tt.currentVersion - opts.CurrentAPIMajorVersion = tt.currentAPIMajorVersion + var deps healthcheck.ProvisionerDaemonsReportDeps + deps.CurrentVersion = tt.currentVersion + deps.CurrentAPIMajorVersion = tt.currentAPIMajorVersion if tt.currentAPIMajorVersion == 0 { - opts.CurrentAPIMajorVersion = provisionersdk.CurrentMajor + deps.CurrentAPIMajorVersion = provisionersdk.CurrentMajor } now := dbtime.Now() - opts.TimeNowFn = func() time.Time { + deps.TimeNowFn = func() time.Time { return now } ctrl := gomock.NewController(t) mDB := dbmock.NewMockStore(ctrl) mDB.EXPECT().GetProvisionerDaemons(gomock.Any()).AnyTimes().Return(tt.provisionerDaemons, tt.provisionerDaemonsErr) - opts.Store = mDB + deps.Store = mDB - rpt.Run(context.Background(), &opts) + rpt.Run(context.Background(), &deps) assert.Equal(t, tt.expectedSeverity, rpt.Severity) if tt.expectedWarningCode != "" && assert.NotEmpty(t, rpt.Warnings) { From 267561577ce24977f2a97db5c1974cb2b24b344c Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 5 Jan 2024 14:56:22 +0000 Subject: [PATCH 15/18] appease linter --- coderd/apidoc/docs.go | 2 +- coderd/apidoc/swagger.json | 2 +- coderd/healthcheck/provisioner.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index f1966d29878b3..80da058186351 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -12983,7 +12983,7 @@ const docTemplate = `{ "CodeDERPOneNodeUnhealthy", "CodeProvisionerDaemonsNoProvisionerDaemons", "CodeProvisionerDaemonVersionMismatch", - "CodeProvisionerDaemonAPIVersionIncompatible" + "CodeProvisionerDaemonAPIMajorVersionDeprecated" ] }, "health.Message": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 1e94363f4ae82..d0d440c15255b 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -11823,7 +11823,7 @@ "CodeDERPOneNodeUnhealthy", "CodeProvisionerDaemonsNoProvisionerDaemons", "CodeProvisionerDaemonVersionMismatch", - "CodeProvisionerDaemonAPIVersionIncompatible" + "CodeProvisionerDaemonAPIMajorVersionDeprecated" ] }, "health.Message": { diff --git a/coderd/healthcheck/provisioner.go b/coderd/healthcheck/provisioner.go index 84463a376e95f..9a21553b6d181 100644 --- a/coderd/healthcheck/provisioner.go +++ b/coderd/healthcheck/provisioner.go @@ -115,7 +115,7 @@ func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDae // 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 neccessity, have a compatible API version. + // 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 From 1fb49ac676e10c2d7b8284301bd0d0653bc7e41e Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 5 Jan 2024 14:56:47 +0000 Subject: [PATCH 16/18] better handle stale daemons --- coderd/healthcheck/provisioner.go | 35 +++++++++++++++----------- coderd/healthcheck/provisioner_test.go | 10 +++++++- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/coderd/healthcheck/provisioner.go b/coderd/healthcheck/provisioner.go index 9a21553b6d181..75d5238515af4 100644 --- a/coderd/healthcheck/provisioner.go +++ b/coderd/healthcheck/provisioner.go @@ -10,6 +10,7 @@ import ( "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" @@ -29,13 +30,14 @@ type ProvisionerDaemonsReport struct { } type ProvisionerDaemonsReportDeps struct { + // Required CurrentVersion string CurrentAPIMajorVersion int + Store ProvisionerDaemonsStore - Store ProvisionerDaemonsStore - - TimeNowFn func() time.Time - StaleInterval time.Duration + // Optional + TimeNowFn func() time.Time // Defaults to dbtime.Now + StaleInterval time.Duration // Defaults to 3 heartbeats Dismissed bool } @@ -49,7 +51,12 @@ func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDae r.Severity = health.SeverityOK r.Warnings = make([]health.Message, 0) r.Dismissed = opts.Dismissed + + if opts.TimeNowFn == nil { + opts.TimeNowFn = dbtime.Now + } now := opts.TimeNowFn() + if opts.StaleInterval == 0 { opts.StaleInterval = provisionerdserver.DefaultHeartbeatInterval * 3 } @@ -79,17 +86,6 @@ func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDae r.Error = ptr.Ref("error fetching provisioner daemons: " + err.Error()) return } - - for _, daemon := range daemons { - r.ProvisionerDaemons = append(r.ProvisionerDaemons, db2sdk.ProvisionerDaemon(daemon)) - } - - if len(r.ProvisionerDaemons) == 0 { - r.Severity = health.SeverityError - r.Error = ptr.Ref("No provisioner daemons found!") - return - } - for _, daemon := range daemons { // Daemon never connected, skip. if !daemon.LastSeenAt.Valid { @@ -99,6 +95,9 @@ func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDae 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() { @@ -128,4 +127,10 @@ func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDae 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 + } } diff --git a/coderd/healthcheck/provisioner_test.go b/coderd/healthcheck/provisioner_test.go index c76050ae6a881..4335601699c3f 100644 --- a/coderd/healthcheck/provisioner_test.go +++ b/coderd/healthcheck/provisioner_test.go @@ -43,7 +43,7 @@ func TestProvisionerDaemonReport(t *testing.T) { currentVersion: "v1.2.3", currentAPIMajorVersion: provisionersdk.CurrentMajor, expectedSeverity: health.SeverityError, - expectedError: "No provisioner daemons found!", + expectedError: "No active provisioner daemons found!", }, { name: "error fetching daemons", @@ -115,6 +115,14 @@ func TestProvisionerDaemonReport(t *testing.T) { 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) { From 2159ac6df8dcedf0a176da614c9589c3846b7003 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 5 Jan 2024 14:57:02 +0000 Subject: [PATCH 17/18] address comment about named return values --- coderd/healthcheck/healthcheck.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/coderd/healthcheck/healthcheck.go b/coderd/healthcheck/healthcheck.go index 5f10c182df51d..1d1890ba23cbb 100644 --- a/coderd/healthcheck/healthcheck.go +++ b/coderd/healthcheck/healthcheck.go @@ -57,32 +57,38 @@ type ReportOptions struct { 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) (report ProvisionerDaemonsReport) { +func (defaultChecker) ProvisionerDaemons(ctx context.Context, opts *ProvisionerDaemonsReportDeps) ProvisionerDaemonsReport { + var report ProvisionerDaemonsReport report.Run(ctx, opts) return report } From 19e896a2b62ba4d517fb8bc7f2f0a155c88fd3b0 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 8 Jan 2024 09:09:46 +0000 Subject: [PATCH 18/18] address PR comments --- coderd/coderd.go | 3 +-- coderd/healthcheck/provisioner.go | 8 ++++---- coderd/healthcheck/provisioner_test.go | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index bfa96b35bb028..3f16c89cb0713 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -444,8 +444,7 @@ func New(options *Options) *API { CurrentVersion: buildinfo.Version(), CurrentAPIMajorVersion: provisionersdk.CurrentMajor, Store: options.Database, - TimeNowFn: dbtime.Now, - StaleInterval: provisionerdserver.DefaultHeartbeatInterval * 3, + // TimeNow and StaleInterval set to defaults, see healthcheck/provisioner.go }, }) } diff --git a/coderd/healthcheck/provisioner.go b/coderd/healthcheck/provisioner.go index 75d5238515af4..bbbd9d2bedd35 100644 --- a/coderd/healthcheck/provisioner.go +++ b/coderd/healthcheck/provisioner.go @@ -36,7 +36,7 @@ type ProvisionerDaemonsReportDeps struct { Store ProvisionerDaemonsStore // Optional - TimeNowFn func() time.Time // Defaults to dbtime.Now + TimeNow func() time.Time // Defaults to dbtime.Now StaleInterval time.Duration // Defaults to 3 heartbeats Dismissed bool @@ -52,10 +52,10 @@ func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDae r.Warnings = make([]health.Message, 0) r.Dismissed = opts.Dismissed - if opts.TimeNowFn == nil { - opts.TimeNowFn = dbtime.Now + if opts.TimeNow == nil { + opts.TimeNow = dbtime.Now } - now := opts.TimeNowFn() + now := opts.TimeNow() if opts.StaleInterval == 0 { opts.StaleInterval = provisionerdserver.DefaultHeartbeatInterval * 3 diff --git a/coderd/healthcheck/provisioner_test.go b/coderd/healthcheck/provisioner_test.go index 4335601699c3f..27c5293d70309 100644 --- a/coderd/healthcheck/provisioner_test.go +++ b/coderd/healthcheck/provisioner_test.go @@ -136,7 +136,7 @@ func TestProvisionerDaemonReport(t *testing.T) { deps.CurrentAPIMajorVersion = provisionersdk.CurrentMajor } now := dbtime.Now() - deps.TimeNowFn = func() time.Time { + deps.TimeNow = func() time.Time { return now }