mirror of
https://github.com/coder/coder.git
synced 2025-07-21 01:28:49 +00:00
feat(coderd): add endpoint to list provisioner daemons (#16028)
Updates #15190 Updates #15084 Supersedes #15940
This commit is contained in:
committed by
GitHub
parent
d7809ecf3f
commit
071bb26018
61
coderd/apidoc/docs.go
generated
61
coderd/apidoc/docs.go
generated
@ -2963,7 +2963,7 @@ const docTemplate = `{
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
"Provisioning"
|
||||
],
|
||||
"summary": "Get provisioner daemons",
|
||||
"operationId": "get-provisioner-daemons",
|
||||
@ -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": {
|
||||
@ -12499,6 +12521,43 @@ 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.ProvisionerJob": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
53
coderd/apidoc/swagger.json
generated
53
coderd/apidoc/swagger.json
generated
@ -2598,7 +2598,7 @@
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Enterprise"],
|
||||
"tags": ["Provisioning"],
|
||||
"summary": "Get provisioner daemons",
|
||||
"operationId": "get-provisioner-daemons",
|
||||
"parameters": [
|
||||
@ -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": {
|
||||
@ -11280,6 +11298,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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.ProvisionerJob": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -3189,6 +3189,24 @@ 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,
|
||||
StaleIntervalMS: 24 * time.Hour.Milliseconds(),
|
||||
})
|
||||
s.NoError(err, "get provisioner daemon with status by org")
|
||||
check.Args(database.GetProvisionerDaemonsWithStatusByOrganizationParams{
|
||||
OrganizationID: org.ID,
|
||||
StaleIntervalMS: 24 * time.Hour.Milliseconds(),
|
||||
}).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{})
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
8
coderd/database/dump.sql
generated
8
coderd/database/dump.sql
generated
@ -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',
|
||||
|
@ -0,0 +1 @@
|
||||
DROP TYPE provisioner_daemon_status;
|
@ -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.';
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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"
|
||||
|
81
coderd/provisionerdaemons.go
Normal file
81
coderd/provisionerdaemons.go
Normal file
@ -0,0 +1,81 @@
|
||||
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/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// @Summary Get provisioner daemons
|
||||
// @ID get-provisioner-daemons
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @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
|
||||
// @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.ProvisionerDaemon {
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
// Add optional fields.
|
||||
pd.KeyName = &dbDaemon.KeyName
|
||||
pd.Status = ptr.Ref(codersdk.ProvisionerDaemonStatus(dbDaemon.Status))
|
||||
pd.CurrentJob = currentJob
|
||||
pd.PreviousJob = previousJob
|
||||
|
||||
return pd
|
||||
}))
|
||||
}
|
27
coderd/provisionerdaemons_test.go
Normal file
27
coderd/provisionerdaemons_test.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
@ -39,17 +39,38 @@ 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"`
|
||||
|
||||
// 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 {
|
||||
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
|
||||
|
10
docs/reference/api/debug.md
generated
10
docs/reference/api/debug.md
generated
@ -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"
|
||||
|
147
docs/reference/api/enterprise.md
generated
147
docs/reference/api/enterprise.md
generated
@ -1474,79 +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",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"provisioners": [
|
||||
"string"
|
||||
],
|
||||
"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) |
|
||||
|
||||
<h3 id="get-provisioner-daemons-responseschema">Response Schema</h3>
|
||||
|
||||
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 | | |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Serve provisioner daemon
|
||||
|
||||
### Code samples
|
||||
@ -1700,14 +1627,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"
|
||||
@ -1739,28 +1676,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).
|
||||
|
||||
|
104
docs/reference/api/provisioning.md
generated
Normal file
104
docs/reference/api/provisioning.md
generated
Normal file
@ -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) |
|
||||
|
||||
<h3 id="get-provisioner-daemons-responseschema">Response Schema</h3>
|
||||
|
||||
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).
|
131
docs/reference/api/schemas.md
generated
131
docs/reference/api/schemas.md
generated
@ -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,74 @@ 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
|
||||
|
||||
```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.ProvisionerJob
|
||||
|
||||
@ -4518,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"
|
||||
@ -9363,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"
|
||||
@ -9489,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"
|
||||
@ -9546,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"
|
||||
|
@ -440,7 +440,6 @@ 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
|
||||
require.Eventually(t, func() bool {
|
||||
daemons, err = client.OrganizationProvisionerDaemons(ctx, anotherOrg.ID, nil)
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
19
site/src/api/typesGenerated.ts
generated
19
site/src/api/typesGenerated.ts
generated
@ -1532,6 +1532,16 @@ export interface ProvisionerDaemon {
|
||||
readonly api_version: string;
|
||||
readonly provisioners: readonly ProvisionerType[];
|
||||
readonly tags: Record<string, string>;
|
||||
readonly key_name: string | null;
|
||||
readonly status: ProvisionerDaemonStatus | null;
|
||||
readonly current_job: ProvisionerDaemonJob | null;
|
||||
readonly previous_job: ProvisionerDaemonJob | null;
|
||||
}
|
||||
|
||||
// From codersdk/provisionerdaemons.go
|
||||
export interface ProvisionerDaemonJob {
|
||||
readonly id: string;
|
||||
readonly status: ProvisionerJobStatus;
|
||||
}
|
||||
|
||||
// From codersdk/client.go
|
||||
@ -1540,6 +1550,15 @@ 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 healthsdk/healthsdk.go
|
||||
export interface ProvisionerDaemonsReport extends BaseReport {
|
||||
readonly items: readonly ProvisionerDaemonsReportItem[];
|
||||
|
@ -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: [
|
||||
{
|
||||
|
Reference in New Issue
Block a user