diff --git a/Makefile b/Makefile index b69e164317f8d..29f8461f48783 100644 --- a/Makefile +++ b/Makefile @@ -809,7 +809,7 @@ provisioner/terraform/testdata/version: .PHONY: provisioner/terraform/testdata/version test: - $(GIT_FLAGS) gotestsum --format standard-quiet -- -v -short -count=1 ./... + $(GIT_FLAGS) gotestsum --format standard-quiet -- -v -short -count=1 ./... $(if $(RUN),-run $(RUN)) .PHONY: test test-cli: diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index b83dc98c2a2ce..2e48634c7de13 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -5605,6 +5605,44 @@ const docTemplate = `{ } } }, + "/templateversions/{templateversion}/presets": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Templates" + ], + "summary": "Get template version presets", + "operationId": "get-template-version-presets", + "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/codersdk.Preset" + } + } + } + } + } + }, "/templateversions/{templateversion}/resources": { "get": { "security": [ @@ -12967,6 +13005,34 @@ const docTemplate = `{ } } }, + "codersdk.Preset": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.PresetParameter" + } + } + } + }, + "codersdk.PresetParameter": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, "codersdk.PrometheusConfig": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 8c9bf31ce9e8e..0e03555da4720 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -4951,6 +4951,40 @@ } } }, + "/templateversions/{templateversion}/presets": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Templates"], + "summary": "Get template version presets", + "operationId": "get-template-version-presets", + "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/codersdk.Preset" + } + } + } + } + } + }, "/templateversions/{templateversion}/resources": { "get": { "security": [ @@ -11700,6 +11734,34 @@ } } }, + "codersdk.Preset": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.PresetParameter" + } + } + } + }, + "codersdk.PresetParameter": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, "codersdk.PrometheusConfig": { "type": "object", "properties": { diff --git a/coderd/coderd.go b/coderd/coderd.go index d11535f58022d..8ff8c05ee75b2 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1058,6 +1058,7 @@ func New(options *Options) *API { r.Get("/rich-parameters", api.templateVersionRichParameters) r.Get("/external-auth", api.templateVersionExternalAuth) r.Get("/variables", api.templateVersionVariables) + r.Get("/presets", api.templateVersionPresets) r.Get("/resources", api.templateVersionResources) r.Get("/logs", api.templateVersionLogs) r.Route("/dry-run", func(r chi.Router) { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index f7c438f4a55e9..46aa96bf1f7a9 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -886,7 +886,6 @@ func (s *MethodTestSuite) TestOrganization() { JobID: job.ID, }) insertPresetParams := database.InsertPresetParams{ - ID: uuid.New(), TemplateVersionID: workspaceBuild.TemplateVersionID, Name: "test", } @@ -3817,13 +3816,11 @@ func (s *MethodTestSuite) TestSystemFunctions() { CreatedBy: user.ID, }) preset, err := db.InsertPreset(ctx, database.InsertPresetParams{ - ID: uuid.New(), TemplateVersionID: templateVersion.ID, Name: "test", }) require.NoError(s.T(), err) _, err = db.InsertPresetParameters(ctx, database.InsertPresetParametersParams{ - ID: uuid.New(), TemplateVersionPresetID: preset.ID, Names: []string{"test"}, Values: []string{"test"}, diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 8d68054a49444..21c40233718ef 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -8149,6 +8149,7 @@ func (q *FakeQuerier) InsertPreset(_ context.Context, arg database.InsertPresetP q.mutex.Lock() defer q.mutex.Unlock() + //nolint:gosimple // arg needs to keep its type for interface reasons and that type is not appropriate for preset below. preset := database.TemplateVersionPreset{ ID: uuid.New(), TemplateVersionID: arg.TemplateVersionID, diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 7bd2052a2276f..20e7d14b57d01 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1266,14 +1266,14 @@ COMMENT ON COLUMN template_version_parameters.display_order IS 'Specifies the or COMMENT ON COLUMN template_version_parameters.ephemeral IS 'The value of an ephemeral parameter will not be preserved between consecutive workspace builds.'; CREATE TABLE template_version_preset_parameters ( - id uuid NOT NULL, + id uuid DEFAULT gen_random_uuid() NOT NULL, template_version_preset_id uuid NOT NULL, name text NOT NULL, value text NOT NULL ); CREATE TABLE template_version_presets ( - id uuid NOT NULL, + id uuid DEFAULT gen_random_uuid() NOT NULL, template_version_id uuid NOT NULL, name text NOT NULL, created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL diff --git a/coderd/database/migrations/000292_generate_default_preset_parameter_ids.down.sql b/coderd/database/migrations/000292_generate_default_preset_parameter_ids.down.sql new file mode 100644 index 0000000000000..0cb92a2619d22 --- /dev/null +++ b/coderd/database/migrations/000292_generate_default_preset_parameter_ids.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE template_version_presets +ALTER COLUMN id DROP DEFAULT; + +ALTER TABLE template_version_preset_parameters +ALTER COLUMN id DROP DEFAULT; diff --git a/coderd/database/migrations/000292_generate_default_preset_parameter_ids.up.sql b/coderd/database/migrations/000292_generate_default_preset_parameter_ids.up.sql new file mode 100644 index 0000000000000..9801d1f37cdc5 --- /dev/null +++ b/coderd/database/migrations/000292_generate_default_preset_parameter_ids.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE template_version_presets +ALTER COLUMN id SET DEFAULT gen_random_uuid(); + +ALTER TABLE template_version_preset_parameters +ALTER COLUMN id SET DEFAULT gen_random_uuid(); diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index fec7157c5c715..2d7fe83296deb 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5494,25 +5494,19 @@ func (q *sqlQuerier) GetPresetsByTemplateVersionID(ctx context.Context, template const insertPreset = `-- name: InsertPreset :one INSERT INTO - template_version_presets (id, template_version_id, name, created_at) + template_version_presets (template_version_id, name, created_at) VALUES - ($1, $2, $3, $4) RETURNING id, template_version_id, name, created_at + ($1, $2, $3) RETURNING id, template_version_id, name, created_at ` type InsertPresetParams struct { - ID uuid.UUID `db:"id" json:"id"` TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` Name string `db:"name" json:"name"` CreatedAt time.Time `db:"created_at" json:"created_at"` } func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) (TemplateVersionPreset, error) { - row := q.db.QueryRowContext(ctx, insertPreset, - arg.ID, - arg.TemplateVersionID, - arg.Name, - arg.CreatedAt, - ) + row := q.db.QueryRowContext(ctx, insertPreset, arg.TemplateVersionID, arg.Name, arg.CreatedAt) var i TemplateVersionPreset err := row.Scan( &i.ID, @@ -5525,29 +5519,22 @@ func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) ( const insertPresetParameters = `-- name: InsertPresetParameters :many INSERT INTO - template_version_preset_parameters (id, template_version_preset_id, name, value) + template_version_preset_parameters (template_version_preset_id, name, value) SELECT $1, - $2, - unnest($3 :: TEXT[]), - unnest($4 :: TEXT[]) + unnest($2 :: TEXT[]), + unnest($3 :: TEXT[]) RETURNING id, template_version_preset_id, name, value ` type InsertPresetParametersParams struct { - ID uuid.UUID `db:"id" json:"id"` TemplateVersionPresetID uuid.UUID `db:"template_version_preset_id" json:"template_version_preset_id"` Names []string `db:"names" json:"names"` Values []string `db:"values" json:"values"` } func (q *sqlQuerier) InsertPresetParameters(ctx context.Context, arg InsertPresetParametersParams) ([]TemplateVersionPresetParameter, error) { - rows, err := q.db.QueryContext(ctx, insertPresetParameters, - arg.ID, - arg.TemplateVersionPresetID, - pq.Array(arg.Names), - pq.Array(arg.Values), - ) + rows, err := q.db.QueryContext(ctx, insertPresetParameters, arg.TemplateVersionPresetID, pq.Array(arg.Names), pq.Array(arg.Values)) if err != nil { return nil, err } diff --git a/coderd/database/queries/presets.sql b/coderd/database/queries/presets.sql index 6bfe31dc09ceb..8e648fce6ca88 100644 --- a/coderd/database/queries/presets.sql +++ b/coderd/database/queries/presets.sql @@ -1,14 +1,13 @@ -- name: InsertPreset :one INSERT INTO - template_version_presets (id, template_version_id, name, created_at) + template_version_presets (template_version_id, name, created_at) VALUES - (@id, @template_version_id, @name, @created_at) RETURNING *; + (@template_version_id, @name, @created_at) RETURNING *; -- name: InsertPresetParameters :many INSERT INTO - template_version_preset_parameters (id, template_version_preset_id, name, value) + template_version_preset_parameters (template_version_preset_id, name, value) SELECT - @id, @template_version_preset_id, unnest(@names :: TEXT[]), unnest(@values :: TEXT[]) diff --git a/coderd/presets.go b/coderd/presets.go new file mode 100644 index 0000000000000..a2787b63e733d --- /dev/null +++ b/coderd/presets.go @@ -0,0 +1,57 @@ +package coderd + +import ( + "net/http" + + "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/codersdk" +) + +// @Summary Get template version presets +// @ID get-template-version-presets +// @Security CoderSessionToken +// @Produce json +// @Tags Templates +// @Param templateversion path string true "Template version ID" format(uuid) +// @Success 200 {array} codersdk.Preset +// @Router /templateversions/{templateversion}/presets [get] +func (api *API) templateVersionPresets(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + templateVersion := httpmw.TemplateVersionParam(r) + + presets, err := api.Database.GetPresetsByTemplateVersionID(ctx, templateVersion.ID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching template version presets.", + Detail: err.Error(), + }) + return + } + + presetParams, err := api.Database.GetPresetParametersByTemplateVersionID(ctx, templateVersion.ID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching template version presets.", + Detail: err.Error(), + }) + return + } + + var res []codersdk.Preset + for _, preset := range presets { + sdkPreset := codersdk.Preset{ + ID: preset.ID, + Name: preset.Name, + } + for _, presetParam := range presetParams { + sdkPreset.Parameters = append(sdkPreset.Parameters, codersdk.PresetParameter{ + Name: presetParam.Name, + Value: presetParam.Value, + }) + } + res = append(res, sdkPreset) + } + + httpapi.Write(ctx, rw, http.StatusOK, res) +} diff --git a/coderd/presets_test.go b/coderd/presets_test.go new file mode 100644 index 0000000000000..ffe51787d5f5c --- /dev/null +++ b/coderd/presets_test.go @@ -0,0 +1,76 @@ +package coderd_test + +import ( + "testing" + + "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/dbauthz" + "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/testutil" +) + +func TestTemplateVersionPresets(t *testing.T) { + // TODO (sasswart): Test case: what if a user tries to read presets or preset parameters from a different org? + + t.Parallel() + + givenPreset := codersdk.Preset{ + Name: "My Preset", + Parameters: []codersdk.PresetParameter{ + { + Name: "preset_param1", + Value: "A1B2C3", + }, + { + Name: "preset_param2", + Value: "D4E5F6", + }, + }, + } + ctx := testutil.Context(t, testutil.WaitShort) + + client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + + // nolint:gocritic // This is a test + provisionerCtx := dbauthz.AsProvisionerd(ctx) + + dbPreset, err := db.InsertPreset(provisionerCtx, database.InsertPresetParams{ + Name: givenPreset.Name, + TemplateVersionID: version.ID, + }) + require.NoError(t, err) + + var presetParameterNames []string + var presetParameterValues []string + for _, presetParameter := range givenPreset.Parameters { + presetParameterNames = append(presetParameterNames, presetParameter.Name) + presetParameterValues = append(presetParameterValues, presetParameter.Value) + } + _, err = db.InsertPresetParameters(provisionerCtx, database.InsertPresetParametersParams{ + TemplateVersionPresetID: dbPreset.ID, + Names: presetParameterNames, + Values: presetParameterValues, + }) + require.NoError(t, err) + + userSubject, _, err := httpmw.UserRBACSubject(ctx, db, user.UserID, rbac.ScopeAll) + require.NoError(t, err) + userCtx := dbauthz.As(ctx, userSubject) + + gotPresets, err := client.TemplateVersionPresets(userCtx, version.ID) + require.NoError(t, err) + + require.Equal(t, 1, len(gotPresets)) + require.Equal(t, givenPreset.Name, gotPresets[0].Name) + + for _, presetParameter := range givenPreset.Parameters { + require.Contains(t, gotPresets[0].Parameters, presetParameter) + } +} diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go index 183fdf2d447cc..a31e5eff4686a 100644 --- a/coderd/wsbuilder/wsbuilder.go +++ b/coderd/wsbuilder/wsbuilder.go @@ -363,23 +363,20 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object var workspaceBuild database.WorkspaceBuild err = b.store.InTx(func(store database.Store) error { err = store.InsertWorkspaceBuild(b.ctx, database.InsertWorkspaceBuildParams{ - ID: workspaceBuildID, - CreatedAt: now, - UpdatedAt: now, - WorkspaceID: b.workspace.ID, - TemplateVersionID: templateVersionID, - BuildNumber: buildNum, - ProvisionerState: state, - InitiatorID: b.initiator, - Transition: b.trans, - JobID: provisionerJob.ID, - Reason: b.reason, - Deadline: time.Time{}, // set by provisioner upon completion - MaxDeadline: time.Time{}, // set by provisioner upon completion - TemplateVersionPresetID: uuid.NullUUID{ - UUID: uuid.Nil, - Valid: false, - }, + ID: workspaceBuildID, + CreatedAt: now, + UpdatedAt: now, + WorkspaceID: b.workspace.ID, + TemplateVersionID: templateVersionID, + BuildNumber: buildNum, + ProvisionerState: state, + InitiatorID: b.initiator, + Transition: b.trans, + JobID: provisionerJob.ID, + Reason: b.reason, + Deadline: time.Time{}, // set by provisioner upon completion + MaxDeadline: time.Time{}, // set by provisioner upon completion + TemplateVersionPresetID: uuid.NullUUID{}, // TODO (sasswart): add this in from the caller }) if err != nil { code := http.StatusInternalServerError diff --git a/codersdk/presets.go b/codersdk/presets.go new file mode 100644 index 0000000000000..110f6c605f026 --- /dev/null +++ b/codersdk/presets.go @@ -0,0 +1,36 @@ +package codersdk + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/google/uuid" + "golang.org/x/xerrors" +) + +type Preset struct { + ID uuid.UUID + Name string + Parameters []PresetParameter +} + +type PresetParameter struct { + Name string + Value string +} + +// TemplateVersionPresets returns the presets associated with a template version. +func (c *Client) TemplateVersionPresets(ctx context.Context, templateVersionID uuid.UUID) ([]Preset, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s/presets", templateVersionID), nil) + if err != nil { + return nil, xerrors.Errorf("do request: %w", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, ReadBodyAsError(res) + } + var presets []Preset + return presets, json.NewDecoder(res.Body).Decode(&presets) +} diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 93870a2b584e2..7b2759e281f8e 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4427,6 +4427,45 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | `address` | [serpent.HostPort](#serpenthostport) | false | | | | `enable` | boolean | false | | | +## codersdk.Preset + +```json +{ + "id": "string", + "name": "string", + "parameters": [ + { + "name": "string", + "value": "string" + } + ] +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|--------------|---------------------------------------------------------------|----------|--------------|-------------| +| `id` | string | false | | | +| `name` | string | false | | | +| `parameters` | array of [codersdk.PresetParameter](#codersdkpresetparameter) | false | | | + +## codersdk.PresetParameter + +```json +{ + "name": "string", + "value": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|---------|--------|----------|--------------|-------------| +| `name` | string | false | | | +| `value` | string | false | | | + ## codersdk.PrometheusConfig ```json diff --git a/docs/reference/api/templates.md b/docs/reference/api/templates.md index 9a2a35f40f182..ab8b4f1b7c131 100644 --- a/docs/reference/api/templates.md +++ b/docs/reference/api/templates.md @@ -2672,6 +2672,65 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/p To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Get template version presets + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/presets \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /templateversions/{templateversion}/presets` + +### Parameters + +| Name | In | Type | Required | Description | +|-------------------|------|--------------|----------|---------------------| +| `templateversion` | path | string(uuid) | true | Template version ID | + +### Example responses + +> 200 Response + +```json +[ + { + "id": "string", + "name": "string", + "parameters": [ + { + "name": "string", + "value": "string" + } + ] + } +] +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|-------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.Preset](schemas.md#codersdkpreset) | + +

Response Schema

+ +Status Code **200** + +| Name | Type | Required | Restrictions | Description | +|----------------|--------|----------|--------------|-------------| +| `[array item]` | array | false | | | +| `» id` | string | false | | | +| `» name` | string | false | | | +| `» parameters` | array | false | | | +| `»» name` | string | false | | | +| `»» value` | string | false | | | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get resources by template version ### Code samples diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 8a779cca7de3f..09541c9767b89 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1550,6 +1550,19 @@ export interface PprofConfig { readonly address: string; } +// From codersdk/presets.go +export interface Preset { + readonly ID: string; + readonly Name: string; + readonly Parameters: readonly PresetParameter[]; +} + +// From codersdk/presets.go +export interface PresetParameter { + readonly Name: string; + readonly Value: string; +} + // From codersdk/deployment.go export interface PrometheusConfig { readonly enable: boolean;