From 373460a2d636c6aa3045d6f8598f12d36b05477b Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 3 Jan 2025 16:54:50 +0000 Subject: [PATCH 1/7] feat(coderd): add endpoint to list provisioner daemons Updates #15190 Updates #15084 Supercedes #15940 --- coderd/apidoc/docs.go | 106 ++++++++++++++++- coderd/apidoc/swagger.json | 98 ++++++++++++++- coderd/coderd.go | 3 + coderd/database/dbauthz/dbauthz.go | 4 + coderd/database/dbauthz/dbauthz_test.go | 12 ++ coderd/database/dbgen/dbgen.go | 45 ++++--- coderd/database/dbmem/dbmem.go | 94 +++++++++++++++ coderd/database/dbmetrics/querymetrics.go | 7 ++ coderd/database/dbmock/dbmock.go | 15 +++ coderd/database/dump.sql | 8 ++ .../000283_provisioner_daemon_status.down.sql | 1 + .../000283_provisioner_daemon_status.up.sql | 3 + coderd/database/modelmethods.go | 4 + coderd/database/models.go | 62 ++++++++++ coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 112 ++++++++++++++++++ .../database/queries/provisionerdaemons.sql | 48 ++++++++ coderd/database/sqlc.yaml | 1 + coderd/provisionerdaemons.go | 79 ++++++++++++ coderd/provisionerdaemons_test.go | 27 +++++ codersdk/organizations.go | 8 +- codersdk/provisionerdaemons.go | 43 +++++-- docs/reference/api/enterprise.md | 64 +++++++--- docs/reference/api/schemas.md | 103 ++++++++++++++++ enterprise/cli/provisionerdaemonstart_test.go | 19 ++- enterprise/coderd/coderd.go | 5 +- enterprise/coderd/provisionerdaemons.go | 45 ------- site/src/api/typesGenerated.ts | 23 ++++ 28 files changed, 930 insertions(+), 110 deletions(-) create mode 100644 coderd/database/migrations/000283_provisioner_daemon_status.down.sql create mode 100644 coderd/database/migrations/000283_provisioner_daemon_status.up.sql create mode 100644 coderd/provisionerdaemons.go create mode 100644 coderd/provisionerdaemons_test.go diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 3d85e63a6081a..e8b608e4e63f8 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -2989,7 +2989,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/codersdk.ProvisionerDaemon" + "$ref": "#/definitions/codersdk.ProvisionerDaemonWithStatus" } } } @@ -12499,6 +12499,110 @@ const docTemplate = `{ } } }, + "codersdk.ProvisionerDaemonJob": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "status": { + "enum": [ + "pending", + "running", + "succeeded", + "canceling", + "canceled", + "failed" + ], + "allOf": [ + { + "$ref": "#/definitions/codersdk.ProvisionerJobStatus" + } + ] + } + } + }, + "codersdk.ProvisionerDaemonStatus": { + "type": "string", + "enum": [ + "offline", + "idle", + "busy" + ], + "x-enum-varnames": [ + "ProvisionerDaemonOffline", + "ProvisionerDaemonIdle", + "ProvisionerDaemonBusy" + ] + }, + "codersdk.ProvisionerDaemonWithStatus": { + "type": "object", + "properties": { + "api_version": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "current_job": { + "$ref": "#/definitions/codersdk.ProvisionerDaemonJob" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "key_id": { + "type": "string", + "format": "uuid" + }, + "key_name": { + "type": "string" + }, + "last_seen_at": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string" + }, + "organization_id": { + "type": "string", + "format": "uuid" + }, + "previous_job": { + "$ref": "#/definitions/codersdk.ProvisionerDaemonJob" + }, + "provisioners": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "enum": [ + "offline", + "idle", + "busy" + ], + "allOf": [ + { + "$ref": "#/definitions/codersdk.ProvisionerDaemonStatus" + } + ] + }, + "tags": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "version": { + "type": "string" + } + } + }, "codersdk.ProvisionerJob": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 615329654edb9..dc50f059e8caa 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -2623,7 +2623,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/codersdk.ProvisionerDaemon" + "$ref": "#/definitions/codersdk.ProvisionerDaemonWithStatus" } } } @@ -11280,6 +11280,102 @@ } } }, + "codersdk.ProvisionerDaemonJob": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "status": { + "enum": [ + "pending", + "running", + "succeeded", + "canceling", + "canceled", + "failed" + ], + "allOf": [ + { + "$ref": "#/definitions/codersdk.ProvisionerJobStatus" + } + ] + } + } + }, + "codersdk.ProvisionerDaemonStatus": { + "type": "string", + "enum": ["offline", "idle", "busy"], + "x-enum-varnames": [ + "ProvisionerDaemonOffline", + "ProvisionerDaemonIdle", + "ProvisionerDaemonBusy" + ] + }, + "codersdk.ProvisionerDaemonWithStatus": { + "type": "object", + "properties": { + "api_version": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "current_job": { + "$ref": "#/definitions/codersdk.ProvisionerDaemonJob" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "key_id": { + "type": "string", + "format": "uuid" + }, + "key_name": { + "type": "string" + }, + "last_seen_at": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string" + }, + "organization_id": { + "type": "string", + "format": "uuid" + }, + "previous_job": { + "$ref": "#/definitions/codersdk.ProvisionerDaemonJob" + }, + "provisioners": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "enum": ["offline", "idle", "busy"], + "allOf": [ + { + "$ref": "#/definitions/codersdk.ProvisionerDaemonStatus" + } + ] + }, + "tags": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "version": { + "type": "string" + } + } + }, "codersdk.ProvisionerJob": { "type": "object", "properties": { diff --git a/coderd/coderd.go b/coderd/coderd.go index 7b8cde9dc6ae4..4cd44f7cc64c0 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1007,6 +1007,9 @@ func New(options *Options) *API { }) }) }) + r.Route("/provisionerdaemons", func(r chi.Router) { + r.Get("/", api.provisionerDaemons) + }) }) }) r.Route("/templates", func(r chi.Router) { diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index a4c3208aa5e6d..b2f0491949238 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1936,6 +1936,10 @@ func (q *querier) GetProvisionerDaemonsByOrganization(ctx context.Context, organ return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetProvisionerDaemonsByOrganization)(ctx, organizationID) } +func (q *querier) GetProvisionerDaemonsWithStatusByOrganization(ctx context.Context, arg database.GetProvisionerDaemonsWithStatusByOrganizationParams) ([]database.GetProvisionerDaemonsWithStatusByOrganizationRow, error) { + return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetProvisionerDaemonsWithStatusByOrganization)(ctx, arg) +} + func (q *querier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (database.ProvisionerJob, error) { job, err := q.db.GetProvisionerJobByID(ctx, id) if err != nil { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 78500792933b5..b4d8bbf75ad07 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -3189,6 +3189,18 @@ func (s *MethodTestSuite) TestExtraMethods() { s.NoError(err, "get provisioner daemon by org") check.Args(database.GetProvisionerDaemonsByOrganizationParams{OrganizationID: org.ID}).Asserts(d, policy.ActionRead).Returns(ds) })) + s.Run("GetProvisionerDaemonsWithStatusByOrganization", s.Subtest(func(db database.Store, check *expects) { + org := dbgen.Organization(s.T(), db, database.Organization{}) + d := dbgen.ProvisionerDaemon(s.T(), db, database.ProvisionerDaemon{ + OrganizationID: org.ID, + Tags: map[string]string{ + provisionersdk.TagScope: provisionersdk.ScopeOrganization, + }, + }) + ds, err := db.GetProvisionerDaemonsWithStatusByOrganization(context.Background(), database.GetProvisionerDaemonsWithStatusByOrganizationParams{OrganizationID: org.ID}) + s.NoError(err, "get provisioner daemon with status by org") + check.Args(database.GetProvisionerDaemonsWithStatusByOrganizationParams{OrganizationID: org.ID}).Asserts(d, policy.ActionRead).Returns(ds) + })) s.Run("GetEligibleProvisionerDaemonsByProvisionerJobIDs", s.Subtest(func(db database.Store, check *expects) { dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) org := dbgen.Organization(s.T(), db, database.Organization{}) diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index dd6a3a2cc1490..1208cf60d573b 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -505,9 +505,27 @@ func GroupMember(t testing.TB, db database.Store, member database.GroupMemberTab // ProvisionerDaemon creates a provisioner daemon as far as the database is concerned. It does not run a provisioner daemon. // If no key is provided, it will create one. -func ProvisionerDaemon(t testing.TB, db database.Store, daemon database.ProvisionerDaemon) database.ProvisionerDaemon { +func ProvisionerDaemon(t testing.TB, db database.Store, orig database.ProvisionerDaemon) database.ProvisionerDaemon { t.Helper() + var defOrgID uuid.UUID + if orig.OrganizationID == uuid.Nil { + defOrg, _ := db.GetDefaultOrganization(genCtx) + defOrgID = defOrg.ID + } + + daemon := database.UpsertProvisionerDaemonParams{ + Name: takeFirst(orig.Name, testutil.GetRandomName(t)), + OrganizationID: takeFirst(orig.OrganizationID, defOrgID, uuid.New()), + CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()), + Provisioners: takeFirstSlice(orig.Provisioners, []database.ProvisionerType{database.ProvisionerTypeEcho}), + Tags: takeFirstMap(orig.Tags, database.StringMap{}), + KeyID: takeFirst(orig.KeyID, uuid.Nil), + LastSeenAt: takeFirst(orig.LastSeenAt, sql.NullTime{Time: dbtime.Now(), Valid: true}), + Version: takeFirst(orig.Version, "v0.0.0"), + APIVersion: takeFirst(orig.APIVersion, "1.1"), + } + if daemon.KeyID == uuid.Nil { key, err := db.InsertProvisionerKey(genCtx, database.InsertProvisionerKeyParams{ ID: uuid.New(), @@ -521,24 +539,7 @@ func ProvisionerDaemon(t testing.TB, db database.Store, daemon database.Provisio daemon.KeyID = key.ID } - if daemon.CreatedAt.IsZero() { - daemon.CreatedAt = dbtime.Now() - } - if daemon.Name == "" { - daemon.Name = "test-daemon" - } - - d, err := db.UpsertProvisionerDaemon(genCtx, database.UpsertProvisionerDaemonParams{ - Name: daemon.Name, - OrganizationID: daemon.OrganizationID, - CreatedAt: daemon.CreatedAt, - Provisioners: daemon.Provisioners, - Tags: daemon.Tags, - KeyID: daemon.KeyID, - LastSeenAt: daemon.LastSeenAt, - Version: daemon.Version, - APIVersion: daemon.APIVersion, - }) + d, err := db.UpsertProvisionerDaemon(genCtx, daemon) require.NoError(t, err) return d } @@ -1109,6 +1110,12 @@ func takeFirstSlice[T any](values ...[]T) []T { }) } +func takeFirstMap[T, E comparable](values ...map[T]E) map[T]E { + return takeFirstF(values, func(v map[T]E) bool { + return v != nil + }) +} + // takeFirstF takes the first value that returns true func takeFirstF[Value any](values []Value, take func(v Value) bool) Value { for _, v := range values { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 9e3c3621b3420..fe15f8e505d3e 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -3756,6 +3756,100 @@ func (q *FakeQuerier) GetProvisionerDaemonsByOrganization(_ context.Context, arg return daemons, nil } +func (q *FakeQuerier) GetProvisionerDaemonsWithStatusByOrganization(_ context.Context, arg database.GetProvisionerDaemonsWithStatusByOrganizationParams) ([]database.GetProvisionerDaemonsWithStatusByOrganizationRow, error) { + err := validateDatabaseType(arg) + if err != nil { + return nil, err + } + + q.mutex.RLock() + defer q.mutex.RUnlock() + + var rows []database.GetProvisionerDaemonsWithStatusByOrganizationRow + for _, daemon := range q.provisionerDaemons { + if daemon.OrganizationID != arg.OrganizationID { + continue + } + if len(arg.IDs) > 0 && !slices.Contains(arg.IDs, daemon.ID) { + continue + } + + if len(arg.Tags) > 0 { + // Special case for untagged provisioners: only match untagged jobs. + // Ref: coderd/database/queries/provisionerjobs.sql:24-30 + // CASE WHEN nested.tags :: jsonb = '{"scope": "organization", "owner": ""}' :: jsonb + // THEN nested.tags :: jsonb = @tags :: jsonb + if tagsEqual(arg.Tags, tagsUntagged) && !tagsEqual(arg.Tags, daemon.Tags) { + continue + } + // ELSE nested.tags :: jsonb <@ @tags :: jsonb + if !tagsSubset(arg.Tags, daemon.Tags) { + continue + } + } + + var status database.ProvisionerDaemonStatus + var currentJob database.ProvisionerJob + if !daemon.LastSeenAt.Valid || daemon.LastSeenAt.Time.Before(time.Now().Add(-time.Duration(arg.StaleIntervalMS)*time.Millisecond)) { + status = database.ProvisionerDaemonStatusOffline + } else { + for _, job := range q.provisionerJobs { + if job.WorkerID.Valid && job.WorkerID.UUID == daemon.ID && !job.CompletedAt.Valid && !job.Error.Valid { + currentJob = job + break + } + } + + if currentJob.ID != uuid.Nil { + status = database.ProvisionerDaemonStatusBusy + } else { + status = database.ProvisionerDaemonStatusIdle + } + } + + var previousJob database.ProvisionerJob + for _, job := range q.provisionerJobs { + if !job.WorkerID.Valid || job.WorkerID.UUID != daemon.ID { + continue + } + + if job.StartedAt.Valid || + job.CanceledAt.Valid || + job.CompletedAt.Valid || + job.Error.Valid { + if job.CompletedAt.Time.After(previousJob.CompletedAt.Time) { + previousJob = job + } + } + } + + // Get the provisioner key name + var keyName string + for _, key := range q.provisionerKeys { + if key.ID == daemon.KeyID { + keyName = key.Name + break + } + } + + rows = append(rows, database.GetProvisionerDaemonsWithStatusByOrganizationRow{ + ProvisionerDaemon: daemon, + Status: status, + KeyName: keyName, + CurrentJobID: uuid.NullUUID{UUID: currentJob.ID, Valid: currentJob.ID != uuid.Nil}, + CurrentJobStatus: database.NullProvisionerJobStatus{ProvisionerJobStatus: currentJob.JobStatus, Valid: currentJob.ID != uuid.Nil}, + PreviousJobID: uuid.NullUUID{UUID: previousJob.ID, Valid: previousJob.ID != uuid.Nil}, + PreviousJobStatus: database.NullProvisionerJobStatus{ProvisionerJobStatus: previousJob.JobStatus, Valid: previousJob.ID != uuid.Nil}, + }) + } + + slices.SortFunc(rows, func(a, b database.GetProvisionerDaemonsWithStatusByOrganizationRow) int { + return a.ProvisionerDaemon.CreatedAt.Compare(b.ProvisionerDaemon.CreatedAt) + }) + + return rows, nil +} + func (q *FakeQuerier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (database.ProvisionerJob, error) { q.mutex.RLock() defer q.mutex.RUnlock() diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 599fad08779ac..f486224e41bb4 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -987,6 +987,13 @@ func (m queryMetricsStore) GetProvisionerDaemonsByOrganization(ctx context.Conte return r0, r1 } +func (m queryMetricsStore) GetProvisionerDaemonsWithStatusByOrganization(ctx context.Context, arg database.GetProvisionerDaemonsWithStatusByOrganizationParams) ([]database.GetProvisionerDaemonsWithStatusByOrganizationRow, error) { + start := time.Now() + r0, r1 := m.s.GetProvisionerDaemonsWithStatusByOrganization(ctx, arg) + m.queryLatencies.WithLabelValues("GetProvisionerDaemonsWithStatusByOrganization").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (database.ProvisionerJob, error) { start := time.Now() job, err := m.s.GetProvisionerJobByID(ctx, id) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 51d0c59c1d879..39e85f489ded1 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -2030,6 +2030,21 @@ func (mr *MockStoreMockRecorder) GetProvisionerDaemonsByOrganization(arg0, arg1 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvisionerDaemonsByOrganization", reflect.TypeOf((*MockStore)(nil).GetProvisionerDaemonsByOrganization), arg0, arg1) } +// GetProvisionerDaemonsWithStatusByOrganization mocks base method. +func (m *MockStore) GetProvisionerDaemonsWithStatusByOrganization(arg0 context.Context, arg1 database.GetProvisionerDaemonsWithStatusByOrganizationParams) ([]database.GetProvisionerDaemonsWithStatusByOrganizationRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProvisionerDaemonsWithStatusByOrganization", arg0, arg1) + ret0, _ := ret[0].([]database.GetProvisionerDaemonsWithStatusByOrganizationRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProvisionerDaemonsWithStatusByOrganization indicates an expected call of GetProvisionerDaemonsWithStatusByOrganization. +func (mr *MockStoreMockRecorder) GetProvisionerDaemonsWithStatusByOrganization(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvisionerDaemonsWithStatusByOrganization", reflect.TypeOf((*MockStore)(nil).GetProvisionerDaemonsWithStatusByOrganization), arg0, arg1) +} + // GetProvisionerJobByID mocks base method. func (m *MockStore) GetProvisionerJobByID(arg0 context.Context, arg1 uuid.UUID) (database.ProvisionerJob, error) { m.ctrl.T.Helper() diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 37e2cd4d764bf..c241548e166c2 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -137,6 +137,14 @@ CREATE TYPE port_share_protocol AS ENUM ( 'https' ); +CREATE TYPE provisioner_daemon_status AS ENUM ( + 'offline', + 'idle', + 'busy' +); + +COMMENT ON TYPE provisioner_daemon_status IS 'The status of a provisioner daemon.'; + CREATE TYPE provisioner_job_status AS ENUM ( 'pending', 'running', diff --git a/coderd/database/migrations/000283_provisioner_daemon_status.down.sql b/coderd/database/migrations/000283_provisioner_daemon_status.down.sql new file mode 100644 index 0000000000000..f4fd46d4a0658 --- /dev/null +++ b/coderd/database/migrations/000283_provisioner_daemon_status.down.sql @@ -0,0 +1 @@ +DROP TYPE provisioner_daemon_status; diff --git a/coderd/database/migrations/000283_provisioner_daemon_status.up.sql b/coderd/database/migrations/000283_provisioner_daemon_status.up.sql new file mode 100644 index 0000000000000..990113d4f7af0 --- /dev/null +++ b/coderd/database/migrations/000283_provisioner_daemon_status.up.sql @@ -0,0 +1,3 @@ +CREATE TYPE provisioner_daemon_status AS ENUM ('offline', 'idle', 'busy'); + +COMMENT ON TYPE provisioner_daemon_status IS 'The status of a provisioner daemon.'; diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index ca74c121bc0a6..0836a948d7ad0 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -269,6 +269,10 @@ func (p ProvisionerDaemon) RBACObject() rbac.Object { InOrg(p.OrganizationID) } +func (p GetProvisionerDaemonsWithStatusByOrganizationRow) RBACObject() rbac.Object { + return p.ProvisionerDaemon.RBACObject() +} + func (p GetEligibleProvisionerDaemonsByProvisionerJobIDsRow) RBACObject() rbac.Object { return p.ProvisionerDaemon.RBACObject() } diff --git a/coderd/database/models.go b/coderd/database/models.go index 2a20a8fa2f63e..4898b7f9e9cf6 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1209,6 +1209,68 @@ func AllPortShareProtocolValues() []PortShareProtocol { } } +// The status of a provisioner daemon. +type ProvisionerDaemonStatus string + +const ( + ProvisionerDaemonStatusOffline ProvisionerDaemonStatus = "offline" + ProvisionerDaemonStatusIdle ProvisionerDaemonStatus = "idle" + ProvisionerDaemonStatusBusy ProvisionerDaemonStatus = "busy" +) + +func (e *ProvisionerDaemonStatus) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = ProvisionerDaemonStatus(s) + case string: + *e = ProvisionerDaemonStatus(s) + default: + return fmt.Errorf("unsupported scan type for ProvisionerDaemonStatus: %T", src) + } + return nil +} + +type NullProvisionerDaemonStatus struct { + ProvisionerDaemonStatus ProvisionerDaemonStatus `json:"provisioner_daemon_status"` + Valid bool `json:"valid"` // Valid is true if ProvisionerDaemonStatus is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullProvisionerDaemonStatus) Scan(value interface{}) error { + if value == nil { + ns.ProvisionerDaemonStatus, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.ProvisionerDaemonStatus.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullProvisionerDaemonStatus) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.ProvisionerDaemonStatus), nil +} + +func (e ProvisionerDaemonStatus) Valid() bool { + switch e { + case ProvisionerDaemonStatusOffline, + ProvisionerDaemonStatusIdle, + ProvisionerDaemonStatusBusy: + return true + } + return false +} + +func AllProvisionerDaemonStatusValues() []ProvisionerDaemonStatus { + return []ProvisionerDaemonStatus{ + ProvisionerDaemonStatusOffline, + ProvisionerDaemonStatusIdle, + ProvisionerDaemonStatusBusy, + } +} + // Computed status of a provisioner job. Jobs could be stuck in a hung state, these states do not guarantee any transition to another state. type ProvisionerJobStatus string diff --git a/coderd/database/querier.go b/coderd/database/querier.go index ba151e0e8abb0..d306082127b18 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -203,6 +203,7 @@ type sqlcQuerier interface { GetPreviousTemplateVersion(ctx context.Context, arg GetPreviousTemplateVersionParams) (TemplateVersion, error) GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) GetProvisionerDaemonsByOrganization(ctx context.Context, arg GetProvisionerDaemonsByOrganizationParams) ([]ProvisionerDaemon, error) + GetProvisionerDaemonsWithStatusByOrganization(ctx context.Context, arg GetProvisionerDaemonsWithStatusByOrganizationParams) ([]GetProvisionerDaemonsWithStatusByOrganizationRow, error) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (ProvisionerJob, error) GetProvisionerJobTimingsByJobID(ctx context.Context, jobID uuid.UUID) ([]ProvisionerJobTiming, error) GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) ([]ProvisionerJob, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 455de7c93da9c..42dea8928e71e 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5572,6 +5572,118 @@ func (q *sqlQuerier) GetProvisionerDaemonsByOrganization(ctx context.Context, ar return items, nil } +const getProvisionerDaemonsWithStatusByOrganization = `-- name: GetProvisionerDaemonsWithStatusByOrganization :many +SELECT + pd.id, pd.created_at, pd.name, pd.provisioners, pd.replica_id, pd.tags, pd.last_seen_at, pd.version, pd.api_version, pd.organization_id, pd.key_id, + CASE + WHEN pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - ($1::bigint || ' ms')::interval) + THEN 'offline' + ELSE CASE + WHEN current_job.id IS NOT NULL THEN 'busy' + ELSE 'idle' + END + END::provisioner_daemon_status AS status, + pk.name AS key_name, + -- NOTE(mafredri): sqlc.embed doesn't support nullable tables nor renaming them. + current_job.id AS current_job_id, + current_job.job_status AS current_job_status, + previous_job.id AS previous_job_id, + previous_job.job_status AS previous_job_status +FROM + provisioner_daemons pd +JOIN + provisioner_keys pk ON pk.id = pd.key_id +LEFT JOIN + provisioner_jobs current_job ON ( + current_job.worker_id = pd.id + AND current_job.completed_at IS NULL + ) +LEFT JOIN + provisioner_jobs previous_job ON ( + previous_job.id = ( + SELECT + id + FROM + provisioner_jobs + WHERE + worker_id = pd.id + AND completed_at IS NOT NULL + ORDER BY + completed_at DESC + LIMIT 1 + ) + ) +WHERE + pd.organization_id = $2::uuid + AND (COALESCE(array_length($3::uuid[], 1), 0) = 0 OR pd.id = ANY($3::uuid[])) + AND ($4::tagset = 'null'::tagset OR provisioner_tagset_contains(pd.tags::tagset, $4::tagset)) +ORDER BY + pd.created_at ASC +` + +type GetProvisionerDaemonsWithStatusByOrganizationParams struct { + StaleIntervalMS int64 `db:"stale_interval_ms" json:"stale_interval_ms"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + IDs []uuid.UUID `db:"ids" json:"ids"` + Tags StringMap `db:"tags" json:"tags"` +} + +type GetProvisionerDaemonsWithStatusByOrganizationRow struct { + ProvisionerDaemon ProvisionerDaemon `db:"provisioner_daemon" json:"provisioner_daemon"` + Status ProvisionerDaemonStatus `db:"status" json:"status"` + KeyName string `db:"key_name" json:"key_name"` + CurrentJobID uuid.NullUUID `db:"current_job_id" json:"current_job_id"` + CurrentJobStatus NullProvisionerJobStatus `db:"current_job_status" json:"current_job_status"` + PreviousJobID uuid.NullUUID `db:"previous_job_id" json:"previous_job_id"` + PreviousJobStatus NullProvisionerJobStatus `db:"previous_job_status" json:"previous_job_status"` +} + +func (q *sqlQuerier) GetProvisionerDaemonsWithStatusByOrganization(ctx context.Context, arg GetProvisionerDaemonsWithStatusByOrganizationParams) ([]GetProvisionerDaemonsWithStatusByOrganizationRow, error) { + rows, err := q.db.QueryContext(ctx, getProvisionerDaemonsWithStatusByOrganization, + arg.StaleIntervalMS, + arg.OrganizationID, + pq.Array(arg.IDs), + arg.Tags, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetProvisionerDaemonsWithStatusByOrganizationRow + for rows.Next() { + var i GetProvisionerDaemonsWithStatusByOrganizationRow + if err := rows.Scan( + &i.ProvisionerDaemon.ID, + &i.ProvisionerDaemon.CreatedAt, + &i.ProvisionerDaemon.Name, + pq.Array(&i.ProvisionerDaemon.Provisioners), + &i.ProvisionerDaemon.ReplicaID, + &i.ProvisionerDaemon.Tags, + &i.ProvisionerDaemon.LastSeenAt, + &i.ProvisionerDaemon.Version, + &i.ProvisionerDaemon.APIVersion, + &i.ProvisionerDaemon.OrganizationID, + &i.ProvisionerDaemon.KeyID, + &i.Status, + &i.KeyName, + &i.CurrentJobID, + &i.CurrentJobStatus, + &i.PreviousJobID, + &i.PreviousJobStatus, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const updateProvisionerDaemonLastSeenAt = `-- name: UpdateProvisionerDaemonLastSeenAt :exec UPDATE provisioner_daemons SET diff --git a/coderd/database/queries/provisionerdaemons.sql b/coderd/database/queries/provisionerdaemons.sql index f76f71f5015bf..abf490c9ab47f 100644 --- a/coderd/database/queries/provisionerdaemons.sql +++ b/coderd/database/queries/provisionerdaemons.sql @@ -28,6 +28,54 @@ JOIN WHERE provisioner_jobs.id = ANY(@provisioner_job_ids :: uuid[]); +-- name: GetProvisionerDaemonsWithStatusByOrganization :many +SELECT + sqlc.embed(pd), + CASE + WHEN pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - (@stale_interval_ms::bigint || ' ms')::interval) + THEN 'offline' + ELSE CASE + WHEN current_job.id IS NOT NULL THEN 'busy' + ELSE 'idle' + END + END::provisioner_daemon_status AS status, + pk.name AS key_name, + -- NOTE(mafredri): sqlc.embed doesn't support nullable tables nor renaming them. + current_job.id AS current_job_id, + current_job.job_status AS current_job_status, + previous_job.id AS previous_job_id, + previous_job.job_status AS previous_job_status +FROM + provisioner_daemons pd +JOIN + provisioner_keys pk ON pk.id = pd.key_id +LEFT JOIN + provisioner_jobs current_job ON ( + current_job.worker_id = pd.id + AND current_job.completed_at IS NULL + ) +LEFT JOIN + provisioner_jobs previous_job ON ( + previous_job.id = ( + SELECT + id + FROM + provisioner_jobs + WHERE + worker_id = pd.id + AND completed_at IS NOT NULL + ORDER BY + completed_at DESC + LIMIT 1 + ) + ) +WHERE + pd.organization_id = @organization_id::uuid + AND (COALESCE(array_length(@ids::uuid[], 1), 0) = 0 OR pd.id = ANY(@ids::uuid[])) + AND (@tags::tagset = 'null'::tagset OR provisioner_tagset_contains(pd.tags::tagset, @tags::tagset)) +ORDER BY + pd.created_at ASC; + -- name: DeleteOldProvisionerDaemons :exec -- Delete provisioner daemons that have been created at least a week ago -- and have not connected to coderd since a week. diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml index fac159f71ebe3..b43281a3f1051 100644 --- a/coderd/database/sqlc.yaml +++ b/coderd/database/sqlc.yaml @@ -146,6 +146,7 @@ sql: login_type_oauth2_provider_app: LoginTypeOAuth2ProviderApp crypto_key_feature_workspace_apps_api_key: CryptoKeyFeatureWorkspaceAppsAPIKey crypto_key_feature_oidc_convert: CryptoKeyFeatureOIDCConvert + stale_interval_ms: StaleIntervalMS rules: - name: do-not-use-public-schema-in-queries message: "do not use public schema in queries" diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go new file mode 100644 index 0000000000000..879ee651bc107 --- /dev/null +++ b/coderd/provisionerdaemons.go @@ -0,0 +1,79 @@ +package coderd + +import ( + "net/http" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/db2sdk" + "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/provisionerdserver" + "github.com/coder/coder/v2/codersdk" +) + +// @Summary Get provisioner daemons +// @ID get-provisioner-daemons +// @Security CoderSessionToken +// @Produce json +// @Tags Enterprise +// @Param organization path string true "Organization ID" format(uuid) +// @Param tags query object false "Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'})" +// @Success 200 {array} codersdk.ProvisionerDaemonWithStatus +// @Router /organizations/{organization}/provisionerdaemons [get] +func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + org = httpmw.OrganizationParam(r) + tagParam = r.URL.Query().Get("tags") + tags = database.StringMap{} + err = tags.Scan([]byte(tagParam)) + ) + + if tagParam != "" && err != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid tags query parameter", + Detail: err.Error(), + }) + return + } + + daemons, err := api.Database.GetProvisionerDaemonsWithStatusByOrganization( + ctx, + database.GetProvisionerDaemonsWithStatusByOrganizationParams{ + OrganizationID: org.ID, + StaleIntervalMS: provisionerdserver.StaleInterval.Milliseconds(), + Tags: tags, + }, + ) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching provisioner daemons.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, db2sdk.List(daemons, func(dbDaemon database.GetProvisionerDaemonsWithStatusByOrganizationRow) codersdk.ProvisionerDaemonWithStatus { + pd := db2sdk.ProvisionerDaemon(dbDaemon.ProvisionerDaemon) + var currentJob, previousJob *codersdk.ProvisionerDaemonJob + if dbDaemon.CurrentJobID.Valid { + currentJob = &codersdk.ProvisionerDaemonJob{ + ID: dbDaemon.CurrentJobID.UUID, + Status: codersdk.ProvisionerJobStatus(dbDaemon.CurrentJobStatus.ProvisionerJobStatus), + } + } + if dbDaemon.PreviousJobID.Valid { + previousJob = &codersdk.ProvisionerDaemonJob{ + ID: dbDaemon.PreviousJobID.UUID, + Status: codersdk.ProvisionerJobStatus(dbDaemon.PreviousJobStatus.ProvisionerJobStatus), + } + } + return codersdk.ProvisionerDaemonWithStatus{ + ProvisionerDaemon: pd, + KeyName: dbDaemon.KeyName, + Status: codersdk.ProvisionerDaemonStatus(dbDaemon.Status), + CurrentJob: currentJob, + PreviousJob: previousJob, + } + })) +} diff --git a/coderd/provisionerdaemons_test.go b/coderd/provisionerdaemons_test.go new file mode 100644 index 0000000000000..243a24add021f --- /dev/null +++ b/coderd/provisionerdaemons_test.go @@ -0,0 +1,27 @@ +package coderd_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/testutil" +) + +func TestGetProvisionerDaemons(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + owner := coderdtest.CreateFirstUser(t, client) + memberClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) + + ctx := testutil.Context(t, testutil.WaitMedium) + + daemons, err := memberClient.ProvisionerDaemons(ctx) + require.NoError(t, err) + require.Len(t, daemons, 1) + }) +} diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 494f6c86887c4..67b6e76665723 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -296,7 +296,7 @@ func (c *Client) DeleteOrganization(ctx context.Context, orgID string) error { } // ProvisionerDaemons returns provisioner daemons available. -func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) { +func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemonWithStatus, error) { res, err := c.Request(ctx, http.MethodGet, // TODO: the organization path parameter is currently ignored. "/api/v2/organizations/default/provisionerdaemons", @@ -311,11 +311,11 @@ func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, e return nil, ReadBodyAsError(res) } - var daemons []ProvisionerDaemon + var daemons []ProvisionerDaemonWithStatus return daemons, json.NewDecoder(res.Body).Decode(&daemons) } -func (c *Client) OrganizationProvisionerDaemons(ctx context.Context, organizationID uuid.UUID, tags map[string]string) ([]ProvisionerDaemon, error) { +func (c *Client) OrganizationProvisionerDaemons(ctx context.Context, organizationID uuid.UUID, tags map[string]string) ([]ProvisionerDaemonWithStatus, error) { baseURL := fmt.Sprintf("/api/v2/organizations/%s/provisionerdaemons", organizationID.String()) queryParams := url.Values{} @@ -339,7 +339,7 @@ func (c *Client) OrganizationProvisionerDaemons(ctx context.Context, organizatio return nil, ReadBodyAsError(res) } - var daemons []ProvisionerDaemon + var daemons []ProvisionerDaemonWithStatus return daemons, json.NewDecoder(res.Body).Decode(&daemons) } diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index a2c8f2109f414..123554fa8b8bc 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -39,17 +39,40 @@ const ( LogLevelError LogLevel = "error" ) +// ProvisionerDaemonStatus represents the status of a provisioner daemon. +type ProvisionerDaemonStatus string + +// ProvisionerDaemonStatus enums. +const ( + ProvisionerDaemonOffline ProvisionerDaemonStatus = "offline" + ProvisionerDaemonIdle ProvisionerDaemonStatus = "idle" + ProvisionerDaemonBusy ProvisionerDaemonStatus = "busy" +) + type ProvisionerDaemon struct { - ID uuid.UUID `json:"id" format:"uuid"` - OrganizationID uuid.UUID `json:"organization_id" format:"uuid"` - KeyID uuid.UUID `json:"key_id" format:"uuid"` - CreatedAt time.Time `json:"created_at" format:"date-time"` - LastSeenAt NullTime `json:"last_seen_at,omitempty" format:"date-time"` - Name string `json:"name"` - Version string `json:"version"` - APIVersion string `json:"api_version"` - Provisioners []ProvisionerType `json:"provisioners"` - Tags map[string]string `json:"tags"` + ID uuid.UUID `json:"id" format:"uuid" table:"id"` + OrganizationID uuid.UUID `json:"organization_id" format:"uuid" table:"organization id"` + KeyID uuid.UUID `json:"key_id" format:"uuid" table:"-"` + CreatedAt time.Time `json:"created_at" format:"date-time" table:"created at"` + LastSeenAt NullTime `json:"last_seen_at,omitempty" format:"date-time" table:"last seen at"` + Name string `json:"name" table:"name,default_sort"` + Version string `json:"version" table:"version"` + APIVersion string `json:"api_version" table:"api version"` + Provisioners []ProvisionerType `json:"provisioners" table:"-"` + Tags map[string]string `json:"tags" table:"tags"` +} + +type ProvisionerDaemonWithStatus struct { + ProvisionerDaemon `table:"provisioner daemon,recursive_inline"` + KeyName string `json:"key_name" table:"key name"` + Status ProvisionerDaemonStatus `json:"status" enums:"offline,idle,busy" table:"status"` + CurrentJob *ProvisionerDaemonJob `json:"current_job" table:"current job,recursive"` + PreviousJob *ProvisionerDaemonJob `json:"previous_job" table:"previous job,recursive"` +} + +type ProvisionerDaemonJob struct { + ID uuid.UUID `json:"id" format:"uuid" table:"id"` + Status ProvisionerJobStatus `json:"status" enums:"pending,running,succeeded,canceling,canceled,failed" table:"status"` } // MatchedProvisioners represents the number of provisioner daemons diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index 80515f3ee7bef..28f5ba58e8dd3 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -1503,14 +1503,24 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi { "api_version": "string", "created_at": "2019-08-24T14:15:22Z", + "current_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", + "key_name": "string", "last_seen_at": "2019-08-24T14:15:22Z", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "previous_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, "provisioners": [ "string" ], + "status": "offline", "tags": { "property1": "string", "property2": "string" @@ -1522,28 +1532,48 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi ### Responses -| Status | Meaning | Description | Schema | -|--------|---------------------------------------------------------|-------------|-----------------------------------------------------------------------------| -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.ProvisionerDaemon](schemas.md#codersdkprovisionerdaemon) | +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|-------------------------------------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.ProvisionerDaemonWithStatus](schemas.md#codersdkprovisionerdaemonwithstatus) |

