mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
feat(coderd): add filters and fix template for provisioner daemons (#16558)
This change adds provisioner daemon ID filter to the provisioner daemons endpoint, and also implements the limiting to 50 results. Test coverage is greatly improved and template information for jobs associated to the daemon was also fixed. Updates #15084 Updates #15192 Related #16532
This commit is contained in:
committed by
GitHub
parent
a69961bbd2
commit
77306f3de1
@ -21,7 +21,7 @@
|
||||
"previous_job": {
|
||||
"id": "======[workspace build job ID]======",
|
||||
"status": "succeeded",
|
||||
"template_name": "",
|
||||
"template_name": "test-template",
|
||||
"template_icon": "",
|
||||
"template_display_name": ""
|
||||
},
|
||||
|
37
coderd/apidoc/docs.go
generated
37
coderd/apidoc/docs.go
generated
@ -2976,6 +2976,43 @@ const docTemplate = `{
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Page limit",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"format": "uuid",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Filter results by job IDs",
|
||||
"name": "ids",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"pending",
|
||||
"running",
|
||||
"succeeded",
|
||||
"canceling",
|
||||
"canceled",
|
||||
"failed",
|
||||
"unknown",
|
||||
"pending",
|
||||
"running",
|
||||
"succeeded",
|
||||
"canceling",
|
||||
"canceled",
|
||||
"failed"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Filter results by status",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"description": "Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'})",
|
||||
|
37
coderd/apidoc/swagger.json
generated
37
coderd/apidoc/swagger.json
generated
@ -2610,6 +2610,43 @@
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Page limit",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"format": "uuid",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Filter results by job IDs",
|
||||
"name": "ids",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"pending",
|
||||
"running",
|
||||
"succeeded",
|
||||
"canceling",
|
||||
"canceled",
|
||||
"failed",
|
||||
"unknown",
|
||||
"pending",
|
||||
"running",
|
||||
"succeeded",
|
||||
"canceling",
|
||||
"canceled",
|
||||
"failed"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Filter results by status",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"description": "Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'})",
|
||||
|
@ -3931,7 +3931,7 @@ func (q *FakeQuerier) GetProvisionerDaemonsByOrganization(_ context.Context, arg
|
||||
return daemons, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetProvisionerDaemonsWithStatusByOrganization(_ context.Context, arg database.GetProvisionerDaemonsWithStatusByOrganizationParams) ([]database.GetProvisionerDaemonsWithStatusByOrganizationRow, error) {
|
||||
func (q *FakeQuerier) GetProvisionerDaemonsWithStatusByOrganization(ctx context.Context, arg database.GetProvisionerDaemonsWithStatusByOrganizationParams) ([]database.GetProvisionerDaemonsWithStatusByOrganizationRow, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -3981,6 +3981,31 @@ func (q *FakeQuerier) GetProvisionerDaemonsWithStatusByOrganization(_ context.Co
|
||||
status = database.ProvisionerDaemonStatusIdle
|
||||
}
|
||||
}
|
||||
var currentTemplate database.Template
|
||||
if currentJob.ID != uuid.Nil {
|
||||
var input codersdk.ProvisionerJobInput
|
||||
err := json.Unmarshal(currentJob.Input, &input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if input.WorkspaceBuildID != nil {
|
||||
b, err := q.getWorkspaceBuildByIDNoLock(ctx, *input.WorkspaceBuildID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input.TemplateVersionID = &b.TemplateVersionID
|
||||
}
|
||||
if input.TemplateVersionID != nil {
|
||||
v, err := q.getTemplateVersionByIDNoLock(ctx, *input.TemplateVersionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
currentTemplate, err = q.getTemplateByIDNoLock(ctx, v.TemplateID.UUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var previousJob database.ProvisionerJob
|
||||
for _, job := range q.provisionerJobs {
|
||||
@ -3997,6 +4022,31 @@ func (q *FakeQuerier) GetProvisionerDaemonsWithStatusByOrganization(_ context.Co
|
||||
}
|
||||
}
|
||||
}
|
||||
var previousTemplate database.Template
|
||||
if previousJob.ID != uuid.Nil {
|
||||
var input codersdk.ProvisionerJobInput
|
||||
err := json.Unmarshal(previousJob.Input, &input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if input.WorkspaceBuildID != nil {
|
||||
b, err := q.getWorkspaceBuildByIDNoLock(ctx, *input.WorkspaceBuildID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input.TemplateVersionID = &b.TemplateVersionID
|
||||
}
|
||||
if input.TemplateVersionID != nil {
|
||||
v, err := q.getTemplateVersionByIDNoLock(ctx, *input.TemplateVersionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
previousTemplate, err = q.getTemplateByIDNoLock(ctx, v.TemplateID.UUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the provisioner key name
|
||||
var keyName string
|
||||
@ -4008,13 +4058,19 @@ func (q *FakeQuerier) GetProvisionerDaemonsWithStatusByOrganization(_ context.Co
|
||||
}
|
||||
|
||||
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},
|
||||
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},
|
||||
CurrentJobTemplateName: currentTemplate.Name,
|
||||
CurrentJobTemplateDisplayName: currentTemplate.DisplayName,
|
||||
CurrentJobTemplateIcon: currentTemplate.Icon,
|
||||
PreviousJobID: uuid.NullUUID{UUID: previousJob.ID, Valid: previousJob.ID != uuid.Nil},
|
||||
PreviousJobStatus: database.NullProvisionerJobStatus{ProvisionerJobStatus: previousJob.JobStatus, Valid: previousJob.ID != uuid.Nil},
|
||||
PreviousJobTemplateName: previousTemplate.Name,
|
||||
PreviousJobTemplateDisplayName: previousTemplate.DisplayName,
|
||||
PreviousJobTemplateIcon: previousTemplate.Icon,
|
||||
})
|
||||
}
|
||||
|
||||
@ -4022,6 +4078,10 @@ func (q *FakeQuerier) GetProvisionerDaemonsWithStatusByOrganization(_ context.Co
|
||||
return a.ProvisionerDaemon.CreatedAt.Compare(b.ProvisionerDaemon.CreatedAt)
|
||||
})
|
||||
|
||||
if arg.Limit.Valid && arg.Limit.Int32 > 0 && len(rows) > int(arg.Limit.Int32) {
|
||||
rows = rows[:arg.Limit.Int32]
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
|
@ -208,6 +208,8 @@ 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)
|
||||
// Current job information.
|
||||
// Previous job information.
|
||||
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)
|
||||
|
@ -5743,9 +5743,12 @@ SELECT
|
||||
current_job.job_status AS current_job_status,
|
||||
previous_job.id AS previous_job_id,
|
||||
previous_job.job_status AS previous_job_status,
|
||||
COALESCE(tmpl.name, ''::text) AS current_job_template_name,
|
||||
COALESCE(tmpl.display_name, ''::text) AS current_job_template_display_name,
|
||||
COALESCE(tmpl.icon, ''::text) AS current_job_template_icon
|
||||
COALESCE(current_template.name, ''::text) AS current_job_template_name,
|
||||
COALESCE(current_template.display_name, ''::text) AS current_job_template_display_name,
|
||||
COALESCE(current_template.icon, ''::text) AS current_job_template_icon,
|
||||
COALESCE(previous_template.name, ''::text) AS previous_job_template_name,
|
||||
COALESCE(previous_template.display_name, ''::text) AS previous_job_template_display_name,
|
||||
COALESCE(previous_template.icon, ''::text) AS previous_job_template_icon
|
||||
FROM
|
||||
provisioner_daemons pd
|
||||
JOIN
|
||||
@ -5771,43 +5774,62 @@ LEFT JOIN
|
||||
)
|
||||
)
|
||||
LEFT JOIN
|
||||
template_versions version ON version.id = (current_job.input->>'template_version_id')::uuid
|
||||
workspace_builds current_build ON current_build.id = CASE WHEN current_job.input ? 'workspace_build_id' THEN (current_job.input->>'workspace_build_id')::uuid END
|
||||
LEFT JOIN
|
||||
templates tmpl ON tmpl.id = version.template_id
|
||||
-- We should always have a template version, either explicitly or implicitly via workspace build.
|
||||
template_versions current_version ON current_version.id = CASE WHEN current_job.input ? 'template_version_id' THEN (current_job.input->>'template_version_id')::uuid ELSE current_build.template_version_id END
|
||||
LEFT JOIN
|
||||
templates current_template ON current_template.id = current_version.template_id
|
||||
LEFT JOIN
|
||||
workspace_builds previous_build ON previous_build.id = CASE WHEN previous_job.input ? 'workspace_build_id' THEN (previous_job.input->>'workspace_build_id')::uuid END
|
||||
LEFT JOIN
|
||||
-- We should always have a template version, either explicitly or implicitly via workspace build.
|
||||
template_versions previous_version ON previous_version.id = CASE WHEN previous_job.input ? 'template_version_id' THEN (previous_job.input->>'template_version_id')::uuid ELSE previous_build.template_version_id END
|
||||
LEFT JOIN
|
||||
templates previous_template ON previous_template.id = previous_version.template_id
|
||||
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
|
||||
LIMIT
|
||||
$5::int
|
||||
`
|
||||
|
||||
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"`
|
||||
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"`
|
||||
Limit sql.NullInt32 `db:"limit" json:"limit"`
|
||||
}
|
||||
|
||||
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"`
|
||||
CurrentJobTemplateName string `db:"current_job_template_name" json:"current_job_template_name"`
|
||||
CurrentJobTemplateDisplayName string `db:"current_job_template_display_name" json:"current_job_template_display_name"`
|
||||
CurrentJobTemplateIcon string `db:"current_job_template_icon" json:"current_job_template_icon"`
|
||||
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"`
|
||||
CurrentJobTemplateName string `db:"current_job_template_name" json:"current_job_template_name"`
|
||||
CurrentJobTemplateDisplayName string `db:"current_job_template_display_name" json:"current_job_template_display_name"`
|
||||
CurrentJobTemplateIcon string `db:"current_job_template_icon" json:"current_job_template_icon"`
|
||||
PreviousJobTemplateName string `db:"previous_job_template_name" json:"previous_job_template_name"`
|
||||
PreviousJobTemplateDisplayName string `db:"previous_job_template_display_name" json:"previous_job_template_display_name"`
|
||||
PreviousJobTemplateIcon string `db:"previous_job_template_icon" json:"previous_job_template_icon"`
|
||||
}
|
||||
|
||||
// Current job information.
|
||||
// Previous job information.
|
||||
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,
|
||||
arg.Limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -5837,6 +5859,9 @@ func (q *sqlQuerier) GetProvisionerDaemonsWithStatusByOrganization(ctx context.C
|
||||
&i.CurrentJobTemplateName,
|
||||
&i.CurrentJobTemplateDisplayName,
|
||||
&i.CurrentJobTemplateIcon,
|
||||
&i.PreviousJobTemplateName,
|
||||
&i.PreviousJobTemplateDisplayName,
|
||||
&i.PreviousJobTemplateIcon,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -45,9 +45,12 @@ SELECT
|
||||
current_job.job_status AS current_job_status,
|
||||
previous_job.id AS previous_job_id,
|
||||
previous_job.job_status AS previous_job_status,
|
||||
COALESCE(tmpl.name, ''::text) AS current_job_template_name,
|
||||
COALESCE(tmpl.display_name, ''::text) AS current_job_template_display_name,
|
||||
COALESCE(tmpl.icon, ''::text) AS current_job_template_icon
|
||||
COALESCE(current_template.name, ''::text) AS current_job_template_name,
|
||||
COALESCE(current_template.display_name, ''::text) AS current_job_template_display_name,
|
||||
COALESCE(current_template.icon, ''::text) AS current_job_template_icon,
|
||||
COALESCE(previous_template.name, ''::text) AS previous_job_template_name,
|
||||
COALESCE(previous_template.display_name, ''::text) AS previous_job_template_display_name,
|
||||
COALESCE(previous_template.icon, ''::text) AS previous_job_template_icon
|
||||
FROM
|
||||
provisioner_daemons pd
|
||||
JOIN
|
||||
@ -72,16 +75,30 @@ LEFT JOIN
|
||||
LIMIT 1
|
||||
)
|
||||
)
|
||||
-- Current job information.
|
||||
LEFT JOIN
|
||||
template_versions version ON version.id = (current_job.input->>'template_version_id')::uuid
|
||||
workspace_builds current_build ON current_build.id = CASE WHEN current_job.input ? 'workspace_build_id' THEN (current_job.input->>'workspace_build_id')::uuid END
|
||||
LEFT JOIN
|
||||
templates tmpl ON tmpl.id = version.template_id
|
||||
-- We should always have a template version, either explicitly or implicitly via workspace build.
|
||||
template_versions current_version ON current_version.id = CASE WHEN current_job.input ? 'template_version_id' THEN (current_job.input->>'template_version_id')::uuid ELSE current_build.template_version_id END
|
||||
LEFT JOIN
|
||||
templates current_template ON current_template.id = current_version.template_id
|
||||
-- Previous job information.
|
||||
LEFT JOIN
|
||||
workspace_builds previous_build ON previous_build.id = CASE WHEN previous_job.input ? 'workspace_build_id' THEN (previous_job.input->>'workspace_build_id')::uuid END
|
||||
LEFT JOIN
|
||||
-- We should always have a template version, either explicitly or implicitly via workspace build.
|
||||
template_versions previous_version ON previous_version.id = CASE WHEN previous_job.input ? 'template_version_id' THEN (previous_job.input->>'template_version_id')::uuid ELSE previous_build.template_version_id END
|
||||
LEFT JOIN
|
||||
templates previous_template ON previous_template.id = previous_version.template_id
|
||||
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;
|
||||
pd.created_at ASC
|
||||
LIMIT
|
||||
sqlc.narg('limit')::int;
|
||||
|
||||
-- name: DeleteOldProvisionerDaemons :exec
|
||||
-- Delete provisioner daemons that have been created at least a week ago
|
||||
|
@ -1,6 +1,7 @@
|
||||
package coderd
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
@ -18,31 +19,50 @@ import (
|
||||
// @Produce json
|
||||
// @Tags Provisioning
|
||||
// @Param organization path string true "Organization ID" format(uuid)
|
||||
// @Param limit query int false "Page limit"
|
||||
// @Param ids query []string false "Filter results by job IDs" format(uuid)
|
||||
// @Param status query codersdk.ProvisionerJobStatus false "Filter results by status" enums(pending,running,succeeded,canceling,canceled,failed)
|
||||
// @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))
|
||||
ctx = r.Context()
|
||||
org = httpmw.OrganizationParam(r)
|
||||
)
|
||||
|
||||
if tagParam != "" && err != nil {
|
||||
qp := r.URL.Query()
|
||||
p := httpapi.NewQueryParamParser()
|
||||
limit := p.PositiveInt32(qp, 50, "limit")
|
||||
ids := p.UUIDs(qp, nil, "ids")
|
||||
tagsRaw := p.String(qp, "", "tags")
|
||||
p.ErrorExcessParams(qp)
|
||||
if len(p.Errors) > 0 {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid tags query parameter",
|
||||
Detail: err.Error(),
|
||||
Message: "Invalid query parameters.",
|
||||
Validations: p.Errors,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
tags := database.StringMap{}
|
||||
if tagsRaw != "" {
|
||||
if err := tags.Scan([]byte(tagsRaw)); 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(),
|
||||
Limit: sql.NullInt32{Int32: limit, Valid: limit > 0},
|
||||
IDs: ids,
|
||||
Tags: tags,
|
||||
},
|
||||
)
|
||||
@ -68,8 +88,11 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
if dbDaemon.PreviousJobID.Valid {
|
||||
previousJob = &codersdk.ProvisionerDaemonJob{
|
||||
ID: dbDaemon.PreviousJobID.UUID,
|
||||
Status: codersdk.ProvisionerJobStatus(dbDaemon.PreviousJobStatus.ProvisionerJobStatus),
|
||||
ID: dbDaemon.PreviousJobID.UUID,
|
||||
Status: codersdk.ProvisionerJobStatus(dbDaemon.PreviousJobStatus.ProvisionerJobStatus),
|
||||
TemplateName: dbDaemon.PreviousJobTemplateName,
|
||||
TemplateIcon: dbDaemon.PreviousJobTemplateIcon,
|
||||
TemplateDisplayName: dbDaemon.PreviousJobTemplateDisplayName,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,27 +1,251 @@
|
||||
package coderd_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestGetProvisionerDaemons(t *testing.T) {
|
||||
func TestProvisionerDaemons(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
db, ps := dbtestutil.NewDB(t,
|
||||
dbtestutil.WithDumpOnFailure(),
|
||||
//nolint:gocritic // Use UTC for consistent timestamp length in golden files.
|
||||
dbtestutil.WithTimezone("UTC"),
|
||||
)
|
||||
client, _, coderdAPI := coderdtest.NewWithAPI(t, &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: false,
|
||||
Database: db,
|
||||
Pubsub: ps,
|
||||
})
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
templateAdminClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgTemplateAdmin(owner.OrganizationID))
|
||||
memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
|
||||
// Create initial resources with a running provisioner.
|
||||
firstProvisioner := coderdtest.NewTaggedProvisionerDaemon(t, coderdAPI, "default-provisioner", map[string]string{"owner": "", "scope": "organization"})
|
||||
t.Cleanup(func() { _ = firstProvisioner.Close() })
|
||||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
|
||||
|
||||
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
// Stop the provisioner so it doesn't grab any more jobs.
|
||||
firstProvisioner.Close()
|
||||
|
||||
// Create a provisioner that's working on a job.
|
||||
pd1 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{
|
||||
Name: "provisioner-1",
|
||||
CreatedAt: dbtime.Now().Add(1 * time.Second),
|
||||
LastSeenAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(time.Hour), Valid: true}, // Stale interval can't be adjusted, keep online.
|
||||
KeyID: codersdk.ProvisionerKeyUUIDBuiltIn,
|
||||
Tags: database.StringMap{"owner": "", "scope": "organization", "foo": "bar"},
|
||||
})
|
||||
w1 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{
|
||||
OwnerID: member.ID,
|
||||
TemplateID: template.ID,
|
||||
})
|
||||
wb1ID := uuid.MustParse("00000000-0000-0000-dddd-000000000001")
|
||||
job1 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{
|
||||
WorkerID: uuid.NullUUID{UUID: pd1.ID, Valid: true},
|
||||
Input: json.RawMessage(`{"workspace_build_id":"` + wb1ID.String() + `"}`),
|
||||
CreatedAt: dbtime.Now().Add(2 * time.Second),
|
||||
StartedAt: sql.NullTime{Time: coderdAPI.Clock.Now(), Valid: true},
|
||||
Tags: database.StringMap{"owner": "", "scope": "organization", "foo": "bar"},
|
||||
})
|
||||
dbgen.WorkspaceBuild(t, coderdAPI.Database, database.WorkspaceBuild{
|
||||
ID: wb1ID,
|
||||
JobID: job1.ID,
|
||||
WorkspaceID: w1.ID,
|
||||
TemplateVersionID: version.ID,
|
||||
})
|
||||
|
||||
// Create a provisioner that completed a job previously and is offline.
|
||||
pd2 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{
|
||||
Name: "provisioner-2",
|
||||
CreatedAt: dbtime.Now().Add(2 * time.Second),
|
||||
LastSeenAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(-time.Hour), Valid: true},
|
||||
KeyID: codersdk.ProvisionerKeyUUIDBuiltIn,
|
||||
Tags: database.StringMap{"owner": "", "scope": "organization"},
|
||||
})
|
||||
w2 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{
|
||||
OwnerID: member.ID,
|
||||
TemplateID: template.ID,
|
||||
})
|
||||
wb2ID := uuid.MustParse("00000000-0000-0000-dddd-000000000002")
|
||||
job2 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{
|
||||
WorkerID: uuid.NullUUID{UUID: pd2.ID, Valid: true},
|
||||
Input: json.RawMessage(`{"workspace_build_id":"` + wb2ID.String() + `"}`),
|
||||
CreatedAt: dbtime.Now().Add(3 * time.Second),
|
||||
StartedAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(-2 * time.Hour), Valid: true},
|
||||
CompletedAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(-time.Hour), Valid: true},
|
||||
Tags: database.StringMap{"owner": "", "scope": "organization"},
|
||||
})
|
||||
dbgen.WorkspaceBuild(t, coderdAPI.Database, database.WorkspaceBuild{
|
||||
ID: wb2ID,
|
||||
JobID: job2.ID,
|
||||
WorkspaceID: w2.ID,
|
||||
TemplateVersionID: version.ID,
|
||||
})
|
||||
|
||||
// Create a pending job.
|
||||
w3 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{
|
||||
OwnerID: member.ID,
|
||||
TemplateID: template.ID,
|
||||
})
|
||||
wb3ID := uuid.MustParse("00000000-0000-0000-dddd-000000000003")
|
||||
job3 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{
|
||||
Input: json.RawMessage(`{"workspace_build_id":"` + wb3ID.String() + `"}`),
|
||||
CreatedAt: dbtime.Now().Add(4 * time.Second),
|
||||
Tags: database.StringMap{"owner": "", "scope": "organization"},
|
||||
})
|
||||
dbgen.WorkspaceBuild(t, coderdAPI.Database, database.WorkspaceBuild{
|
||||
ID: wb3ID,
|
||||
JobID: job3.ID,
|
||||
WorkspaceID: w3.ID,
|
||||
TemplateVersionID: version.ID,
|
||||
})
|
||||
|
||||
// Create a provisioner that is idle.
|
||||
pd3 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{
|
||||
Name: "provisioner-3",
|
||||
CreatedAt: dbtime.Now().Add(3 * time.Second),
|
||||
LastSeenAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(time.Hour), Valid: true},
|
||||
KeyID: codersdk.ProvisionerKeyUUIDBuiltIn,
|
||||
Tags: database.StringMap{"owner": "", "scope": "organization"},
|
||||
})
|
||||
|
||||
// Add more provisioners than the default limit.
|
||||
var userDaemons []database.ProvisionerDaemon
|
||||
for i := range 50 {
|
||||
userDaemons = append(userDaemons, dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{
|
||||
Name: "user-provisioner-" + strconv.Itoa(i),
|
||||
CreatedAt: dbtime.Now().Add(3 * time.Second),
|
||||
KeyID: codersdk.ProvisionerKeyUUIDUserAuth,
|
||||
Tags: database.StringMap{"count": strconv.Itoa(i)},
|
||||
}))
|
||||
}
|
||||
|
||||
t.Run("Default limit", 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 := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 50)
|
||||
})
|
||||
|
||||
daemons, err := memberClient.ProvisionerDaemons(ctx)
|
||||
t.Run("IDs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{
|
||||
IDs: []uuid.UUID{pd1.ID, pd2.ID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 2)
|
||||
require.Equal(t, pd1.ID, daemons[0].ID)
|
||||
require.Equal(t, pd2.ID, daemons[1].ID)
|
||||
})
|
||||
|
||||
t.Run("Tags", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{
|
||||
Tags: map[string]string{"count": "1"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 1)
|
||||
require.Equal(t, userDaemons[1].ID, daemons[0].ID)
|
||||
})
|
||||
|
||||
t.Run("Limit", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{
|
||||
Limit: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 1)
|
||||
})
|
||||
|
||||
t.Run("Busy", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{
|
||||
IDs: []uuid.UUID{pd1.ID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 1)
|
||||
// Verify status.
|
||||
require.NotNil(t, daemons[0].Status)
|
||||
require.Equal(t, codersdk.ProvisionerDaemonBusy, *daemons[0].Status)
|
||||
require.NotNil(t, daemons[0].CurrentJob)
|
||||
require.Nil(t, daemons[0].PreviousJob)
|
||||
// Verify job.
|
||||
require.Equal(t, job1.ID, daemons[0].CurrentJob.ID)
|
||||
require.Equal(t, codersdk.ProvisionerJobRunning, daemons[0].CurrentJob.Status)
|
||||
require.Equal(t, template.Name, daemons[0].CurrentJob.TemplateName)
|
||||
require.Equal(t, template.DisplayName, daemons[0].CurrentJob.TemplateDisplayName)
|
||||
require.Equal(t, template.Icon, daemons[0].CurrentJob.TemplateIcon)
|
||||
})
|
||||
|
||||
t.Run("Offline", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{
|
||||
IDs: []uuid.UUID{pd2.ID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 1)
|
||||
// Verify status.
|
||||
require.NotNil(t, daemons[0].Status)
|
||||
require.Equal(t, codersdk.ProvisionerDaemonOffline, *daemons[0].Status)
|
||||
require.Nil(t, daemons[0].CurrentJob)
|
||||
require.NotNil(t, daemons[0].PreviousJob)
|
||||
// Verify job.
|
||||
require.Equal(t, job2.ID, daemons[0].PreviousJob.ID)
|
||||
require.Equal(t, codersdk.ProvisionerJobSucceeded, daemons[0].PreviousJob.Status)
|
||||
require.Equal(t, template.Name, daemons[0].PreviousJob.TemplateName)
|
||||
require.Equal(t, template.DisplayName, daemons[0].PreviousJob.TemplateDisplayName)
|
||||
require.Equal(t, template.Icon, daemons[0].PreviousJob.TemplateIcon)
|
||||
})
|
||||
|
||||
t.Run("Idle", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{
|
||||
IDs: []uuid.UUID{pd3.ID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 1)
|
||||
// Verify status.
|
||||
require.NotNil(t, daemons[0].Status)
|
||||
require.Equal(t, codersdk.ProvisionerDaemonIdle, *daemons[0].Status)
|
||||
require.Nil(t, daemons[0].CurrentJob)
|
||||
require.Nil(t, daemons[0].PreviousJob)
|
||||
})
|
||||
|
||||
t.Run("MemberAllowed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
daemons, err := memberClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 50)
|
||||
})
|
||||
}
|
||||
|
@ -316,21 +316,34 @@ func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, e
|
||||
return daemons, json.NewDecoder(res.Body).Decode(&daemons)
|
||||
}
|
||||
|
||||
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())
|
||||
type OrganizationProvisionerDaemonsOptions struct {
|
||||
Limit int
|
||||
IDs []uuid.UUID
|
||||
Tags map[string]string
|
||||
}
|
||||
|
||||
queryParams := url.Values{}
|
||||
tagsJSON, err := json.Marshal(tags)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("marshal tags: %w", err)
|
||||
func (c *Client) OrganizationProvisionerDaemons(ctx context.Context, organizationID uuid.UUID, opts *OrganizationProvisionerDaemonsOptions) ([]ProvisionerDaemon, error) {
|
||||
qp := url.Values{}
|
||||
if opts != nil {
|
||||
if opts.Limit > 0 {
|
||||
qp.Add("limit", strconv.Itoa(opts.Limit))
|
||||
}
|
||||
if len(opts.IDs) > 0 {
|
||||
qp.Add("ids", joinSliceStringer(opts.IDs))
|
||||
}
|
||||
if len(opts.Tags) > 0 {
|
||||
tagsRaw, err := json.Marshal(opts.Tags)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("marshal tags: %w", err)
|
||||
}
|
||||
qp.Add("tags", string(tagsRaw))
|
||||
}
|
||||
}
|
||||
|
||||
queryParams.Add("tags", string(tagsJSON))
|
||||
if len(queryParams) > 0 {
|
||||
baseURL = fmt.Sprintf("%s?%s", baseURL, queryParams.Encode())
|
||||
}
|
||||
|
||||
res, err := c.Request(ctx, http.MethodGet, baseURL, nil)
|
||||
res, err := c.Request(ctx, http.MethodGet,
|
||||
fmt.Sprintf("/api/v2/organizations/%s/provisionerdaemons?%s", organizationID.String(), qp.Encode()),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("execute request: %w", err)
|
||||
}
|
||||
|
21
docs/reference/api/provisioning.md
generated
21
docs/reference/api/provisioning.md
generated
@ -18,8 +18,29 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi
|
||||
| Name | In | Type | Required | Description |
|
||||
|----------------|-------|--------------|----------|------------------------------------------------------------------------------------|
|
||||
| `organization` | path | string(uuid) | true | Organization ID |
|
||||
| `limit` | query | integer | false | Page limit |
|
||||
| `ids` | query | array(uuid) | false | Filter results by job IDs |
|
||||
| `status` | query | string | false | Filter results by status |
|
||||
| `tags` | query | object | false | Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'}) |
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Parameter | Value |
|
||||
|-----------|-------------|
|
||||
| `status` | `pending` |
|
||||
| `status` | `running` |
|
||||
| `status` | `succeeded` |
|
||||
| `status` | `canceling` |
|
||||
| `status` | `canceled` |
|
||||
| `status` | `failed` |
|
||||
| `status` | `unknown` |
|
||||
| `status` | `pending` |
|
||||
| `status` | `running` |
|
||||
| `status` | `succeeded` |
|
||||
| `status` | `canceling` |
|
||||
| `status` | `canceled` |
|
||||
| `status` | `failed` |
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
@ -990,7 +990,9 @@ func TestGetProvisionerDaemons(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, allDaemons, 1)
|
||||
|
||||
daemonsAsFound, err := orgAdmin.OrganizationProvisionerDaemons(ctx, org.ID, tt.tagsToFilterBy)
|
||||
daemonsAsFound, err := orgAdmin.OrganizationProvisionerDaemons(ctx, org.ID, &codersdk.OrganizationProvisionerDaemonsOptions{
|
||||
Tags: tt.tagsToFilterBy,
|
||||
})
|
||||
if tt.expectToGetDaemon {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemonsAsFound, 1)
|
||||
|
7
site/src/api/typesGenerated.ts
generated
7
site/src/api/typesGenerated.ts
generated
@ -1435,6 +1435,13 @@ export interface OrganizationMemberWithUserData extends OrganizationMember {
|
||||
readonly global_roles: readonly SlimRole[];
|
||||
}
|
||||
|
||||
// From codersdk/organizations.go
|
||||
export interface OrganizationProvisionerDaemonsOptions {
|
||||
readonly Limit: number;
|
||||
readonly IDs: readonly string[];
|
||||
readonly Tags: Record<string, string>;
|
||||
}
|
||||
|
||||
// From codersdk/organizations.go
|
||||
export interface OrganizationProvisionerJobsOptions {
|
||||
readonly Limit: number;
|
||||
|
Reference in New Issue
Block a user