feat: Add basic support for rich parameters to coderd and provisionerd (#5710)

This commit is contained in:
Marcin Tojek
2023-01-17 11:22:11 +01:00
committed by GitHub
parent 70fd78673d
commit 6ebadabe4e
39 changed files with 2592 additions and 800 deletions

38
coderd/apidoc/docs.go generated
View File

@ -2516,6 +2516,44 @@ const docTemplate = `{
}
}
},
"/templateversions/{templateversion}/rich-parameters": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": [
"application/json"
],
"tags": [
"Templates"
],
"summary": "Get rich parameters by template version",
"operationId": "get-rich-parameters-by-template-version",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Template version ID",
"name": "templateversion",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/parameter.ComputedValue"
}
}
}
}
}
},
"/templateversions/{templateversion}/schema": {
"get": {
"security": [

View File

@ -2216,6 +2216,40 @@
}
}
},
"/templateversions/{templateversion}/rich-parameters": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": ["application/json"],
"tags": ["Templates"],
"summary": "Get rich parameters by template version",
"operationId": "get-rich-parameters-by-template-version",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Template version ID",
"name": "templateversion",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/parameter.ComputedValue"
}
}
}
}
}
},
"/templateversions/{templateversion}/schema": {
"get": {
"security": [

View File

@ -435,6 +435,7 @@ func New(options *Options) *API {
r.Patch("/cancel", api.patchCancelTemplateVersion)
r.Get("/schema", api.templateVersionSchema)
r.Get("/parameters", api.templateVersionParameters)
r.Get("/rich-parameters", api.templateVersionRichParameters)
r.Get("/resources", api.templateVersionResources)
r.Get("/logs", api.templateVersionLogs)
r.Route("/dry-run", func(r chi.Router) {

View File

@ -177,6 +177,10 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
AssertAction: rbac.ActionRead,
AssertObject: rbac.ResourceTemplate.InOrg(a.Template.OrganizationID),
},
"GET:/api/v2/templateversions/{templateversion}/rich-parameters": {
AssertAction: rbac.ActionRead,
AssertObject: rbac.ResourceTemplate.InOrg(a.Template.OrganizationID),
},
"GET:/api/v2/templateversions/{templateversion}/resources": {
AssertAction: rbac.ActionRead,
AssertObject: rbac.ResourceTemplate.InOrg(a.Template.OrganizationID),

View File

@ -111,10 +111,12 @@ type data struct {
provisionerJobs []database.ProvisionerJob
replicas []database.Replica
templateVersions []database.TemplateVersion
templateVersionParameters []database.TemplateVersionParameter
templates []database.Template
workspaceAgents []database.WorkspaceAgent
workspaceApps []database.WorkspaceApp
workspaceBuilds []database.WorkspaceBuild
workspaceBuildParameters []database.WorkspaceBuildParameter
workspaceResourceMetadata []database.WorkspaceResourceMetadatum
workspaceResources []database.WorkspaceResource
workspaces []database.Workspace
@ -1365,6 +1367,20 @@ func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(_ context.Con
return database.WorkspaceBuild{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetWorkspaceBuildParameters(_ context.Context, workspaceBuildID uuid.UUID) ([]database.WorkspaceBuildParameter, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
params := make([]database.WorkspaceBuildParameter, 0)
for _, param := range params {
if param.WorkspaceBuildID != workspaceBuildID {
continue
}
params = append(params, param)
}
return params, nil
}
func (q *fakeQuerier) GetWorkspaceBuildsCreatedAfter(_ context.Context, after time.Time) ([]database.WorkspaceBuild, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@ -1656,6 +1672,20 @@ func (q *fakeQuerier) GetTemplateVersionByTemplateIDAndName(_ context.Context, a
return database.TemplateVersion{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetTemplateVersionParameters(_ context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionParameter, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
parameters := make([]database.TemplateVersionParameter, 0)
for _, param := range q.templateVersionParameters {
if param.TemplateVersionID != templateVersionID {
continue
}
parameters = append(parameters, param)
}
return parameters, nil
}
func (q *fakeQuerier) GetTemplateVersionByOrganizationAndName(_ context.Context, arg database.GetTemplateVersionByOrganizationAndNameParams) (database.TemplateVersion, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@ -2398,6 +2428,28 @@ func (q *fakeQuerier) InsertTemplateVersion(_ context.Context, arg database.Inse
return version, nil
}
func (q *fakeQuerier) InsertTemplateVersionParameter(_ context.Context, arg database.InsertTemplateVersionParameterParams) (database.TemplateVersionParameter, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
//nolint:gosimple
param := database.TemplateVersionParameter{
TemplateVersionID: arg.TemplateVersionID,
Name: arg.Name,
Description: arg.Description,
Type: arg.Type,
Mutable: arg.Mutable,
DefaultValue: arg.DefaultValue,
Icon: arg.Icon,
Options: arg.Options,
ValidationRegex: arg.ValidationRegex,
ValidationMin: arg.ValidationMin,
ValidationMax: arg.ValidationMax,
}
q.templateVersionParameters = append(q.templateVersionParameters, param)
return param, nil
}
func (q *fakeQuerier) InsertProvisionerJobLogs(_ context.Context, arg database.InsertProvisionerJobLogsParams) ([]database.ProvisionerJobLog, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
@ -2723,6 +2775,20 @@ func (q *fakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.Inser
return workspaceBuild, nil
}
func (q *fakeQuerier) InsertWorkspaceBuildParameters(_ context.Context, arg database.InsertWorkspaceBuildParametersParams) error {
q.mutex.Lock()
defer q.mutex.Unlock()
for index, name := range arg.Name {
q.workspaceBuildParameters = append(q.workspaceBuildParameters, database.WorkspaceBuildParameter{
WorkspaceBuildID: arg.WorkspaceBuildID,
Name: name,
Value: arg.Value[index],
})
}
return nil
}
func (q *fakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertWorkspaceAppParams) (database.WorkspaceApp, error) {
q.mutex.Lock()
defer q.mutex.Unlock()

View File

@ -331,6 +331,40 @@ CREATE TABLE site_configs (
value character varying(8192) NOT NULL
);
CREATE TABLE template_version_parameters (
template_version_id uuid NOT NULL,
name text NOT NULL,
description text NOT NULL,
type text NOT NULL,
mutable boolean NOT NULL,
default_value text NOT NULL,
icon text NOT NULL,
options jsonb DEFAULT '[]'::jsonb NOT NULL,
validation_regex text NOT NULL,
validation_min integer NOT NULL,
validation_max integer NOT NULL
);
COMMENT ON COLUMN template_version_parameters.name IS 'Parameter name';
COMMENT ON COLUMN template_version_parameters.description IS 'Parameter description';
COMMENT ON COLUMN template_version_parameters.type IS 'Parameter type';
COMMENT ON COLUMN template_version_parameters.mutable IS 'Is parameter mutable?';
COMMENT ON COLUMN template_version_parameters.default_value IS 'Default value';
COMMENT ON COLUMN template_version_parameters.icon IS 'Icon';
COMMENT ON COLUMN template_version_parameters.options IS 'Additional options';
COMMENT ON COLUMN template_version_parameters.validation_regex IS 'Validation: regex pattern';
COMMENT ON COLUMN template_version_parameters.validation_min IS 'Validation: minimum length of value';
COMMENT ON COLUMN template_version_parameters.validation_max IS 'Validation: maximum length of value';
CREATE TABLE template_versions (
id uuid NOT NULL,
template_id uuid,
@ -443,6 +477,16 @@ CREATE TABLE workspace_apps (
external boolean DEFAULT false NOT NULL
);
CREATE TABLE workspace_build_parameters (
workspace_build_id uuid NOT NULL,
name text NOT NULL,
value text NOT NULL
);
COMMENT ON COLUMN workspace_build_parameters.name IS 'Parameter name';
COMMENT ON COLUMN workspace_build_parameters.value IS 'Parameter value';
CREATE TABLE workspace_builds (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
@ -578,6 +622,9 @@ ALTER TABLE ONLY provisioner_jobs
ALTER TABLE ONLY site_configs
ADD CONSTRAINT site_configs_key_key UNIQUE (key);
ALTER TABLE ONLY template_version_parameters
ADD CONSTRAINT template_version_parameters_template_version_id_name_key UNIQUE (template_version_id, name);
ALTER TABLE ONLY template_versions
ADD CONSTRAINT template_versions_pkey PRIMARY KEY (id);
@ -602,6 +649,9 @@ ALTER TABLE ONLY workspace_apps
ALTER TABLE ONLY workspace_apps
ADD CONSTRAINT workspace_apps_pkey PRIMARY KEY (id);
ALTER TABLE ONLY workspace_build_parameters
ADD CONSTRAINT workspace_build_parameters_workspace_build_id_name_key UNIQUE (workspace_build_id, name);
ALTER TABLE ONLY workspace_builds
ADD CONSTRAINT workspace_builds_job_id_key UNIQUE (job_id);
@ -697,6 +747,9 @@ ALTER TABLE ONLY provisioner_job_logs
ALTER TABLE ONLY provisioner_jobs
ADD CONSTRAINT provisioner_jobs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
ALTER TABLE ONLY template_version_parameters
ADD CONSTRAINT template_version_parameters_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE;
ALTER TABLE ONLY template_versions
ADD CONSTRAINT template_versions_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT;
@ -721,6 +774,9 @@ ALTER TABLE ONLY workspace_agents
ALTER TABLE ONLY workspace_apps
ADD CONSTRAINT workspace_apps_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_build_parameters
ADD CONSTRAINT workspace_build_parameters_workspace_build_id_fkey FOREIGN KEY (workspace_build_id) REFERENCES workspace_builds(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_builds
ADD CONSTRAINT workspace_builds_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE;

View File

@ -0,0 +1,3 @@
DROP TABLE template_version_parameters;
DROP TABLE workspace_build_parameters;

View File

@ -0,0 +1,35 @@
CREATE TABLE IF NOT EXISTS template_version_parameters (
template_version_id uuid not null references template_versions (id) on delete cascade,
name text not null,
description text not null,
type text not null,
mutable boolean not null,
default_value text not null,
icon text not null,
options jsonb not null default '[]'::jsonb,
validation_regex text not null,
validation_min integer not null,
validation_max integer not null,
unique (template_version_id, name)
);
COMMENT ON COLUMN template_version_parameters.name IS 'Parameter name';
COMMENT ON COLUMN template_version_parameters.description IS 'Parameter description';
COMMENT ON COLUMN template_version_parameters.type IS 'Parameter type';
COMMENT ON COLUMN template_version_parameters.mutable IS 'Is parameter mutable?';
COMMENT ON COLUMN template_version_parameters.default_value IS 'Default value';
COMMENT ON COLUMN template_version_parameters.icon IS 'Icon';
COMMENT ON COLUMN template_version_parameters.options IS 'Additional options';
COMMENT ON COLUMN template_version_parameters.validation_regex IS 'Validation: regex pattern';
COMMENT ON COLUMN template_version_parameters.validation_min IS 'Validation: minimum length of value';
COMMENT ON COLUMN template_version_parameters.validation_max IS 'Validation: maximum length of value';
CREATE TABLE IF NOT EXISTS workspace_build_parameters (
workspace_build_id uuid not null references workspace_builds (id) on delete cascade,
name text not null,
value text not null,
unique (workspace_build_id, name)
);
COMMENT ON COLUMN workspace_build_parameters.name IS 'Parameter name';
COMMENT ON COLUMN workspace_build_parameters.value IS 'Parameter value';

View File

@ -239,6 +239,8 @@ func TestMigrateUpWithFixtures(t *testing.T) {
"group_members",
"licenses",
"replicas",
"template_version_parameters",
"workspace_build_parameters",
}
s := &tableStats{s: make(map[string]int)}

View File

@ -612,6 +612,30 @@ type TemplateVersion struct {
CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
}
type TemplateVersionParameter struct {
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
// Parameter name
Name string `db:"name" json:"name"`
// Parameter description
Description string `db:"description" json:"description"`
// Parameter type
Type string `db:"type" json:"type"`
// Is parameter mutable?
Mutable bool `db:"mutable" json:"mutable"`
// Default value
DefaultValue string `db:"default_value" json:"default_value"`
// Icon
Icon string `db:"icon" json:"icon"`
// Additional options
Options json.RawMessage `db:"options" json:"options"`
// Validation: regex pattern
ValidationRegex string `db:"validation_regex" json:"validation_regex"`
// Validation: minimum length of value
ValidationMin int32 `db:"validation_min" json:"validation_min"`
// Validation: maximum length of value
ValidationMax int32 `db:"validation_max" json:"validation_max"`
}
type User struct {
ID uuid.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
@ -713,6 +737,14 @@ type WorkspaceBuild struct {
DailyCost int32 `db:"daily_cost" json:"daily_cost"`
}
type WorkspaceBuildParameter struct {
WorkspaceBuildID uuid.UUID `db:"workspace_build_id" json:"workspace_build_id"`
// Parameter name
Name string `db:"name" json:"name"`
// Parameter value
Value string `db:"value" json:"value"`
}
type WorkspaceResource struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`

View File

@ -87,6 +87,7 @@ type sqlcQuerier interface {
GetTemplateVersionByJobID(ctx context.Context, jobID uuid.UUID) (TemplateVersion, error)
GetTemplateVersionByOrganizationAndName(ctx context.Context, arg GetTemplateVersionByOrganizationAndNameParams) (TemplateVersion, error)
GetTemplateVersionByTemplateIDAndName(ctx context.Context, arg GetTemplateVersionByTemplateIDAndNameParams) (TemplateVersion, error)
GetTemplateVersionParameters(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionParameter, error)
GetTemplateVersionsByIDs(ctx context.Context, ids []uuid.UUID) ([]TemplateVersion, error)
GetTemplateVersionsByTemplateID(ctx context.Context, arg GetTemplateVersionsByTemplateIDParams) ([]TemplateVersion, error)
GetTemplateVersionsCreatedAfter(ctx context.Context, createdAt time.Time) ([]TemplateVersion, error)
@ -117,6 +118,7 @@ type sqlcQuerier interface {
GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error)
GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error)
GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error)
GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]WorkspaceBuildParameter, error)
GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuild, error)
GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error)
GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUID) (Workspace, error)
@ -159,12 +161,14 @@ type sqlcQuerier interface {
InsertReplica(ctx context.Context, arg InsertReplicaParams) (Replica, error)
InsertTemplate(ctx context.Context, arg InsertTemplateParams) (Template, error)
InsertTemplateVersion(ctx context.Context, arg InsertTemplateVersionParams) (TemplateVersion, error)
InsertTemplateVersionParameter(ctx context.Context, arg InsertTemplateVersionParameterParams) (TemplateVersionParameter, error)
InsertUser(ctx context.Context, arg InsertUserParams) (User, error)
InsertUserLink(ctx context.Context, arg InsertUserLinkParams) (UserLink, error)
InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (Workspace, error)
InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error)
InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error)
InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) (WorkspaceBuild, error)
InsertWorkspaceBuildParameters(ctx context.Context, arg InsertWorkspaceBuildParametersParams) error
InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error)
InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error)
ParameterValue(ctx context.Context, id uuid.UUID) (ParameterValue, error)