Response Schema

Status Code **200** -| Name | Type | Required | Restrictions | Description | -|---------------------|-------------------|----------|--------------|-------------| -| `[array item]` | array | false | | | -| `» api_version` | string | false | | | -| `» created_at` | string(date-time) | false | | | -| `» id` | string(uuid) | false | | | -| `» key_id` | string(uuid) | false | | | -| `» last_seen_at` | string(date-time) | false | | | -| `» name` | string | false | | | -| `» organization_id` | string(uuid) | false | | | -| `» provisioners` | array | false | | | -| `» tags` | object | false | | | -| `»» [any property]` | string | false | | | -| `» version` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|---------------------|--------------------------------------------------------------------------------|----------|--------------|-------------| +| `[array item]` | array | false | | | +| `» api_version` | string | false | | | +| `» created_at` | string(date-time) | false | | | +| `» current_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | +| `»» id` | string(uuid) | false | | | +| `»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | +| `» id` | string(uuid) | false | | | +| `» key_id` | string(uuid) | false | | | +| `» key_name` | string | false | | | +| `» last_seen_at` | string(date-time) | false | | | +| `» name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | +| `» previous_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | +| `» provisioners` | array | false | | | +| `» status` | [codersdk.ProvisionerDaemonStatus](schemas.md#codersdkprovisionerdaemonstatus) | false | | | +| `» tags` | object | false | | | +| `»» [any property]` | string | false | | | +| `» version` | string | false | | | + +#### Enumerated Values + +| Property | Value | +|----------|-------------| +| `status` | `pending` | +| `status` | `running` | +| `status` | `succeeded` | +| `status` | `canceling` | +| `status` | `canceled` | +| `status` | `failed` | +| `status` | `offline` | +| `status` | `idle` | +| `status` | `busy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 5b80483409149..48d0b8aaab19f 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4377,6 +4377,109 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | » `[any property]` | string | false | | | | `version` | string | false | | | +## codersdk.ProvisionerDaemonJob + +```json +{ + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|----------|----------------------------------------------------------------|----------|--------------|-------------| +| `id` | string | false | | | +| `status` | [codersdk.ProvisionerJobStatus](#codersdkprovisionerjobstatus) | false | | | + +#### Enumerated Values + +| Property | Value | +|----------|-------------| +| `status` | `pending` | +| `status` | `running` | +| `status` | `succeeded` | +| `status` | `canceling` | +| `status` | `canceled` | +| `status` | `failed` | + +## codersdk.ProvisionerDaemonStatus + +```json +"offline" +``` + +### Properties + +#### Enumerated Values + +| Value | +|-----------| +| `offline` | +| `idle` | +| `busy` | + +## codersdk.ProvisionerDaemonWithStatus + +```json +{ + "api_version": "string", + "created_at": "2019-08-24T14:15:22Z", + "current_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", + "key_name": "string", + "last_seen_at": "2019-08-24T14:15:22Z", + "name": "string", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "previous_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, + "provisioners": [ + "string" + ], + "status": "offline", + "tags": { + "property1": "string", + "property2": "string" + }, + "version": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|--------------------|----------------------------------------------------------------------|----------|--------------|-------------| +| `api_version` | string | false | | | +| `created_at` | string | false | | | +| `current_job` | [codersdk.ProvisionerDaemonJob](#codersdkprovisionerdaemonjob) | false | | | +| `id` | string | false | | | +| `key_id` | string | false | | | +| `key_name` | string | false | | | +| `last_seen_at` | string | false | | | +| `name` | string | false | | | +| `organization_id` | string | false | | | +| `previous_job` | [codersdk.ProvisionerDaemonJob](#codersdkprovisionerdaemonjob) | false | | | +| `provisioners` | array of string | false | | | +| `status` | [codersdk.ProvisionerDaemonStatus](#codersdkprovisionerdaemonstatus) | false | | | +| `tags` | object | false | | | +| » `[any property]` | string | false | | | +| `version` | string | false | | | + +#### Enumerated Values + +| Property | Value | +|----------|-----------| +| `status` | `offline` | +| `status` | `idle` | +| `status` | `busy` | + ## codersdk.ProvisionerJob ```json diff --git a/enterprise/cli/provisionerdaemonstart_test.go b/enterprise/cli/provisionerdaemonstart_test.go index 4829ccc38f23d..ebbc097fe3063 100644 --- a/enterprise/cli/provisionerdaemonstart_test.go +++ b/enterprise/cli/provisionerdaemonstart_test.go @@ -49,7 +49,7 @@ func TestProvisionerDaemon_PSK(t *testing.T) { pty.ExpectNoMatchBefore(ctx, "check entitlement", "starting provisioner daemon") pty.ExpectMatchContext(ctx, "matt-daemon") - var daemons []codersdk.ProvisionerDaemon + var daemons []codersdk.ProvisionerDaemonWithStatus require.Eventually(t, func() bool { daemons, err = client.ProvisionerDaemons(ctx) if err != nil { @@ -126,7 +126,7 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) { clitest.Start(t, inv) pty.ExpectMatchContext(ctx, "starting provisioner daemon") - var daemons []codersdk.ProvisionerDaemon + var daemons []codersdk.ProvisionerDaemonWithStatus var err error require.Eventually(t, func() bool { daemons, err = client.ProvisionerDaemons(ctx) @@ -161,7 +161,7 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) { clitest.Start(t, inv) pty.ExpectMatchContext(ctx, "starting provisioner daemon") - var daemons []codersdk.ProvisionerDaemon + var daemons []codersdk.ProvisionerDaemonWithStatus var err error require.Eventually(t, func() bool { daemons, err = client.ProvisionerDaemons(ctx) @@ -197,7 +197,7 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) { clitest.Start(t, inv) pty.ExpectMatchContext(ctx, "starting provisioner daemon") - var daemons []codersdk.ProvisionerDaemon + var daemons []codersdk.ProvisionerDaemonWithStatus var err error require.Eventually(t, func() bool { daemons, err = client.ProvisionerDaemons(ctx) @@ -233,7 +233,7 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) { clitest.Start(t, inv) pty.ExpectMatchContext(ctx, "starting provisioner daemon") - var daemons []codersdk.ProvisionerDaemon + var daemons []codersdk.ProvisionerDaemonWithStatus var err error require.Eventually(t, func() bool { daemons, err = client.OrganizationProvisionerDaemons(ctx, anotherOrg.ID, nil) @@ -280,7 +280,7 @@ func TestProvisionerDaemon_ProvisionerKey(t *testing.T) { pty.ExpectNoMatchBefore(ctx, "check entitlement", "starting provisioner daemon") pty.ExpectMatchContext(ctx, "matt-daemon") - var daemons []codersdk.ProvisionerDaemon + var daemons []codersdk.ProvisionerDaemonWithStatus require.Eventually(t, func() bool { daemons, err = client.OrganizationProvisionerDaemons(ctx, user.OrganizationID, nil) if err != nil { @@ -325,7 +325,7 @@ func TestProvisionerDaemon_ProvisionerKey(t *testing.T) { pty.ExpectNoMatchBefore(ctx, "check entitlement", "starting provisioner daemon") pty.ExpectMatchContext(ctx, `tags={"tag1":"value1","tag2":"value2"}`) - var daemons []codersdk.ProvisionerDaemon + var daemons []codersdk.ProvisionerDaemonWithStatus require.Eventually(t, func() bool { daemons, err = client.OrganizationProvisionerDaemons(ctx, user.OrganizationID, nil) if err != nil { @@ -440,8 +440,7 @@ func TestProvisionerDaemon_ProvisionerKey(t *testing.T) { clitest.Start(t, inv) pty.ExpectNoMatchBefore(ctx, "check entitlement", "starting provisioner daemon") pty.ExpectMatchContext(ctx, "matt-daemon") - - var daemons []codersdk.ProvisionerDaemon + var daemons []codersdk.ProvisionerDaemonWithStatus require.Eventually(t, func() bool { daemons, err = client.OrganizationProvisionerDaemons(ctx, anotherOrg.ID, nil) if err != nil { @@ -482,7 +481,7 @@ func TestProvisionerDaemon_PrometheusEnabled(t *testing.T) { clitest.Start(t, inv) pty.ExpectMatchContext(ctx, "starting provisioner daemon") - var daemons []codersdk.ProvisionerDaemon + var daemons []codersdk.ProvisionerDaemonWithStatus var err error require.Eventually(t, func() bool { daemons, err = client.ProvisionerDaemons(ctx) diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 90042bf0997fc..b32f763720b9d 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -379,7 +379,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { // // We may in future decide to scope provisioner daemons to organizations, so we'll keep the API // route as is. - r.Route("/organizations/{organization}/provisionerdaemons", func(r chi.Router) { + r.Route("/organizations/{organization}/provisionerdaemons/serve", func(r chi.Router) { r.Use( api.provisionerDaemonsEnabledMW, apiKeyMiddlewareOptional, @@ -393,8 +393,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { httpmw.RequireAPIKeyOrProvisionerDaemonAuth(), httpmw.ExtractOrganizationParam(api.Database), ) - r.With(apiKeyMiddleware).Get("/", api.provisionerDaemons) - r.With(apiKeyMiddlewareOptional).Get("/serve", api.provisionerDaemonServe) + r.Get("/", api.provisionerDaemonServe) }) r.Route("/templates/{template}/acl", func(r chi.Router) { r.Use( diff --git a/enterprise/coderd/provisionerdaemons.go b/enterprise/coderd/provisionerdaemons.go index 41991897f6614..f4335438654b5 100644 --- a/enterprise/coderd/provisionerdaemons.go +++ b/enterprise/coderd/provisionerdaemons.go @@ -20,7 +20,6 @@ import ( "cdr.dev/slog" "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" @@ -49,50 +48,6 @@ func (api *API) provisionerDaemonsEnabledMW(next http.Handler) http.Handler { }) } -// @Summary Get provisioner daemons -// @ID get-provisioner-daemons -// @Security CoderSessionToken -// @Produce json -// @Tags Enterprise -// @Param organization path string true "Organization ID" format(uuid) -// @Param tags query object false "Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'})" -// @Success 200 {array} codersdk.ProvisionerDaemon -// @Router /organizations/{organization}/provisionerdaemons [get] -func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) { - var ( - ctx = r.Context() - org = httpmw.OrganizationParam(r) - tagParam = r.URL.Query().Get("tags") - tags = database.StringMap{} - err = tags.Scan([]byte(tagParam)) - ) - - if tagParam != "" && err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid tags query parameter", - Detail: err.Error(), - }) - return - } - - daemons, err := api.Database.GetProvisionerDaemonsByOrganization( - ctx, - database.GetProvisionerDaemonsByOrganizationParams{ - OrganizationID: org.ID, - WantTags: tags, - }, - ) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching provisioner daemons.", - Detail: err.Error(), - }) - return - } - - httpapi.Write(ctx, rw, http.StatusOK, db2sdk.List(daemons, db2sdk.ProvisionerDaemon)) -} - type provisiionerDaemonAuthResponse struct { keyID uuid.UUID orgID uuid.UUID diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 122c36aab0aa7..f30db15b6315c 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1534,12 +1534,35 @@ export interface ProvisionerDaemon { readonly tags: Record; } +// From codersdk/provisionerdaemons.go +export interface ProvisionerDaemonJob { + readonly id: string; + readonly status: ProvisionerJobStatus; +} + // From codersdk/client.go export const ProvisionerDaemonKey = "Coder-Provisioner-Daemon-Key"; // From codersdk/client.go export const ProvisionerDaemonPSK = "Coder-Provisioner-Daemon-PSK"; +// From codersdk/provisionerdaemons.go +export type ProvisionerDaemonStatus = "busy" | "idle" | "offline"; + +export const ProvisionerDaemonStatuses: ProvisionerDaemonStatus[] = [ + "busy", + "idle", + "offline", +]; + +// From codersdk/provisionerdaemons.go +export interface ProvisionerDaemonWithStatus extends ProvisionerDaemon { + readonly key_name: string; + readonly status: ProvisionerDaemonStatus; + readonly current_job: ProvisionerDaemonJob | null; + readonly previous_job: ProvisionerDaemonJob | null; +} + // From healthsdk/healthsdk.go export interface ProvisionerDaemonsReport extends BaseReport { readonly items: readonly ProvisionerDaemonsReportItem[]; From 1f3b660596f3b812b14b0bbd70c107fbbab318c6 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Mon, 13 Jan 2025 13:02:25 +0000 Subject: [PATCH 2/7] fix dbauthz check stale timeout --- coderd/database/dbauthz/dbauthz_test.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index b4d8bbf75ad07..ff2e9b4d5cde0 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -3197,9 +3197,15 @@ func (s *MethodTestSuite) TestExtraMethods() { provisionersdk.TagScope: provisionersdk.ScopeOrganization, }, }) - ds, err := db.GetProvisionerDaemonsWithStatusByOrganization(context.Background(), database.GetProvisionerDaemonsWithStatusByOrganizationParams{OrganizationID: org.ID}) + ds, err := db.GetProvisionerDaemonsWithStatusByOrganization(context.Background(), database.GetProvisionerDaemonsWithStatusByOrganizationParams{ + OrganizationID: org.ID, + StaleIntervalMS: 86400000, // 24 hours. + }) s.NoError(err, "get provisioner daemon with status by org") - check.Args(database.GetProvisionerDaemonsWithStatusByOrganizationParams{OrganizationID: org.ID}).Asserts(d, policy.ActionRead).Returns(ds) + check.Args(database.GetProvisionerDaemonsWithStatusByOrganizationParams{ + OrganizationID: org.ID, + StaleIntervalMS: 86400000, // 24 hours. + }).Asserts(d, policy.ActionRead).Returns(ds) })) s.Run("GetEligibleProvisionerDaemonsByProvisionerJobIDs", s.Subtest(func(db database.Store, check *expects) { dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) From 68275284f0ffb23e9d52783d9f925796c7561aea Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Mon, 13 Jan 2025 13:47:11 +0000 Subject: [PATCH 3/7] remove provisioner daemon with status type --- coderd/apidoc/docs.go | 91 +++-------- coderd/apidoc/swagger.json | 83 +++------- coderd/provisionerdaemons.go | 20 +-- codersdk/organizations.go | 8 +- codersdk/provisionerdaemons.go | 12 +- docs/reference/api/debug.md | 10 ++ docs/reference/api/enterprise.md | 120 ++++++++------ docs/reference/api/schemas.md | 148 +++++++++--------- enterprise/cli/provisionerdaemonstart_test.go | 18 +-- site/src/api/typesGenerated.ts | 12 +- 10 files changed, 235 insertions(+), 287 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index e8b608e4e63f8..97060dccafb6f 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -2989,7 +2989,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/codersdk.ProvisionerDaemonWithStatus" + "$ref": "#/definitions/codersdk.ProvisionerDaemon" } } } @@ -12463,6 +12463,9 @@ const docTemplate = `{ "type": "string", "format": "date-time" }, + "current_job": { + "$ref": "#/definitions/codersdk.ProvisionerDaemonJob" + }, "id": { "type": "string", "format": "uuid" @@ -12471,6 +12474,10 @@ const docTemplate = `{ "type": "string", "format": "uuid" }, + "key_name": { + "description": "Optional fields.", + "type": "string" + }, "last_seen_at": { "type": "string", "format": "date-time" @@ -12482,12 +12489,27 @@ const docTemplate = `{ "type": "string", "format": "uuid" }, + "previous_job": { + "$ref": "#/definitions/codersdk.ProvisionerDaemonJob" + }, "provisioners": { "type": "array", "items": { "type": "string" } }, + "status": { + "enum": [ + "offline", + "idle", + "busy" + ], + "allOf": [ + { + "$ref": "#/definitions/codersdk.ProvisionerDaemonStatus" + } + ] + }, "tags": { "type": "object", "additionalProperties": { @@ -12536,73 +12558,6 @@ const docTemplate = `{ "ProvisionerDaemonBusy" ] }, - "codersdk.ProvisionerDaemonWithStatus": { - "type": "object", - "properties": { - "api_version": { - "type": "string" - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "current_job": { - "$ref": "#/definitions/codersdk.ProvisionerDaemonJob" - }, - "id": { - "type": "string", - "format": "uuid" - }, - "key_id": { - "type": "string", - "format": "uuid" - }, - "key_name": { - "type": "string" - }, - "last_seen_at": { - "type": "string", - "format": "date-time" - }, - "name": { - "type": "string" - }, - "organization_id": { - "type": "string", - "format": "uuid" - }, - "previous_job": { - "$ref": "#/definitions/codersdk.ProvisionerDaemonJob" - }, - "provisioners": { - "type": "array", - "items": { - "type": "string" - } - }, - "status": { - "enum": [ - "offline", - "idle", - "busy" - ], - "allOf": [ - { - "$ref": "#/definitions/codersdk.ProvisionerDaemonStatus" - } - ] - }, - "tags": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "version": { - "type": "string" - } - } - }, "codersdk.ProvisionerJob": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index dc50f059e8caa..1e77a05775ac8 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -2623,7 +2623,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/codersdk.ProvisionerDaemonWithStatus" + "$ref": "#/definitions/codersdk.ProvisionerDaemon" } } } @@ -11244,6 +11244,9 @@ "type": "string", "format": "date-time" }, + "current_job": { + "$ref": "#/definitions/codersdk.ProvisionerDaemonJob" + }, "id": { "type": "string", "format": "uuid" @@ -11252,6 +11255,10 @@ "type": "string", "format": "uuid" }, + "key_name": { + "description": "Optional fields.", + "type": "string" + }, "last_seen_at": { "type": "string", "format": "date-time" @@ -11263,12 +11270,23 @@ "type": "string", "format": "uuid" }, + "previous_job": { + "$ref": "#/definitions/codersdk.ProvisionerDaemonJob" + }, "provisioners": { "type": "array", "items": { "type": "string" } }, + "status": { + "enum": ["offline", "idle", "busy"], + "allOf": [ + { + "$ref": "#/definitions/codersdk.ProvisionerDaemonStatus" + } + ] + }, "tags": { "type": "object", "additionalProperties": { @@ -11313,69 +11331,6 @@ "ProvisionerDaemonBusy" ] }, - "codersdk.ProvisionerDaemonWithStatus": { - "type": "object", - "properties": { - "api_version": { - "type": "string" - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "current_job": { - "$ref": "#/definitions/codersdk.ProvisionerDaemonJob" - }, - "id": { - "type": "string", - "format": "uuid" - }, - "key_id": { - "type": "string", - "format": "uuid" - }, - "key_name": { - "type": "string" - }, - "last_seen_at": { - "type": "string", - "format": "date-time" - }, - "name": { - "type": "string" - }, - "organization_id": { - "type": "string", - "format": "uuid" - }, - "previous_job": { - "$ref": "#/definitions/codersdk.ProvisionerDaemonJob" - }, - "provisioners": { - "type": "array", - "items": { - "type": "string" - } - }, - "status": { - "enum": ["offline", "idle", "busy"], - "allOf": [ - { - "$ref": "#/definitions/codersdk.ProvisionerDaemonStatus" - } - ] - }, - "tags": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "version": { - "type": "string" - } - } - }, "codersdk.ProvisionerJob": { "type": "object", "properties": { diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index 879ee651bc107..8b0b41cb9de72 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -18,7 +18,7 @@ import ( // @Tags Enterprise // @Param organization path string true "Organization ID" format(uuid) // @Param tags query object false "Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'})" -// @Success 200 {array} codersdk.ProvisionerDaemonWithStatus +// @Success 200 {array} codersdk.ProvisionerDaemon // @Router /organizations/{organization}/provisionerdaemons [get] func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) { var ( @@ -53,7 +53,7 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) { return } - httpapi.Write(ctx, rw, http.StatusOK, db2sdk.List(daemons, func(dbDaemon database.GetProvisionerDaemonsWithStatusByOrganizationRow) codersdk.ProvisionerDaemonWithStatus { + httpapi.Write(ctx, rw, http.StatusOK, db2sdk.List(daemons, func(dbDaemon database.GetProvisionerDaemonsWithStatusByOrganizationRow) codersdk.ProvisionerDaemon { pd := db2sdk.ProvisionerDaemon(dbDaemon.ProvisionerDaemon) var currentJob, previousJob *codersdk.ProvisionerDaemonJob if dbDaemon.CurrentJobID.Valid { @@ -68,12 +68,14 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) { Status: codersdk.ProvisionerJobStatus(dbDaemon.PreviousJobStatus.ProvisionerJobStatus), } } - return codersdk.ProvisionerDaemonWithStatus{ - ProvisionerDaemon: pd, - KeyName: dbDaemon.KeyName, - Status: codersdk.ProvisionerDaemonStatus(dbDaemon.Status), - CurrentJob: currentJob, - PreviousJob: previousJob, - } + + // Add optional fields. + status := codersdk.ProvisionerDaemonStatus(dbDaemon.Status) + pd.KeyName = &dbDaemon.KeyName + pd.Status = &status + pd.CurrentJob = currentJob + pd.PreviousJob = previousJob + + return pd })) } diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 67b6e76665723..494f6c86887c4 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -296,7 +296,7 @@ func (c *Client) DeleteOrganization(ctx context.Context, orgID string) error { } // ProvisionerDaemons returns provisioner daemons available. -func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemonWithStatus, error) { +func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) { res, err := c.Request(ctx, http.MethodGet, // TODO: the organization path parameter is currently ignored. "/api/v2/organizations/default/provisionerdaemons", @@ -311,11 +311,11 @@ func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemonWit return nil, ReadBodyAsError(res) } - var daemons []ProvisionerDaemonWithStatus + var daemons []ProvisionerDaemon return daemons, json.NewDecoder(res.Body).Decode(&daemons) } -func (c *Client) OrganizationProvisionerDaemons(ctx context.Context, organizationID uuid.UUID, tags map[string]string) ([]ProvisionerDaemonWithStatus, error) { +func (c *Client) OrganizationProvisionerDaemons(ctx context.Context, organizationID uuid.UUID, tags map[string]string) ([]ProvisionerDaemon, error) { baseURL := fmt.Sprintf("/api/v2/organizations/%s/provisionerdaemons", organizationID.String()) queryParams := url.Values{} @@ -339,7 +339,7 @@ func (c *Client) OrganizationProvisionerDaemons(ctx context.Context, organizatio return nil, ReadBodyAsError(res) } - var daemons []ProvisionerDaemonWithStatus + var daemons []ProvisionerDaemon return daemons, json.NewDecoder(res.Body).Decode(&daemons) } diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index 123554fa8b8bc..675c31408243a 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -60,14 +60,12 @@ type ProvisionerDaemon struct { APIVersion string `json:"api_version" table:"api version"` Provisioners []ProvisionerType `json:"provisioners" table:"-"` Tags map[string]string `json:"tags" table:"tags"` -} -type ProvisionerDaemonWithStatus struct { - ProvisionerDaemon `table:"provisioner daemon,recursive_inline"` - KeyName string `json:"key_name" table:"key name"` - Status ProvisionerDaemonStatus `json:"status" enums:"offline,idle,busy" table:"status"` - CurrentJob *ProvisionerDaemonJob `json:"current_job" table:"current job,recursive"` - PreviousJob *ProvisionerDaemonJob `json:"previous_job" table:"previous job,recursive"` + // Optional fields. + KeyName *string `json:"key_name" table:"key name"` + Status *ProvisionerDaemonStatus `json:"status" enums:"offline,idle,busy" table:"status"` + CurrentJob *ProvisionerDaemonJob `json:"current_job" table:"current job,recursive"` + PreviousJob *ProvisionerDaemonJob `json:"previous_job" table:"previous job,recursive"` } type ProvisionerDaemonJob struct { diff --git a/docs/reference/api/debug.md b/docs/reference/api/debug.md index d92b7482fc374..63fd1aeda8f98 100644 --- a/docs/reference/api/debug.md +++ b/docs/reference/api/debug.md @@ -307,14 +307,24 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "provisioner_daemon": { "api_version": "string", "created_at": "2019-08-24T14:15:22Z", + "current_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", + "key_name": "string", "last_seen_at": "2019-08-24T14:15:22Z", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "previous_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, "provisioners": [ "string" ], + "status": "offline", "tags": { "property1": "string", "property2": "string" diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index 28f5ba58e8dd3..bfc53cac53deb 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -1532,34 +1532,34 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi ### Responses -| Status | Meaning | Description | Schema | -|--------|---------------------------------------------------------|-------------|-------------------------------------------------------------------------------------------------| -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.ProvisionerDaemonWithStatus](schemas.md#codersdkprovisionerdaemonwithstatus) | +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|-----------------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.ProvisionerDaemon](schemas.md#codersdkprovisionerdaemon) |

Response Schema

Status Code **200** -| Name | Type | Required | Restrictions | Description | -|---------------------|--------------------------------------------------------------------------------|----------|--------------|-------------| -| `[array item]` | array | false | | | -| `» api_version` | string | false | | | -| `» created_at` | string(date-time) | false | | | -| `» current_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | -| `»» id` | string(uuid) | false | | | -| `»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | -| `» id` | string(uuid) | false | | | -| `» key_id` | string(uuid) | false | | | -| `» key_name` | string | false | | | -| `» last_seen_at` | string(date-time) | false | | | -| `» name` | string | false | | | -| `» organization_id` | string(uuid) | false | | | -| `» previous_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | -| `» provisioners` | array | false | | | -| `» status` | [codersdk.ProvisionerDaemonStatus](schemas.md#codersdkprovisionerdaemonstatus) | false | | | -| `» tags` | object | false | | | -| `»» [any property]` | string | false | | | -| `» version` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|---------------------|--------------------------------------------------------------------------------|----------|--------------|------------------| +| `[array item]` | array | false | | | +| `» api_version` | string | false | | | +| `» created_at` | string(date-time) | false | | | +| `» current_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | +| `»» id` | string(uuid) | false | | | +| `»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | +| `» id` | string(uuid) | false | | | +| `» key_id` | string(uuid) | false | | | +| `» key_name` | string | false | | Optional fields. | +| `» last_seen_at` | string(date-time) | false | | | +| `» name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | +| `» previous_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | +| `» provisioners` | array | false | | | +| `» status` | [codersdk.ProvisionerDaemonStatus](schemas.md#codersdkprovisionerdaemonstatus) | false | | | +| `» tags` | object | false | | | +| `»» [any property]` | string | false | | | +| `» version` | string | false | | | #### Enumerated Values @@ -1730,14 +1730,24 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi { "api_version": "string", "created_at": "2019-08-24T14:15:22Z", + "current_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", + "key_name": "string", "last_seen_at": "2019-08-24T14:15:22Z", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "previous_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, "provisioners": [ "string" ], + "status": "offline", "tags": { "property1": "string", "property2": "string" @@ -1769,28 +1779,48 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi Status Code **200** -| Name | Type | Required | Restrictions | Description | -|----------------------|----------------------------------------------------------------------|----------|--------------|-------------| -| `[array item]` | array | false | | | -| `» daemons` | array | false | | | -| `»» api_version` | string | false | | | -| `»» created_at` | string(date-time) | false | | | -| `»» id` | string(uuid) | false | | | -| `»» key_id` | string(uuid) | false | | | -| `»» last_seen_at` | string(date-time) | false | | | -| `»» name` | string | false | | | -| `»» organization_id` | string(uuid) | false | | | -| `»» provisioners` | array | false | | | -| `»» tags` | object | false | | | -| `»»» [any property]` | string | false | | | -| `»» version` | string | false | | | -| `» key` | [codersdk.ProvisionerKey](schemas.md#codersdkprovisionerkey) | false | | | -| `»» created_at` | string(date-time) | false | | | -| `»» id` | string(uuid) | false | | | -| `»» name` | string | false | | | -| `»» organization` | string(uuid) | false | | | -| `»» tags` | [codersdk.ProvisionerKeyTags](schemas.md#codersdkprovisionerkeytags) | false | | | -| `»»» [any property]` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|----------------------|--------------------------------------------------------------------------------|----------|--------------|------------------| +| `[array item]` | array | false | | | +| `» daemons` | array | false | | | +| `»» api_version` | string | false | | | +| `»» created_at` | string(date-time) | false | | | +| `»» current_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | +| `»»» id` | string(uuid) | false | | | +| `»»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | +| `»» id` | string(uuid) | false | | | +| `»» key_id` | string(uuid) | false | | | +| `»» key_name` | string | false | | Optional fields. | +| `»» last_seen_at` | string(date-time) | false | | | +| `»» name` | string | false | | | +| `»» organization_id` | string(uuid) | false | | | +| `»» previous_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | +| `»» provisioners` | array | false | | | +| `»» status` | [codersdk.ProvisionerDaemonStatus](schemas.md#codersdkprovisionerdaemonstatus) | false | | | +| `»» tags` | object | false | | | +| `»»» [any property]` | string | false | | | +| `»» version` | string | false | | | +| `» key` | [codersdk.ProvisionerKey](schemas.md#codersdkprovisionerkey) | false | | | +| `»» created_at` | string(date-time) | false | | | +| `»» id` | string(uuid) | false | | | +| `»» name` | string | false | | | +| `»» organization` | string(uuid) | false | | | +| `»» tags` | [codersdk.ProvisionerKeyTags](schemas.md#codersdkprovisionerkeytags) | false | | | +| `»»» [any property]` | string | false | | | + +#### Enumerated Values + +| Property | Value | +|----------|-------------| +| `status` | `pending` | +| `status` | `running` | +| `status` | `succeeded` | +| `status` | `canceling` | +| `status` | `canceled` | +| `status` | `failed` | +| `status` | `offline` | +| `status` | `idle` | +| `status` | `busy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 48d0b8aaab19f..b20e0342de0c5 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4345,14 +4345,24 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith { "api_version": "string", "created_at": "2019-08-24T14:15:22Z", + "current_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", + "key_name": "string", "last_seen_at": "2019-08-24T14:15:22Z", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "previous_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, "provisioners": [ "string" ], + "status": "offline", "tags": { "property1": "string", "property2": "string" @@ -4363,19 +4373,31 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith ### Properties -| Name | Type | Required | Restrictions | Description | -|--------------------|-----------------|----------|--------------|-------------| -| `api_version` | string | false | | | -| `created_at` | string | false | | | -| `id` | string | false | | | -| `key_id` | string | false | | | -| `last_seen_at` | string | false | | | -| `name` | string | false | | | -| `organization_id` | string | false | | | -| `provisioners` | array of string | false | | | -| `tags` | object | false | | | -| » `[any property]` | string | false | | | -| `version` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|--------------------|----------------------------------------------------------------------|----------|--------------|------------------| +| `api_version` | string | false | | | +| `created_at` | string | false | | | +| `current_job` | [codersdk.ProvisionerDaemonJob](#codersdkprovisionerdaemonjob) | false | | | +| `id` | string | false | | | +| `key_id` | string | false | | | +| `key_name` | string | false | | Optional fields. | +| `last_seen_at` | string | false | | | +| `name` | string | false | | | +| `organization_id` | string | false | | | +| `previous_job` | [codersdk.ProvisionerDaemonJob](#codersdkprovisionerdaemonjob) | false | | | +| `provisioners` | array of string | false | | | +| `status` | [codersdk.ProvisionerDaemonStatus](#codersdkprovisionerdaemonstatus) | false | | | +| `tags` | object | false | | | +| » `[any property]` | string | false | | | +| `version` | string | false | | | + +#### Enumerated Values + +| Property | Value | +|----------|-----------| +| `status` | `offline` | +| `status` | `idle` | +| `status` | `busy` | ## codersdk.ProvisionerDaemonJob @@ -4420,66 +4442,6 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | `idle` | | `busy` | -## codersdk.ProvisionerDaemonWithStatus - -```json -{ - "api_version": "string", - "created_at": "2019-08-24T14:15:22Z", - "current_job": { - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "status": "pending" - }, - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", - "key_name": "string", - "last_seen_at": "2019-08-24T14:15:22Z", - "name": "string", - "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "previous_job": { - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "status": "pending" - }, - "provisioners": [ - "string" - ], - "status": "offline", - "tags": { - "property1": "string", - "property2": "string" - }, - "version": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -|--------------------|----------------------------------------------------------------------|----------|--------------|-------------| -| `api_version` | string | false | | | -| `created_at` | string | false | | | -| `current_job` | [codersdk.ProvisionerDaemonJob](#codersdkprovisionerdaemonjob) | false | | | -| `id` | string | false | | | -| `key_id` | string | false | | | -| `key_name` | string | false | | | -| `last_seen_at` | string | false | | | -| `name` | string | false | | | -| `organization_id` | string | false | | | -| `previous_job` | [codersdk.ProvisionerDaemonJob](#codersdkprovisionerdaemonjob) | false | | | -| `provisioners` | array of string | false | | | -| `status` | [codersdk.ProvisionerDaemonStatus](#codersdkprovisionerdaemonstatus) | false | | | -| `tags` | object | false | | | -| » `[any property]` | string | false | | | -| `version` | string | false | | | - -#### Enumerated Values - -| Property | Value | -|----------|-----------| -| `status` | `offline` | -| `status` | `idle` | -| `status` | `busy` | - ## codersdk.ProvisionerJob ```json @@ -4621,14 +4583,24 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith { "api_version": "string", "created_at": "2019-08-24T14:15:22Z", + "current_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", + "key_name": "string", "last_seen_at": "2019-08-24T14:15:22Z", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "previous_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, "provisioners": [ "string" ], + "status": "offline", "tags": { "property1": "string", "property2": "string" @@ -9466,14 +9438,24 @@ Zero means unspecified. There might be a limit, but the client need not try to r "provisioner_daemon": { "api_version": "string", "created_at": "2019-08-24T14:15:22Z", + "current_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", + "key_name": "string", "last_seen_at": "2019-08-24T14:15:22Z", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "previous_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, "provisioners": [ "string" ], + "status": "offline", "tags": { "property1": "string", "property2": "string" @@ -9592,14 +9574,24 @@ Zero means unspecified. There might be a limit, but the client need not try to r "provisioner_daemon": { "api_version": "string", "created_at": "2019-08-24T14:15:22Z", + "current_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", + "key_name": "string", "last_seen_at": "2019-08-24T14:15:22Z", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "previous_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, "provisioners": [ "string" ], + "status": "offline", "tags": { "property1": "string", "property2": "string" @@ -9649,14 +9641,24 @@ Zero means unspecified. There might be a limit, but the client need not try to r "provisioner_daemon": { "api_version": "string", "created_at": "2019-08-24T14:15:22Z", + "current_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", + "key_name": "string", "last_seen_at": "2019-08-24T14:15:22Z", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "previous_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, "provisioners": [ "string" ], + "status": "offline", "tags": { "property1": "string", "property2": "string" diff --git a/enterprise/cli/provisionerdaemonstart_test.go b/enterprise/cli/provisionerdaemonstart_test.go index ebbc097fe3063..58603715f8184 100644 --- a/enterprise/cli/provisionerdaemonstart_test.go +++ b/enterprise/cli/provisionerdaemonstart_test.go @@ -49,7 +49,7 @@ func TestProvisionerDaemon_PSK(t *testing.T) { pty.ExpectNoMatchBefore(ctx, "check entitlement", "starting provisioner daemon") pty.ExpectMatchContext(ctx, "matt-daemon") - var daemons []codersdk.ProvisionerDaemonWithStatus + var daemons []codersdk.ProvisionerDaemon require.Eventually(t, func() bool { daemons, err = client.ProvisionerDaemons(ctx) if err != nil { @@ -126,7 +126,7 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) { clitest.Start(t, inv) pty.ExpectMatchContext(ctx, "starting provisioner daemon") - var daemons []codersdk.ProvisionerDaemonWithStatus + var daemons []codersdk.ProvisionerDaemon var err error require.Eventually(t, func() bool { daemons, err = client.ProvisionerDaemons(ctx) @@ -161,7 +161,7 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) { clitest.Start(t, inv) pty.ExpectMatchContext(ctx, "starting provisioner daemon") - var daemons []codersdk.ProvisionerDaemonWithStatus + var daemons []codersdk.ProvisionerDaemon var err error require.Eventually(t, func() bool { daemons, err = client.ProvisionerDaemons(ctx) @@ -197,7 +197,7 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) { clitest.Start(t, inv) pty.ExpectMatchContext(ctx, "starting provisioner daemon") - var daemons []codersdk.ProvisionerDaemonWithStatus + var daemons []codersdk.ProvisionerDaemon var err error require.Eventually(t, func() bool { daemons, err = client.ProvisionerDaemons(ctx) @@ -233,7 +233,7 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) { clitest.Start(t, inv) pty.ExpectMatchContext(ctx, "starting provisioner daemon") - var daemons []codersdk.ProvisionerDaemonWithStatus + var daemons []codersdk.ProvisionerDaemon var err error require.Eventually(t, func() bool { daemons, err = client.OrganizationProvisionerDaemons(ctx, anotherOrg.ID, nil) @@ -280,7 +280,7 @@ func TestProvisionerDaemon_ProvisionerKey(t *testing.T) { pty.ExpectNoMatchBefore(ctx, "check entitlement", "starting provisioner daemon") pty.ExpectMatchContext(ctx, "matt-daemon") - var daemons []codersdk.ProvisionerDaemonWithStatus + var daemons []codersdk.ProvisionerDaemon require.Eventually(t, func() bool { daemons, err = client.OrganizationProvisionerDaemons(ctx, user.OrganizationID, nil) if err != nil { @@ -325,7 +325,7 @@ func TestProvisionerDaemon_ProvisionerKey(t *testing.T) { pty.ExpectNoMatchBefore(ctx, "check entitlement", "starting provisioner daemon") pty.ExpectMatchContext(ctx, `tags={"tag1":"value1","tag2":"value2"}`) - var daemons []codersdk.ProvisionerDaemonWithStatus + var daemons []codersdk.ProvisionerDaemon require.Eventually(t, func() bool { daemons, err = client.OrganizationProvisionerDaemons(ctx, user.OrganizationID, nil) if err != nil { @@ -440,7 +440,7 @@ func TestProvisionerDaemon_ProvisionerKey(t *testing.T) { clitest.Start(t, inv) pty.ExpectNoMatchBefore(ctx, "check entitlement", "starting provisioner daemon") pty.ExpectMatchContext(ctx, "matt-daemon") - var daemons []codersdk.ProvisionerDaemonWithStatus + var daemons []codersdk.ProvisionerDaemon require.Eventually(t, func() bool { daemons, err = client.OrganizationProvisionerDaemons(ctx, anotherOrg.ID, nil) if err != nil { @@ -481,7 +481,7 @@ func TestProvisionerDaemon_PrometheusEnabled(t *testing.T) { clitest.Start(t, inv) pty.ExpectMatchContext(ctx, "starting provisioner daemon") - var daemons []codersdk.ProvisionerDaemonWithStatus + var daemons []codersdk.ProvisionerDaemon var err error require.Eventually(t, func() bool { daemons, err = client.ProvisionerDaemons(ctx) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index f30db15b6315c..9ba1e9b2a106a 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1532,6 +1532,10 @@ export interface ProvisionerDaemon { readonly api_version: string; readonly provisioners: readonly ProvisionerType[]; readonly tags: Record; + readonly key_name: string | null; + readonly status: ProvisionerDaemonStatus | null; + readonly current_job: ProvisionerDaemonJob | null; + readonly previous_job: ProvisionerDaemonJob | null; } // From codersdk/provisionerdaemons.go @@ -1555,14 +1559,6 @@ export const ProvisionerDaemonStatuses: ProvisionerDaemonStatus[] = [ "offline", ]; -// From codersdk/provisionerdaemons.go -export interface ProvisionerDaemonWithStatus extends ProvisionerDaemon { - readonly key_name: string; - readonly status: ProvisionerDaemonStatus; - readonly current_job: ProvisionerDaemonJob | null; - readonly previous_job: ProvisionerDaemonJob | null; -} - // From healthsdk/healthsdk.go export interface ProvisionerDaemonsReport extends BaseReport { readonly items: readonly ProvisionerDaemonsReportItem[]; From c865db8d9b6c11e3f99bd5b041f137ddc50bf6f0 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Mon, 13 Jan 2025 14:11:08 +0000 Subject: [PATCH 4/7] fix migrations --- ..._status.down.sql => 000284_provisioner_daemon_status.down.sql} | 0 ...emon_status.up.sql => 000284_provisioner_daemon_status.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000283_provisioner_daemon_status.down.sql => 000284_provisioner_daemon_status.down.sql} (100%) rename coderd/database/migrations/{000283_provisioner_daemon_status.up.sql => 000284_provisioner_daemon_status.up.sql} (100%) diff --git a/coderd/database/migrations/000283_provisioner_daemon_status.down.sql b/coderd/database/migrations/000284_provisioner_daemon_status.down.sql similarity index 100% rename from coderd/database/migrations/000283_provisioner_daemon_status.down.sql rename to coderd/database/migrations/000284_provisioner_daemon_status.down.sql diff --git a/coderd/database/migrations/000283_provisioner_daemon_status.up.sql b/coderd/database/migrations/000284_provisioner_daemon_status.up.sql similarity index 100% rename from coderd/database/migrations/000283_provisioner_daemon_status.up.sql rename to coderd/database/migrations/000284_provisioner_daemon_status.up.sql From d93d79efcbd85d689149917a158e03ab6a016d76 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Mon, 13 Jan 2025 14:37:47 +0000 Subject: [PATCH 5/7] fix lint --- ...000286_provisioner_daemon_status.down.sql} | 0 ...> 000286_provisioner_daemon_status.up.sql} | 0 site/src/testHelpers/entities.ts | 23 +++++++++++++++++++ 3 files changed, 23 insertions(+) rename coderd/database/migrations/{000284_provisioner_daemon_status.down.sql => 000286_provisioner_daemon_status.down.sql} (100%) rename coderd/database/migrations/{000284_provisioner_daemon_status.up.sql => 000286_provisioner_daemon_status.up.sql} (100%) diff --git a/coderd/database/migrations/000284_provisioner_daemon_status.down.sql b/coderd/database/migrations/000286_provisioner_daemon_status.down.sql similarity index 100% rename from coderd/database/migrations/000284_provisioner_daemon_status.down.sql rename to coderd/database/migrations/000286_provisioner_daemon_status.down.sql diff --git a/coderd/database/migrations/000284_provisioner_daemon_status.up.sql b/coderd/database/migrations/000286_provisioner_daemon_status.up.sql similarity index 100% rename from coderd/database/migrations/000284_provisioner_daemon_status.up.sql rename to coderd/database/migrations/000286_provisioner_daemon_status.up.sql diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index e15377de05430..701fc3e8e1b0c 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -580,6 +580,10 @@ export const MockProvisioner: TypesGen.ProvisionerDaemon = { version: MockBuildInfo.version, api_version: MockBuildInfo.provisioner_api_version, last_seen_at: new Date().toISOString(), + key_name: "test-provisioner", + status: "idle", + current_job: null, + previous_job: null, }; export const MockUserAuthProvisioner: TypesGen.ProvisionerDaemon = { @@ -594,6 +598,7 @@ export const MockPskProvisioner: TypesGen.ProvisionerDaemon = { ...MockProvisioner, id: "test-psk-provisioner", key_id: MockProvisionerPskKey.id, + key_name: MockProvisionerPskKey.name, name: "Test psk provisioner", }; @@ -601,6 +606,7 @@ export const MockKeyProvisioner: TypesGen.ProvisionerDaemon = { ...MockProvisioner, id: "test-key-provisioner", key_id: MockProvisionerKey.id, + key_name: MockProvisionerKey.name, organization_id: MockProvisionerKey.organization, name: "Test key provisioner", tags: MockProvisionerKey.tags, @@ -611,6 +617,7 @@ export const MockProvisioner2: TypesGen.ProvisionerDaemon = { id: "test-provisioner-2", name: "Test Provisioner 2", key_id: MockProvisionerKey.id, + key_name: MockProvisionerKey.name, }; export const MockUserProvisioner: TypesGen.ProvisionerDaemon = { @@ -3741,6 +3748,10 @@ export const MockHealth: TypesGen.HealthcheckReport = { tag_1: "1", tag_yes: "yes", }, + key_name: MockProvisionerKey.name, + current_job: null, + previous_job: null, + status: "idle", }, warnings: [], }, @@ -3763,6 +3774,10 @@ export const MockHealth: TypesGen.HealthcheckReport = { tag_1: "1", tag_YES: "YES", }, + key_name: MockProvisionerKey.name, + current_job: null, + previous_job: null, + status: "idle", }, warnings: [], }, @@ -3785,6 +3800,10 @@ export const MockHealth: TypesGen.HealthcheckReport = { tag_0: "0", tag_no: "no", }, + key_name: MockProvisionerKey.name, + current_job: null, + previous_job: null, + status: "idle", }, warnings: [ { @@ -3938,6 +3957,10 @@ export const DeploymentHealthUnhealthy: TypesGen.HealthcheckReport = { owner: "", scope: "organization", }, + key_name: MockProvisionerKey.name, + current_job: null, + previous_job: null, + status: "idle", }, warnings: [ { From 11be5c0ca8b4de61db9f492e44f149a4d592ae94 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Tue, 14 Jan 2025 14:03:37 +0000 Subject: [PATCH 6/7] add query tests --- coderd/database/dbauthz/dbauthz_test.go | 4 +- coderd/database/querier_test.go | 120 ++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 2 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index ff2e9b4d5cde0..5ad8dca748c3f 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -3199,12 +3199,12 @@ func (s *MethodTestSuite) TestExtraMethods() { }) ds, err := db.GetProvisionerDaemonsWithStatusByOrganization(context.Background(), database.GetProvisionerDaemonsWithStatusByOrganizationParams{ OrganizationID: org.ID, - StaleIntervalMS: 86400000, // 24 hours. + StaleIntervalMS: 24 * time.Hour.Milliseconds(), }) s.NoError(err, "get provisioner daemon with status by org") check.Args(database.GetProvisionerDaemonsWithStatusByOrganizationParams{ OrganizationID: org.ID, - StaleIntervalMS: 86400000, // 24 hours. + StaleIntervalMS: 24 * time.Hour.Milliseconds(), }).Asserts(d, policy.ActionRead).Returns(ds) })) s.Run("GetEligibleProvisionerDaemonsByProvisionerJobIDs", s.Subtest(func(db database.Store, check *expects) { diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index 0c7a445ed7104..7d489cf559220 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -353,6 +353,126 @@ func TestGetEligibleProvisionerDaemonsByProvisionerJobIDs(t *testing.T) { }) } +func TestGetProvisionerDaemonsWithStatusByOrganization(t *testing.T) { + t.Parallel() + + t.Run("NoDaemonsInOrgReturnsEmpty", func(t *testing.T) { + t.Parallel() + db, _ := dbtestutil.NewDB(t) + org := dbgen.Organization(t, db, database.Organization{}) + otherOrg := dbgen.Organization(t, db, database.Organization{}) + dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{ + Name: "non-matching-daemon", + OrganizationID: otherOrg.ID, + }) + daemons, err := db.GetProvisionerDaemonsWithStatusByOrganization(context.Background(), database.GetProvisionerDaemonsWithStatusByOrganizationParams{ + OrganizationID: org.ID, + }) + require.NoError(t, err) + require.Empty(t, daemons) + }) + + t.Run("MatchesProvisionerIDs", func(t *testing.T) { + t.Parallel() + db, _ := dbtestutil.NewDB(t) + org := dbgen.Organization(t, db, database.Organization{}) + + matchingDaemon0 := dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{ + Name: "matching-daemon0", + OrganizationID: org.ID, + }) + matchingDaemon1 := dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{ + Name: "matching-daemon1", + OrganizationID: org.ID, + }) + dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{ + Name: "non-matching-daemon", + OrganizationID: org.ID, + }) + + daemons, err := db.GetProvisionerDaemonsWithStatusByOrganization(context.Background(), database.GetProvisionerDaemonsWithStatusByOrganizationParams{ + OrganizationID: org.ID, + IDs: []uuid.UUID{matchingDaemon0.ID, matchingDaemon1.ID}, + }) + require.NoError(t, err) + require.Len(t, daemons, 2) + if daemons[0].ProvisionerDaemon.ID != matchingDaemon0.ID { + daemons[0], daemons[1] = daemons[1], daemons[0] + } + require.Equal(t, matchingDaemon0.ID, daemons[0].ProvisionerDaemon.ID) + require.Equal(t, matchingDaemon1.ID, daemons[1].ProvisionerDaemon.ID) + }) + + t.Run("MatchesTags", func(t *testing.T) { + t.Parallel() + db, _ := dbtestutil.NewDB(t) + org := dbgen.Organization(t, db, database.Organization{}) + + fooDaemon := dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{ + Name: "foo-daemon", + OrganizationID: org.ID, + Tags: database.StringMap{ + "foo": "bar", + }, + }) + dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{ + Name: "baz-daemon", + OrganizationID: org.ID, + Tags: database.StringMap{ + "baz": "qux", + }, + }) + + daemons, err := db.GetProvisionerDaemonsWithStatusByOrganization(context.Background(), database.GetProvisionerDaemonsWithStatusByOrganizationParams{ + OrganizationID: org.ID, + Tags: database.StringMap{"foo": "bar"}, + }) + require.NoError(t, err) + require.Len(t, daemons, 1) + require.Equal(t, fooDaemon.ID, daemons[0].ProvisionerDaemon.ID) + }) + + t.Run("UsesStaleInterval", func(t *testing.T) { + t.Parallel() + db, _ := dbtestutil.NewDB(t) + org := dbgen.Organization(t, db, database.Organization{}) + + daemon1 := dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{ + Name: "stale-daemon", + OrganizationID: org.ID, + CreatedAt: dbtime.Now().Add(-time.Hour), + LastSeenAt: sql.NullTime{ + Valid: true, + Time: dbtime.Now().Add(-time.Hour), + }, + }) + daemon2 := dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{ + Name: "idle-daemon", + OrganizationID: org.ID, + CreatedAt: dbtime.Now().Add(-(30 * time.Minute)), + LastSeenAt: sql.NullTime{ + Valid: true, + Time: dbtime.Now().Add(-(30 * time.Minute)), + }, + }) + + daemons, err := db.GetProvisionerDaemonsWithStatusByOrganization(context.Background(), database.GetProvisionerDaemonsWithStatusByOrganizationParams{ + OrganizationID: org.ID, + StaleIntervalMS: 45 * time.Minute.Milliseconds(), + }) + require.NoError(t, err) + require.Len(t, daemons, 2) + + if daemons[0].ProvisionerDaemon.ID != daemon1.ID { + daemons[0], daemons[1] = daemons[1], daemons[0] + } + require.Equal(t, daemon1.ID, daemons[0].ProvisionerDaemon.ID) + require.Equal(t, daemon2.ID, daemons[1].ProvisionerDaemon.ID) + require.Equal(t, database.ProvisionerDaemonStatusOffline, daemons[0].Status) + require.Equal(t, database.ProvisionerDaemonStatusIdle, daemons[1].Status) + }) +} + func TestGetWorkspaceAgentUsageStats(t *testing.T) { t.Parallel() From 2ad4bb43c4601dedb4e7e3b1e9a4f639c68b8846 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Tue, 14 Jan 2025 14:45:13 +0000 Subject: [PATCH 7/7] remove ent tag --- coderd/apidoc/docs.go | 2 +- coderd/apidoc/swagger.json | 2 +- coderd/provisionerdaemons.go | 6 +- docs/reference/api/enterprise.md | 103 ---------------------------- docs/reference/api/provisioning.md | 104 +++++++++++++++++++++++++++++ 5 files changed, 109 insertions(+), 108 deletions(-) create mode 100644 docs/reference/api/provisioning.md diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 97060dccafb6f..3b319226e043f 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -2963,7 +2963,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Enterprise" + "Provisioning" ], "summary": "Get provisioner daemons", "operationId": "get-provisioner-daemons", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 1e77a05775ac8..c4611109df79b 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -2598,7 +2598,7 @@ } ], "produces": ["application/json"], - "tags": ["Enterprise"], + "tags": ["Provisioning"], "summary": "Get provisioner daemons", "operationId": "get-provisioner-daemons", "parameters": [ diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index 8b0b41cb9de72..30add82e3e287 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -8,6 +8,7 @@ import ( "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/provisionerdserver" + "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" ) @@ -15,7 +16,7 @@ import ( // @ID get-provisioner-daemons // @Security CoderSessionToken // @Produce json -// @Tags Enterprise +// @Tags Provisioning // @Param organization path string true "Organization ID" format(uuid) // @Param tags query object false "Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'})" // @Success 200 {array} codersdk.ProvisionerDaemon @@ -70,9 +71,8 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) { } // Add optional fields. - status := codersdk.ProvisionerDaemonStatus(dbDaemon.Status) pd.KeyName = &dbDaemon.KeyName - pd.Status = &status + pd.Status = ptr.Ref(codersdk.ProvisionerDaemonStatus(dbDaemon.Status)) pd.CurrentJob = currentJob pd.PreviousJob = previousJob diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index bfc53cac53deb..6f8b061ed9025 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -1474,109 +1474,6 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members To perform this operation, you must be authenticated. [Learn more](authentication.md). -## Get provisioner daemons - -### Code samples - -```shell -# Example request using curl -curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisionerdaemons \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`GET /organizations/{organization}/provisionerdaemons` - -### Parameters - -| Name | In | Type | Required | Description | -|----------------|-------|--------------|----------|------------------------------------------------------------------------------------| -| `organization` | path | string(uuid) | true | Organization ID | -| `tags` | query | object | false | Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'}) | - -### Example responses - -> 200 Response - -```json -[ - { - "api_version": "string", - "created_at": "2019-08-24T14:15:22Z", - "current_job": { - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "status": "pending" - }, - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", - "key_name": "string", - "last_seen_at": "2019-08-24T14:15:22Z", - "name": "string", - "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "previous_job": { - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "status": "pending" - }, - "provisioners": [ - "string" - ], - "status": "offline", - "tags": { - "property1": "string", - "property2": "string" - }, - "version": "string" - } -] -``` - -### Responses - -| Status | Meaning | Description | Schema | -|--------|---------------------------------------------------------|-------------|-----------------------------------------------------------------------------| -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.ProvisionerDaemon](schemas.md#codersdkprovisionerdaemon) | - -

Response Schema

- -Status Code **200** - -| Name | Type | Required | Restrictions | Description | -|---------------------|--------------------------------------------------------------------------------|----------|--------------|------------------| -| `[array item]` | array | false | | | -| `» api_version` | string | false | | | -| `» created_at` | string(date-time) | false | | | -| `» current_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | -| `»» id` | string(uuid) | false | | | -| `»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | -| `» id` | string(uuid) | false | | | -| `» key_id` | string(uuid) | false | | | -| `» key_name` | string | false | | Optional fields. | -| `» last_seen_at` | string(date-time) | false | | | -| `» name` | string | false | | | -| `» organization_id` | string(uuid) | false | | | -| `» previous_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | -| `» provisioners` | array | false | | | -| `» status` | [codersdk.ProvisionerDaemonStatus](schemas.md#codersdkprovisionerdaemonstatus) | false | | | -| `» tags` | object | false | | | -| `»» [any property]` | string | false | | | -| `» version` | string | false | | | - -#### Enumerated Values - -| Property | Value | -|----------|-------------| -| `status` | `pending` | -| `status` | `running` | -| `status` | `succeeded` | -| `status` | `canceling` | -| `status` | `canceled` | -| `status` | `failed` | -| `status` | `offline` | -| `status` | `idle` | -| `status` | `busy` | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - ## Serve provisioner daemon ### Code samples diff --git a/docs/reference/api/provisioning.md b/docs/reference/api/provisioning.md new file mode 100644 index 0000000000000..bf3c36269fafa --- /dev/null +++ b/docs/reference/api/provisioning.md @@ -0,0 +1,104 @@ +# Provisioning + +## Get provisioner daemons + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisionerdaemons \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /organizations/{organization}/provisionerdaemons` + +### Parameters + +| Name | In | Type | Required | Description | +|----------------|-------|--------------|----------|------------------------------------------------------------------------------------| +| `organization` | path | string(uuid) | true | Organization ID | +| `tags` | query | object | false | Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'}) | + +### Example responses + +> 200 Response + +```json +[ + { + "api_version": "string", + "created_at": "2019-08-24T14:15:22Z", + "current_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", + "key_name": "string", + "last_seen_at": "2019-08-24T14:15:22Z", + "name": "string", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "previous_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, + "provisioners": [ + "string" + ], + "status": "offline", + "tags": { + "property1": "string", + "property2": "string" + }, + "version": "string" + } +] +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|-----------------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.ProvisionerDaemon](schemas.md#codersdkprovisionerdaemon) | + +

Response Schema

+ +Status Code **200** + +| Name | Type | Required | Restrictions | Description | +|---------------------|--------------------------------------------------------------------------------|----------|--------------|------------------| +| `[array item]` | array | false | | | +| `» api_version` | string | false | | | +| `» created_at` | string(date-time) | false | | | +| `» current_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | +| `»» id` | string(uuid) | false | | | +| `»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | +| `» id` | string(uuid) | false | | | +| `» key_id` | string(uuid) | false | | | +| `» key_name` | string | false | | Optional fields. | +| `» last_seen_at` | string(date-time) | false | | | +| `» name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | +| `» previous_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | +| `» provisioners` | array | false | | | +| `» status` | [codersdk.ProvisionerDaemonStatus](schemas.md#codersdkprovisionerdaemonstatus) | false | | | +| `» tags` | object | false | | | +| `»» [any property]` | string | false | | | +| `» version` | string | false | | | + +#### Enumerated Values + +| Property | Value | +|----------|-------------| +| `status` | `pending` | +| `status` | `running` | +| `status` | `succeeded` | +| `status` | `canceling` | +| `status` | `canceled` | +| `status` | `failed` | +| `status` | `offline` | +| `status` | `idle` | +| `status` | `busy` | + +To perform this operation, you must be authenticated. [Learn more](authentication.md).