diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index aa5b75f0a1616..e0f64e1804540 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -2255,6 +2255,51 @@ const docTemplate = `{ } } } + }, + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Templates" + ], + "summary": "Patch template version by ID", + "operationId": "patch-template-version-by-id", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Template version ID", + "name": "templateversion", + "in": "path", + "required": true + }, + { + "description": "Patch template version request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchTemplateVersionRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.TemplateVersion" + } + } + } } }, "/templateversions/{templateversion}/cancel": { @@ -7400,6 +7445,14 @@ const docTemplate = `{ "ParameterSourceSchemeData" ] }, + "codersdk.PatchTemplateVersionRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, "codersdk.PprofConfig": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 6401630d38c4a..f2461cf940f92 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1971,6 +1971,45 @@ } } } + }, + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Templates"], + "summary": "Patch template version by ID", + "operationId": "patch-template-version-by-id", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Template version ID", + "name": "templateversion", + "in": "path", + "required": true + }, + { + "description": "Patch template version request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchTemplateVersionRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.TemplateVersion" + } + } + } } }, "/templateversions/{templateversion}/cancel": { @@ -6620,6 +6659,14 @@ "ParameterSourceSchemeData" ] }, + "codersdk.PatchTemplateVersionRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, "codersdk.PprofConfig": { "type": "object", "properties": { diff --git a/coderd/coderd.go b/coderd/coderd.go index a5c7b127d04b0..b386d85a11975 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -500,6 +500,7 @@ func New(options *Options) *API { httpmw.ExtractTemplateVersionParam(options.Database), ) r.Get("/", api.templateVersion) + r.Patch("/", api.patchTemplateVersion) r.Patch("/cancel", api.patchCancelTemplateVersion) r.Get("/schema", api.templateVersionSchema) r.Get("/parameters", api.templateVersionParameters) diff --git a/coderd/database/dbauthz/querier.go b/coderd/database/dbauthz/querier.go index 867bfa36383d1..42b4cb87e81d6 100644 --- a/coderd/database/dbauthz/querier.go +++ b/coderd/database/dbauthz/querier.go @@ -850,13 +850,13 @@ func (q *querier) UpdateTemplateScheduleByID(ctx context.Context, arg database.U return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateTemplateScheduleByID)(ctx, arg) } -func (q *querier) UpdateTemplateVersionByID(ctx context.Context, arg database.UpdateTemplateVersionByIDParams) error { +func (q *querier) UpdateTemplateVersionByID(ctx context.Context, arg database.UpdateTemplateVersionByIDParams) (database.TemplateVersion, error) { template, err := q.db.GetTemplateByID(ctx, arg.TemplateID.UUID) if err != nil { - return err + return database.TemplateVersion{}, err } if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil { - return err + return database.TemplateVersion{}, err } return q.db.UpdateTemplateVersionByID(ctx, arg) } diff --git a/coderd/database/dbauthz/querier_test.go b/coderd/database/dbauthz/querier_test.go index 41c27f37ad426..bde5d4bb42d01 100644 --- a/coderd/database/dbauthz/querier_test.go +++ b/coderd/database/dbauthz/querier_test.go @@ -721,7 +721,9 @@ func (s *MethodTestSuite) TestTemplate() { check.Args(database.UpdateTemplateVersionByIDParams{ ID: tv.ID, TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true}, - }).Asserts(t1, rbac.ActionUpdate).Returns() + Name: tv.Name, + UpdatedAt: tv.UpdatedAt, + }).Asserts(t1, rbac.ActionUpdate).Returns(tv) })) s.Run("UpdateTemplateVersionDescriptionByJobID", s.Subtest(func(db database.Store, check *expects) { jobID := uuid.New() diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go index f77b19f81d9e1..cfe67a729076f 100644 --- a/coderd/database/dbfake/databasefake.go +++ b/coderd/database/dbfake/databasefake.go @@ -3413,9 +3413,9 @@ func (q *fakeQuerier) UpdateTemplateACLByID(_ context.Context, arg database.Upda return database.Template{}, sql.ErrNoRows } -func (q *fakeQuerier) UpdateTemplateVersionByID(_ context.Context, arg database.UpdateTemplateVersionByIDParams) error { +func (q *fakeQuerier) UpdateTemplateVersionByID(_ context.Context, arg database.UpdateTemplateVersionByIDParams) (database.TemplateVersion, error) { if err := validateDatabaseType(arg); err != nil { - return err + return database.TemplateVersion{}, err } q.mutex.Lock() @@ -3427,10 +3427,11 @@ func (q *fakeQuerier) UpdateTemplateVersionByID(_ context.Context, arg database. } templateVersion.TemplateID = arg.TemplateID templateVersion.UpdatedAt = arg.UpdatedAt + templateVersion.Name = arg.Name q.templateVersions[index] = templateVersion - return nil + return templateVersion, nil } - return sql.ErrNoRows + return database.TemplateVersion{}, sql.ErrNoRows } func (q *fakeQuerier) UpdateTemplateVersionDescriptionByJobID(_ context.Context, arg database.UpdateTemplateVersionDescriptionByJobIDParams) error { diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 174e5fcec1575..77ce2395d8424 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -210,7 +210,7 @@ type sqlcQuerier interface { UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTemplateDeletedByIDParams) error UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) (Template, error) UpdateTemplateScheduleByID(ctx context.Context, arg UpdateTemplateScheduleByIDParams) (Template, error) - UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) error + UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) (TemplateVersion, error) UpdateTemplateVersionDescriptionByJobID(ctx context.Context, arg UpdateTemplateVersionDescriptionByJobIDParams) error UpdateTemplateVersionGitAuthProvidersByJobID(ctx context.Context, arg UpdateTemplateVersionGitAuthProvidersByJobIDParams) error UpdateUserDeletedByID(ctx context.Context, arg UpdateUserDeletedByIDParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 7d5539ba5b369..1ff1cb7c7c6cc 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -4095,25 +4095,45 @@ func (q *sqlQuerier) InsertTemplateVersion(ctx context.Context, arg InsertTempla return i, err } -const updateTemplateVersionByID = `-- name: UpdateTemplateVersionByID :exec +const updateTemplateVersionByID = `-- name: UpdateTemplateVersionByID :one UPDATE template_versions SET template_id = $2, - updated_at = $3 + updated_at = $3, + name = $4 WHERE - id = $1 + id = $1 RETURNING id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, git_auth_providers ` type UpdateTemplateVersionByIDParams struct { ID uuid.UUID `db:"id" json:"id"` TemplateID uuid.NullUUID `db:"template_id" json:"template_id"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + Name string `db:"name" json:"name"` } -func (q *sqlQuerier) UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) error { - _, err := q.db.ExecContext(ctx, updateTemplateVersionByID, arg.ID, arg.TemplateID, arg.UpdatedAt) - return err +func (q *sqlQuerier) UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) (TemplateVersion, error) { + row := q.db.QueryRowContext(ctx, updateTemplateVersionByID, + arg.ID, + arg.TemplateID, + arg.UpdatedAt, + arg.Name, + ) + var i TemplateVersion + err := row.Scan( + &i.ID, + &i.TemplateID, + &i.OrganizationID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.Readme, + &i.JobID, + &i.CreatedBy, + pq.Array(&i.GitAuthProviders), + ) + return i, err } const updateTemplateVersionDescriptionByJobID = `-- name: UpdateTemplateVersionDescriptionByJobID :exec diff --git a/coderd/database/queries/templateversions.sql b/coderd/database/queries/templateversions.sql index e4efc7f46ab90..89f56ef617bcd 100644 --- a/coderd/database/queries/templateversions.sql +++ b/coderd/database/queries/templateversions.sql @@ -84,14 +84,15 @@ INSERT INTO VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *; --- name: UpdateTemplateVersionByID :exec +-- name: UpdateTemplateVersionByID :one UPDATE template_versions SET template_id = $2, - updated_at = $3 + updated_at = $3, + name = $4 WHERE - id = $1; + id = $1 RETURNING *; -- name: UpdateTemplateVersionDescriptionByJobID :exec UPDATE diff --git a/coderd/templates.go b/coderd/templates.go index 9bdc591cc6da1..ac28814eda9dc 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -269,13 +269,14 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque templateAudit.New = dbTemplate - err = tx.UpdateTemplateVersionByID(ctx, database.UpdateTemplateVersionByIDParams{ + _, err = tx.UpdateTemplateVersionByID(ctx, database.UpdateTemplateVersionByIDParams{ ID: templateVersion.ID, TemplateID: uuid.NullUUID{ UUID: dbTemplate.ID, Valid: true, }, UpdatedAt: database.Now(), + Name: templateVersion.Name, }) if err != nil { return xerrors.Errorf("insert template version: %s", err) diff --git a/coderd/templateversions.go b/coderd/templateversions.go index 59f42c8150df1..023e185907349 100644 --- a/coderd/templateversions.go +++ b/coderd/templateversions.go @@ -63,6 +63,66 @@ func (api *API) templateVersion(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(job), user)) } +// @Summary Patch template version by ID +// @ID patch-template-version-by-id +// @Security CoderSessionToken +// @Accept json +// @Produce json +// @Tags Templates +// @Param templateversion path string true "Template version ID" format(uuid) +// @Param request body codersdk.PatchTemplateVersionRequest true "Patch template version request" +// @Success 200 {object} codersdk.TemplateVersion +// @Router /templateversions/{templateversion} [patch] +func (api *API) patchTemplateVersion(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + templateVersion := httpmw.TemplateVersionParam(r) + + var params codersdk.PatchTemplateVersionRequest + if !httpapi.Read(ctx, rw, r, ¶ms) { + return + } + + updateParams := database.UpdateTemplateVersionByIDParams{ + ID: templateVersion.ID, + TemplateID: templateVersion.TemplateID, + UpdatedAt: database.Now(), + Name: templateVersion.Name, + } + + if params.Name != "" { + updateParams.Name = params.Name + } + // It is not allowed to "patch" the template ID, and reassign it. + updatedTemplateVersion, err := api.Database.UpdateTemplateVersionByID(ctx, updateParams) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Error on patching template version.", + Detail: err.Error(), + }) + 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 + } + + user, err := api.Database.GetUserByID(ctx, templateVersion.CreatedBy) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error on fetching user.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(updatedTemplateVersion, convertProvisionerJob(job), user)) +} + // @Summary Cancel template version by ID // @ID cancel-template-version-by-id // @Security CoderSessionToken diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index 176c338f5ed1c..6e3742df23c1b 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -1334,3 +1334,68 @@ func TestTemplateVersionVariables(t *testing.T) { require.Equal(t, "*redacted*", actualVariables[0].Value) }) } + +func TestTemplateVersionPatch(t *testing.T) { + t.Parallel() + t.Run("Update the name", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + const newName = "new_name" + updatedVersion, err := client.UpdateTemplateVersion(ctx, version.ID, codersdk.PatchTemplateVersionRequest{ + Name: newName, + }) + + require.NoError(t, err) + assert.Equal(t, newName, updatedVersion.Name) + assert.NotEqual(t, updatedVersion.Name, version.Name) + }) + + t.Run("Use the same name if a new name is not passed", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + updatedVersion, err := client.UpdateTemplateVersion(ctx, version.ID, codersdk.PatchTemplateVersionRequest{}) + require.NoError(t, err) + assert.Equal(t, version.Name, updatedVersion.Name) + }) + + t.Run("Use the same name for two different templates", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID) + version2 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.CreateTemplate(t, client, user.OrganizationID, version2.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + const commonTemplateVersionName = "common-template-version-name" + updatedVersion1, err := client.UpdateTemplateVersion(ctx, version1.ID, codersdk.PatchTemplateVersionRequest{ + Name: commonTemplateVersionName, + }) + require.NoError(t, err) + + updatedVersion2, err := client.UpdateTemplateVersion(ctx, version2.ID, codersdk.PatchTemplateVersionRequest{ + Name: commonTemplateVersionName, + }) + require.NoError(t, err) + + assert.NotEqual(t, updatedVersion1.ID, updatedVersion2.ID) + assert.Equal(t, updatedVersion1.Name, updatedVersion2.Name) + }) +} diff --git a/codersdk/templateversions.go b/codersdk/templateversions.go index 80a1a14776440..d82f86a2c38b2 100644 --- a/codersdk/templateversions.go +++ b/codersdk/templateversions.go @@ -76,6 +76,10 @@ type TemplateVersionVariable struct { Sensitive bool `json:"sensitive"` } +type PatchTemplateVersionRequest struct { + Name string `json:"name"` +} + // TemplateVersion returns a template version by ID. func (c *Client) TemplateVersion(ctx context.Context, id uuid.UUID) (TemplateVersion, error) { res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s", id), nil) @@ -291,3 +295,16 @@ func (c *Client) PreviousTemplateVersion(ctx context.Context, organization uuid. var version TemplateVersion return version, json.NewDecoder(res.Body).Decode(&version) } + +func (c *Client) UpdateTemplateVersion(ctx context.Context, versionID uuid.UUID, req PatchTemplateVersionRequest) (TemplateVersion, error) { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/templateversions/%s", versionID), req) + if err != nil { + return TemplateVersion{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return TemplateVersion{}, ReadBodyAsError(res) + } + var version TemplateVersion + return version, json.NewDecoder(res.Body).Decode(&version) +} diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 0c77abb129c0e..5901610907217 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -3009,6 +3009,20 @@ Parameter represents a set value for the scope. | `none` | | `data` | +## codersdk.PatchTemplateVersionRequest + +```json +{ + "name": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------ | ------ | -------- | ------------ | ----------- | +| `name` | string | false | | | + ## codersdk.PprofConfig ```json diff --git a/docs/api/templates.md b/docs/api/templates.md index d3397e02d8209..7953da485e25a 100644 --- a/docs/api/templates.md +++ b/docs/api/templates.md @@ -1250,6 +1250,91 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion} \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Patch template version by ID + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/templateversions/{templateversion} \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /templateversions/{templateversion}` + +> Body parameter + +```json +{ + "name": "string" +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +| ----------------- | ---- | -------------------------------------------------------------------------------------- | -------- | ------------------------------ | +| `templateversion` | path | string(uuid) | true | Template version ID | +| `body` | body | [codersdk.PatchTemplateVersionRequest](schemas.md#codersdkpatchtemplateversionrequest) | true | Patch template version request | + +### Example responses + +> 200 Response + +```json +{ + "created_at": "2019-08-24T14:15:22Z", + "created_by": { + "avatar_url": "http://example.com", + "created_at": "2019-08-24T14:15:22Z", + "email": "user@example.com", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "last_seen_at": "2019-08-24T14:15:22Z", + "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "roles": [ + { + "display_name": "string", + "name": "string" + } + ], + "status": "active", + "username": "string" + }, + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "job": { + "canceled_at": "2019-08-24T14:15:22Z", + "completed_at": "2019-08-24T14:15:22Z", + "created_at": "2019-08-24T14:15:22Z", + "error": "string", + "error_code": "MISSING_TEMPLATE_PARAMETER", + "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "started_at": "2019-08-24T14:15:22Z", + "status": "pending", + "tags": { + "property1": "string", + "property2": "string" + }, + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + }, + "name": "string", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "readme": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "updated_at": "2019-08-24T14:15:22Z" +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.TemplateVersion](schemas.md#codersdktemplateversion) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Cancel template version by ID ### Code samples diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 28546db83f274..b7a3e6f2d3fed 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -581,6 +581,11 @@ export interface PatchGroupRequest { readonly quota_allowance?: number } +// From codersdk/templateversions.go +export interface PatchTemplateVersionRequest { + readonly name: string +} + // From codersdk/deployment.go export interface PprofConfig { readonly enable: boolean