View File

@ -3542,6 +3542,121 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl
return i, err
}
const getTemplateVersionParameters = `-- name: GetTemplateVersionParameters :many
SELECT template_version_id, name, description, type, mutable, default_value, icon, options, validation_regex, validation_min, validation_max FROM template_version_parameters WHERE template_version_id = $1
`
func (q *sqlQuerier) GetTemplateVersionParameters(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionParameter, error) {
rows, err := q.db.QueryContext(ctx, getTemplateVersionParameters, templateVersionID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TemplateVersionParameter
for rows.Next() {
var i TemplateVersionParameter
if err := rows.Scan(
&i.TemplateVersionID,
&i.Name,
&i.Description,
&i.Type,
&i.Mutable,
&i.DefaultValue,
&i.Icon,
&i.Options,
&i.ValidationRegex,
&i.ValidationMin,
&i.ValidationMax,
); 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 insertTemplateVersionParameter = `-- name: InsertTemplateVersionParameter :one
INSERT INTO
template_version_parameters (
template_version_id,
name,
description,
type,
mutable,
default_value,
icon,
options,
validation_regex,
validation_min,
validation_max
)
VALUES
(
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11
) RETURNING template_version_id, name, description, type, mutable, default_value, icon, options, validation_regex, validation_min, validation_max
`
type InsertTemplateVersionParameterParams struct {
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
Name string `db:"name" json:"name"`
Description string `db:"description" json:"description"`
Type string `db:"type" json:"type"`
Mutable bool `db:"mutable" json:"mutable"`
DefaultValue string `db:"default_value" json:"default_value"`
Icon string `db:"icon" json:"icon"`
Options json.RawMessage `db:"options" json:"options"`
ValidationRegex string `db:"validation_regex" json:"validation_regex"`
ValidationMin int32 `db:"validation_min" json:"validation_min"`
ValidationMax int32 `db:"validation_max" json:"validation_max"`
}
func (q *sqlQuerier) InsertTemplateVersionParameter(ctx context.Context, arg InsertTemplateVersionParameterParams) (TemplateVersionParameter, error) {
row := q.db.QueryRowContext(ctx, insertTemplateVersionParameter,
arg.TemplateVersionID,
arg.Name,
arg.Description,
arg.Type,
arg.Mutable,
arg.DefaultValue,
arg.Icon,
arg.Options,
arg.ValidationRegex,
arg.ValidationMin,
arg.ValidationMax,
)
var i TemplateVersionParameter
err := row.Scan(
&i.TemplateVersionID,
&i.Name,
&i.Description,
&i.Type,
&i.Mutable,
&i.DefaultValue,
&i.Icon,
&i.Options,
&i.ValidationRegex,
&i.ValidationMin,
&i.ValidationMax,
)
return i, err
}
const getPreviousTemplateVersion = `-- name: GetPreviousTemplateVersion :one
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by
@ -5384,6 +5499,59 @@ func (q *sqlQuerier) UpdateWorkspaceAppHealthByID(ctx context.Context, arg Updat
return err
}
const getWorkspaceBuildParameters = `-- name: GetWorkspaceBuildParameters :many
SELECT
workspace_build_id, name, value
FROM
workspace_build_parameters
WHERE
workspace_build_id = $1
`
func (q *sqlQuerier) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]WorkspaceBuildParameter, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceBuildParameters, workspaceBuildID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceBuildParameter
for rows.Next() {
var i WorkspaceBuildParameter
if err := rows.Scan(&i.WorkspaceBuildID, &i.Name, &i.Value); 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 insertWorkspaceBuildParameters = `-- name: InsertWorkspaceBuildParameters :exec
INSERT INTO
workspace_build_parameters (workspace_build_id, name, value)
SELECT
$1 :: uuid AS workspace_build_id,
unnest($2 :: text[]) AS name,
unnest($3 :: text[]) AS value
RETURNING workspace_build_id, name, value
`
type InsertWorkspaceBuildParametersParams struct {
WorkspaceBuildID uuid.UUID `db:"workspace_build_id" json:"workspace_build_id"`
Name []string `db:"name" json:"name"`
Value []string `db:"value" json:"value"`
}
func (q *sqlQuerier) InsertWorkspaceBuildParameters(ctx context.Context, arg InsertWorkspaceBuildParametersParams) error {
_, err := q.db.ExecContext(ctx, insertWorkspaceBuildParameters, arg.WorkspaceBuildID, pq.Array(arg.Name), pq.Array(arg.Value))
return err
}
const getLatestWorkspaceBuildByWorkspaceID = `-- name: GetLatestWorkspaceBuildByWorkspaceID :one
SELECT
id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost

View File

@ -0,0 +1,32 @@
-- name: InsertTemplateVersionParameter :one
INSERT INTO
template_version_parameters (
template_version_id,
name,
description,
type,
mutable,
default_value,
icon,
options,
validation_regex,
validation_min,
validation_max
)
VALUES
(
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11
) RETURNING *;
-- name: GetTemplateVersionParameters :many
SELECT * FROM template_version_parameters WHERE template_version_id = $1;

View File

@ -0,0 +1,16 @@
-- name: InsertWorkspaceBuildParameters :exec
INSERT INTO
workspace_build_parameters (workspace_build_id, name, value)
SELECT
@workspace_build_id :: uuid AS workspace_build_id,
unnest(@name :: text[]) AS name,
unnest(@value :: text[]) AS value
RETURNING *;
-- name: GetWorkspaceBuildParameters :many
SELECT
*
FROM
workspace_build_parameters
WHERE
workspace_build_id = $1;

View File

@ -6,26 +6,28 @@ type UniqueConstraint string
// UniqueConstraint enums.
const (
UniqueFilesHashCreatedByKey UniqueConstraint = "files_hash_created_by_key" // ALTER TABLE ONLY files ADD CONSTRAINT files_hash_created_by_key UNIQUE (hash, created_by);
UniqueGitAuthLinksProviderIDUserIDKey UniqueConstraint = "git_auth_links_provider_id_user_id_key" // ALTER TABLE ONLY git_auth_links ADD CONSTRAINT git_auth_links_provider_id_user_id_key UNIQUE (provider_id, user_id);
UniqueGroupMembersUserIDGroupIDKey UniqueConstraint = "group_members_user_id_group_id_key" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_user_id_group_id_key UNIQUE (user_id, group_id);
UniqueGroupsNameOrganizationIDKey UniqueConstraint = "groups_name_organization_id_key" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_name_organization_id_key UNIQUE (name, organization_id);
UniqueLicensesJWTKey UniqueConstraint = "licenses_jwt_key" // ALTER TABLE ONLY licenses ADD CONSTRAINT licenses_jwt_key UNIQUE (jwt);
UniqueParameterSchemasJobIDNameKey UniqueConstraint = "parameter_schemas_job_id_name_key" // ALTER TABLE ONLY parameter_schemas ADD CONSTRAINT parameter_schemas_job_id_name_key UNIQUE (job_id, name);
UniqueParameterValuesScopeIDNameKey UniqueConstraint = "parameter_values_scope_id_name_key" // ALTER TABLE ONLY parameter_values ADD CONSTRAINT parameter_values_scope_id_name_key UNIQUE (scope_id, name);
UniqueProvisionerDaemonsNameKey UniqueConstraint = "provisioner_daemons_name_key" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_name_key UNIQUE (name);
UniqueSiteConfigsKeyKey UniqueConstraint = "site_configs_key_key" // ALTER TABLE ONLY site_configs ADD CONSTRAINT site_configs_key_key UNIQUE (key);
UniqueTemplateVersionsTemplateIDNameKey UniqueConstraint = "template_versions_template_id_name_key" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_template_id_name_key UNIQUE (template_id, name);
UniqueWorkspaceAppsAgentIDSlugIndex UniqueConstraint = "workspace_apps_agent_id_slug_idx" // ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_agent_id_slug_idx UNIQUE (agent_id, slug);
UniqueWorkspaceBuildsJobIDKey UniqueConstraint = "workspace_builds_job_id_key" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_job_id_key UNIQUE (job_id);
UniqueWorkspaceBuildsWorkspaceIDBuildNumberKey UniqueConstraint = "workspace_builds_workspace_id_build_number_key" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_workspace_id_build_number_key UNIQUE (workspace_id, build_number);
UniqueWorkspaceResourceMetadataName UniqueConstraint = "workspace_resource_metadata_name" // ALTER TABLE ONLY workspace_resource_metadata ADD CONSTRAINT workspace_resource_metadata_name UNIQUE (workspace_resource_id, key);
UniqueIndexOrganizationName UniqueConstraint = "idx_organization_name" // CREATE UNIQUE INDEX idx_organization_name ON organizations USING btree (name);
UniqueIndexOrganizationNameLower UniqueConstraint = "idx_organization_name_lower" // CREATE UNIQUE INDEX idx_organization_name_lower ON organizations USING btree (lower(name));
UniqueIndexUsersEmail UniqueConstraint = "idx_users_email" // CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE (deleted = false);
UniqueIndexUsersUsername UniqueConstraint = "idx_users_username" // CREATE UNIQUE INDEX idx_users_username ON users USING btree (username) WHERE (deleted = false);
UniqueTemplatesOrganizationIDNameIndex UniqueConstraint = "templates_organization_id_name_idx" // CREATE UNIQUE INDEX templates_organization_id_name_idx ON templates USING btree (organization_id, lower((name)::text)) WHERE (deleted = false);
UniqueUsersEmailLowerIndex UniqueConstraint = "users_email_lower_idx" // CREATE UNIQUE INDEX users_email_lower_idx ON users USING btree (lower(email)) WHERE (deleted = false);
UniqueUsersUsernameLowerIndex UniqueConstraint = "users_username_lower_idx" // CREATE UNIQUE INDEX users_username_lower_idx ON users USING btree (lower(username)) WHERE (deleted = false);
UniqueWorkspacesOwnerIDLowerIndex UniqueConstraint = "workspaces_owner_id_lower_idx" // CREATE UNIQUE INDEX workspaces_owner_id_lower_idx ON workspaces USING btree (owner_id, lower((name)::text)) WHERE (deleted = false);
UniqueFilesHashCreatedByKey UniqueConstraint = "files_hash_created_by_key" // ALTER TABLE ONLY files ADD CONSTRAINT files_hash_created_by_key UNIQUE (hash, created_by);
UniqueGitAuthLinksProviderIDUserIDKey UniqueConstraint = "git_auth_links_provider_id_user_id_key" // ALTER TABLE ONLY git_auth_links ADD CONSTRAINT git_auth_links_provider_id_user_id_key UNIQUE (provider_id, user_id);
UniqueGroupMembersUserIDGroupIDKey UniqueConstraint = "group_members_user_id_group_id_key" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_user_id_group_id_key UNIQUE (user_id, group_id);
UniqueGroupsNameOrganizationIDKey UniqueConstraint = "groups_name_organization_id_key" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_name_organization_id_key UNIQUE (name, organization_id);
UniqueLicensesJWTKey UniqueConstraint = "licenses_jwt_key" // ALTER TABLE ONLY licenses ADD CONSTRAINT licenses_jwt_key UNIQUE (jwt);
UniqueParameterSchemasJobIDNameKey UniqueConstraint = "parameter_schemas_job_id_name_key" // ALTER TABLE ONLY parameter_schemas ADD CONSTRAINT parameter_schemas_job_id_name_key UNIQUE (job_id, name);
UniqueParameterValuesScopeIDNameKey UniqueConstraint = "parameter_values_scope_id_name_key" // ALTER TABLE ONLY parameter_values ADD CONSTRAINT parameter_values_scope_id_name_key UNIQUE (scope_id, name);
UniqueProvisionerDaemonsNameKey UniqueConstraint = "provisioner_daemons_name_key" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_name_key UNIQUE (name);
UniqueSiteConfigsKeyKey UniqueConstraint = "site_configs_key_key" // ALTER TABLE ONLY site_configs ADD CONSTRAINT site_configs_key_key UNIQUE (key);
UniqueTemplateVersionParametersTemplateVersionIDNameKey UniqueConstraint = "template_version_parameters_template_version_id_name_key" // ALTER TABLE ONLY template_version_parameters ADD CONSTRAINT template_version_parameters_template_version_id_name_key UNIQUE (template_version_id, name);
UniqueTemplateVersionsTemplateIDNameKey UniqueConstraint = "template_versions_template_id_name_key" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_template_id_name_key UNIQUE (template_id, name);
UniqueWorkspaceAppsAgentIDSlugIndex UniqueConstraint = "workspace_apps_agent_id_slug_idx" // ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_agent_id_slug_idx UNIQUE (agent_id, slug);
UniqueWorkspaceBuildParametersWorkspaceBuildIDNameKey UniqueConstraint = "workspace_build_parameters_workspace_build_id_name_key" // ALTER TABLE ONLY workspace_build_parameters ADD CONSTRAINT workspace_build_parameters_workspace_build_id_name_key UNIQUE (workspace_build_id, name);
UniqueWorkspaceBuildsJobIDKey UniqueConstraint = "workspace_builds_job_id_key" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_job_id_key UNIQUE (job_id);
UniqueWorkspaceBuildsWorkspaceIDBuildNumberKey UniqueConstraint = "workspace_builds_workspace_id_build_number_key" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_workspace_id_build_number_key UNIQUE (workspace_id, build_number);
UniqueWorkspaceResourceMetadataName UniqueConstraint = "workspace_resource_metadata_name" // ALTER TABLE ONLY workspace_resource_metadata ADD CONSTRAINT workspace_resource_metadata_name UNIQUE (workspace_resource_id, key);
UniqueIndexOrganizationName UniqueConstraint = "idx_organization_name" // CREATE UNIQUE INDEX idx_organization_name ON organizations USING btree (name);
UniqueIndexOrganizationNameLower UniqueConstraint = "idx_organization_name_lower" // CREATE UNIQUE INDEX idx_organization_name_lower ON organizations USING btree (lower(name));
UniqueIndexUsersEmail UniqueConstraint = "idx_users_email" // CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE (deleted = false);
UniqueIndexUsersUsername UniqueConstraint = "idx_users_username" // CREATE UNIQUE INDEX idx_users_username ON users USING btree (username) WHERE (deleted = false);
UniqueTemplatesOrganizationIDNameIndex UniqueConstraint = "templates_organization_id_name_idx" // CREATE UNIQUE INDEX templates_organization_id_name_idx ON templates USING btree (organization_id, lower((name)::text)) WHERE (deleted = false);
UniqueUsersEmailLowerIndex UniqueConstraint = "users_email_lower_idx" // CREATE UNIQUE INDEX users_email_lower_idx ON users USING btree (lower(email)) WHERE (deleted = false);
UniqueUsersUsernameLowerIndex UniqueConstraint = "users_username_lower_idx" // CREATE UNIQUE INDEX users_username_lower_idx ON users USING btree (lower(username)) WHERE (deleted = false);
UniqueWorkspacesOwnerIDLowerIndex UniqueConstraint = "workspaces_owner_id_lower_idx" // CREATE UNIQUE INDEX workspaces_owner_id_lower_idx ON workspaces USING btree (owner_id, lower((name)::text)) WHERE (deleted = false);
)

View File

@ -181,12 +181,18 @@ func (server *Server) AcquireJob(ctx context.Context, _ *proto.Empty) (*proto.Ac
return nil, failJob(fmt.Sprintf("convert workspace transition: %s", err))
}
workspaceBuildParameters, err := server.Database.GetWorkspaceBuildParameters(ctx, workspaceBuild.ID)
if err != nil {
return nil, failJob(fmt.Sprintf("get workspace build parameters: %s", err))
}
protoJob.Type = &proto.AcquiredJob_WorkspaceBuild_{
WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{
WorkspaceBuildId: workspaceBuild.ID.String(),
WorkspaceName: workspace.Name,
State: workspaceBuild.ProvisionerState,
ParameterValues: protoParameters,
WorkspaceBuildId: workspaceBuild.ID.String(),
WorkspaceName: workspace.Name,
State: workspaceBuild.ProvisionerState,
ParameterValues: protoParameters,
RichParameterValues: convertRichParameterValues(workspaceBuildParameters),
Metadata: &sdkproto.Provision_Metadata{
CoderUrl: server.AccessURL.String(),
WorkspaceTransition: transition,
@ -597,6 +603,12 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete
switch jobType := completed.Type.(type) {
case *proto.CompletedJob_TemplateImport_:
var input TemplateVersionImportJob
err = json.Unmarshal(job.Input, &input)
if err != nil {
return nil, xerrors.Errorf("template version ID is expected: %w", err)
}
for transition, resources := range map[database.WorkspaceTransition][]*sdkproto.Resource{
database.WorkspaceTransitionStart: jobType.TemplateImport.StartResources,
database.WorkspaceTransitionStop: jobType.TemplateImport.StopResources,
@ -615,6 +627,33 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete
}
}
for _, richParameter := range jobType.TemplateImport.RichParameters {
server.Logger.Info(ctx, "inserting template import job parameter",
slog.F("job_id", job.ID.String()),
slog.F("parameter_name", richParameter.Name),
)
options, err := json.Marshal(richParameter.Options)
if err != nil {
return nil, xerrors.Errorf("marshal parameter options: %w", err)
}
_, err = server.Database.InsertTemplateVersionParameter(ctx, database.InsertTemplateVersionParameterParams{
TemplateVersionID: input.TemplateVersionID,
Name: richParameter.Name,
Description: richParameter.Description,
Type: richParameter.Type,
Mutable: richParameter.Mutable,
DefaultValue: richParameter.DefaultValue,
Icon: richParameter.Icon,
Options: options,
ValidationRegex: richParameter.ValidationRegex,
ValidationMin: richParameter.ValidationMin,
ValidationMax: richParameter.ValidationMax,
})
if err != nil {
return nil, xerrors.Errorf("insert parameter: %w", err)
}
}
err = server.Database.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{
ID: jobID,
UpdatedAt: database.Now(),
@ -1056,6 +1095,17 @@ func convertLogSource(logSource proto.LogSource) (database.LogSource, error) {
}
}
func convertRichParameterValues(workspaceBuildParameters []database.WorkspaceBuildParameter) []*sdkproto.RichParameterValue {
protoParameters := make([]*sdkproto.RichParameterValue, len(workspaceBuildParameters))
for i, buildParameter := range workspaceBuildParameters {
protoParameters[i] = &sdkproto.RichParameterValue{
Name: buildParameter.Name,
Value: buildParameter.Value,
}
}
return protoParameters
}
func convertComputedParameterValues(parameters []parameter.ComputedValue) ([]*sdkproto.ParameterValue, error) {
protoParameters := make([]*sdkproto.ParameterValue, len(parameters))
for i, computedParameter := range parameters {
@ -1113,6 +1163,10 @@ func auditActionFromTransition(transition database.WorkspaceTransition) database
}
}
type TemplateVersionImportJob struct {
TemplateVersionID uuid.UUID `json:"template_version_id"`
}
// WorkspaceProvisionJob is the payload for the "workspace_provision" job type.
type WorkspaceProvisionJob struct {
WorkspaceBuildID uuid.UUID `json:"workspace_build_id"`

View File

@ -580,6 +580,7 @@ func TestCompleteJob(t *testing.T) {
job, err := srv.Database.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
ID: uuid.New(),
Provisioner: database.ProvisionerTypeEcho,
Input: []byte(`{"template_version_id": "` + uuid.NewString() + `"}`),
})
require.NoError(t, err)
_, err = srv.Database.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{

View File

@ -26,6 +26,7 @@ import (
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/examples"
sdkproto "github.com/coder/coder/provisionersdk/proto"
)
// @Summary Get template version by ID
@ -192,6 +193,59 @@ func (api *API) templateVersionSchema(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, apiSchemas)
}
// @Summary Get rich parameters by template version
// @ID get-rich-parameters-by-template-version
// @Security CoderSessionToken
// @Produce json
// @Tags Templates
// @Param templateversion path string true "Template version ID" format(uuid)
// @Success 200 {array} parameter.ComputedValue
// @Router /templateversions/{templateversion}/rich-parameters [get]
func (api *API) templateVersionRichParameters(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
templateVersion := httpmw.TemplateVersionParam(r)
template := httpmw.TemplateParam(r)
if !api.Authorize(r, rbac.ActionRead, templateVersion.RBACObject(template)) {
httpapi.ResourceNotFound(rw)
return
}
job, err := api.Database.GetProvisionerJobByID(ctx, templateVersion.JobID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching provisioner job.",
Detail: err.Error(),
})
return
}
if !job.CompletedAt.Valid {
httpapi.Write(ctx, rw, http.StatusPreconditionFailed, codersdk.Response{
Message: "Job hasn't completed!",
})
return
}
dbParameters, err := api.Database.GetTemplateVersionParameters(ctx, templateVersion.ID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching template version parameters.",
Detail: err.Error(),
})
return
}
params := make([]codersdk.TemplateVersionParameter, 0)
for _, dbParameter := range dbParameters {
param, err := convertTemplateVersionParameter(dbParameter)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error converting template version parameter.",
Detail: err.Error(),
})
return
}
params = append(params, param)
}
httpapi.Write(ctx, rw, http.StatusOK, params)
}
// @Summary Get parameters by template version
// @ID get-parameters-by-template-version
// @Security CoderSessionToken
@ -1142,6 +1196,14 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
}
}
templateVersionID := uuid.New()
jobInput, err := json.Marshal(provisionerdserver.TemplateVersionImportJob{
TemplateVersionID: templateVersionID,
})
if err != nil {
return xerrors.Errorf("marshal job input: %w", err)
}
provisionerJob, err = tx.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
ID: jobID,
CreatedAt: database.Now(),
@ -1152,7 +1214,7 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
StorageMethod: database.ProvisionerStorageMethodFile,
FileID: file.ID,
Type: database.ProvisionerJobTypeTemplateVersionImport,
Input: []byte{'{', '}'},
Input: jobInput,
Tags: tags,
})
if err != nil {
@ -1172,7 +1234,7 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
}
templateVersion, err = tx.InsertTemplateVersion(ctx, database.InsertTemplateVersionParams{
ID: uuid.New(),
ID: templateVersionID,
TemplateID: templateID,
OrganizationID: organization.ID,
CreatedAt: database.Now(),
@ -1307,6 +1369,35 @@ func convertTemplateVersion(version database.TemplateVersion, job codersdk.Provi
}
}
func convertTemplateVersionParameter(param database.TemplateVersionParameter) (codersdk.TemplateVersionParameter, error) {
var protoOptions []*sdkproto.RichParameterOption
err := json.Unmarshal(param.Options, &protoOptions)
if err != nil {
return codersdk.TemplateVersionParameter{}, err
}
options := make([]codersdk.TemplateVersionParameterOption, 0)
for _, option := range protoOptions {
options = append(options, codersdk.TemplateVersionParameterOption{
Name: option.Name,
Description: option.Description,
Value: option.Value,
Icon: option.Icon,
})
}
return codersdk.TemplateVersionParameter{
Name: param.Name,
Description: param.Description,
Type: param.Type,
Mutable: param.Mutable,
DefaultValue: param.DefaultValue,
Icon: param.Icon,
Options: options,
ValidationRegex: param.ValidationRegex,
ValidationMin: param.ValidationMin,
ValidationMax: param.ValidationMax,
}, nil
}
func watchTemplateChannel(id uuid.UUID) string {
return fmt.Sprintf("template:%s", id)
}

View File

@ -632,3 +632,56 @@ func TestWorkspaceBuildStatus(t *testing.T) {
require.NoError(t, err)
require.EqualValues(t, codersdk.WorkspaceStatusDeleted, workspace.LatestBuild.Status)
}
func TestWorkspaceBuildWithRichParameters(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Provision_Response{
{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Parameters: []*proto.RichParameter{
{
Name: "first_parameter",
Description: "This is first parameter",
},
{
Name: "second_parameter",
Description: "This is second parameter",
},
},
},
},
}},
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
richParameters, err := client.TemplateVersionRichParameters(ctx, version.ID)
require.NoError(t, err)
require.Len(t, richParameters, 2)
require.Equal(t, richParameters[0].Name, "first_parameter")
require.Equal(t, richParameters[1].Name, "second_parameter")
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.RichParameterValues = []codersdk.WorkspaceBuildParameter{
{
Name: "first_parameter",
Value: "1",
},
{
Name: "second_parameter",
Value: "2",
},
}
})
_, err = client.WorkspaceBuild(ctx, workspace.LatestBuild.ID)
require.NoError(t, err)
}