feat: cli: allow editing template metadata (#2159)

This PR adds a CLI command template edit which allows updating the following metadata fields of a template:
- Description
- Max TTL
- Min Autostart Interval
This commit is contained in:
Cian Johnston
2022-06-08 15:14:57 +01:00
committed by GitHub
parent b65259f95e
commit 8cfe223192
12 changed files with 447 additions and 3 deletions
+1
View File
@@ -199,6 +199,7 @@ func New(options *Options) *API {
r.Get("/", api.template)
r.Delete("/", api.deleteTemplate)
r.Patch("/", api.patchTemplateMeta)
r.Route("/versions", func(r chi.Router) {
r.Get("/", api.templateVersionsByTemplate)
r.Patch("/", api.patchActiveTemplateVersion)
@@ -742,6 +742,25 @@ func (q *fakeQuerier) GetTemplateByOrganizationAndName(_ context.Context, arg da
return database.Template{}, sql.ErrNoRows
}
func (q *fakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.UpdateTemplateMetaByIDParams) error {
q.mutex.RLock()
defer q.mutex.RUnlock()
for idx, tpl := range q.templates {
if tpl.ID != arg.ID {
continue
}
tpl.UpdatedAt = database.Now()
tpl.Description = arg.Description
tpl.MaxTtl = arg.MaxTtl
tpl.MinAutostartInterval = arg.MinAutostartInterval
q.templates[idx] = tpl
return nil
}
return sql.ErrNoRows
}
func (q *fakeQuerier) GetTemplateVersionsByTemplateID(_ context.Context, arg database.GetTemplateVersionsByTemplateIDParams) (version []database.TemplateVersion, err error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
+1
View File
@@ -108,6 +108,7 @@ type querier interface {
UpdateProvisionerJobWithCompleteByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteByIDParams) error
UpdateTemplateActiveVersionByID(ctx context.Context, arg UpdateTemplateActiveVersionByIDParams) error
UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTemplateDeletedByIDParams) error
UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error
UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) error
UpdateTemplateVersionDescriptionByJobID(ctx context.Context, arg UpdateTemplateVersionDescriptionByJobIDParams) error
UpdateUserHashedPassword(ctx context.Context, arg UpdateUserHashedPasswordParams) error
+33
View File
@@ -1862,6 +1862,39 @@ func (q *sqlQuerier) UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTe
return err
}
const updateTemplateMetaByID = `-- name: UpdateTemplateMetaByID :exec
UPDATE
templates
SET
updated_at = $2,
description = $3,
max_ttl = $4,
min_autostart_interval = $5
WHERE
id = $1
RETURNING
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval
`
type UpdateTemplateMetaByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Description string `db:"description" json:"description"`
MaxTtl int64 `db:"max_ttl" json:"max_ttl"`
MinAutostartInterval int64 `db:"min_autostart_interval" json:"min_autostart_interval"`
}
func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error {
_, err := q.db.ExecContext(ctx, updateTemplateMetaByID,
arg.ID,
arg.UpdatedAt,
arg.Description,
arg.MaxTtl,
arg.MinAutostartInterval,
)
return err
}
const getTemplateVersionByID = `-- name: GetTemplateVersionByID :one
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id
+13
View File
@@ -69,3 +69,16 @@ SET
deleted = $2
WHERE
id = $1;
-- name: UpdateTemplateMetaByID :exec
UPDATE
templates
SET
updated_at = $2,
description = $3,
max_ttl = $4,
min_autostart_interval = $5
WHERE
id = $1
RETURNING
*;
+96
View File
@@ -307,6 +307,102 @@ func (api *API) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Re
httpapi.Write(rw, http.StatusOK, convertTemplate(template, count))
}
func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
template := httpmw.TemplateParam(r)
if !api.Authorize(rw, r, rbac.ActionUpdate, template) {
return
}
var req codersdk.UpdateTemplateMeta
if !httpapi.Read(rw, r, &req) {
return
}
var validErrs []httpapi.Error
if req.MaxTTLMillis < 0 {
validErrs = append(validErrs, httpapi.Error{Field: "max_ttl_ms", Detail: "Must be a positive integer."})
}
if req.MinAutostartIntervalMillis < 0 {
validErrs = append(validErrs, httpapi.Error{Field: "min_autostart_interval_ms", Detail: "Must be a positive integer."})
}
if len(validErrs) > 0 {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "Invalid request to update template metadata!",
Validations: validErrs,
})
return
}
count := uint32(0)
var updated database.Template
err := api.Database.InTx(func(s database.Store) error {
// Fetch workspace counts
workspaceCounts, err := s.GetWorkspaceOwnerCountsByTemplateIDs(r.Context(), []uuid.UUID{template.ID})
if xerrors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
return err
}
if len(workspaceCounts) > 0 {
count = uint32(workspaceCounts[0].Count)
}
if req.Description == template.Description &&
req.MaxTTLMillis == time.Duration(template.MaxTtl).Milliseconds() &&
req.MinAutostartIntervalMillis == time.Duration(template.MinAutostartInterval).Milliseconds() {
return nil
}
// Update template metadata -- empty fields are not overwritten.
desc := req.Description
maxTTL := time.Duration(req.MaxTTLMillis) * time.Millisecond
minAutostartInterval := time.Duration(req.MinAutostartIntervalMillis) * time.Millisecond
if desc == "" {
desc = template.Description
}
if maxTTL == 0 {
maxTTL = time.Duration(template.MaxTtl)
}
if minAutostartInterval == 0 {
minAutostartInterval = time.Duration(template.MinAutostartInterval)
}
if err := s.UpdateTemplateMetaByID(r.Context(), database.UpdateTemplateMetaByIDParams{
ID: template.ID,
UpdatedAt: database.Now(),
Description: desc,
MaxTtl: int64(maxTTL),
MinAutostartInterval: int64(minAutostartInterval),
}); err != nil {
return err
}
updated, err = s.GetTemplateByID(r.Context(), template.ID)
if err != nil {
return err
}
return nil
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: "Internal error updating template metadata.",
Detail: err.Error(),
})
return
}
if updated.UpdatedAt.IsZero() {
httpapi.Write(rw, http.StatusNotModified, nil)
return
}
httpapi.Write(rw, http.StatusOK, convertTemplate(updated, count))
}
func convertTemplates(templates []database.Template, workspaceCounts []database.GetWorkspaceOwnerCountsByTemplateIDsRow) []codersdk.Template {
apiTemplates := make([]codersdk.Template, 0, len(templates))
for _, template := range templates {
+95
View File
@@ -4,12 +4,14 @@ import (
"context"
"net/http"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/codersdk"
)
@@ -145,6 +147,99 @@ func TestTemplateByOrganizationAndName(t *testing.T) {
})
}
func TestPatchTemplateMeta(t *testing.T) {
t.Parallel()
t.Run("Modified", func(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.Description = "original description"
ctr.MaxTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
ctr.MinAutostartIntervalMillis = ptr.Ref(time.Hour.Milliseconds())
})
req := codersdk.UpdateTemplateMeta{
Description: "lorem ipsum dolor sit amet et cetera",
MaxTTLMillis: 12 * time.Hour.Milliseconds(),
MinAutostartIntervalMillis: time.Minute.Milliseconds(),
}
updated, err := client.UpdateTemplateMeta(ctx, template.ID, req)
require.NoError(t, err)
assert.Greater(t, updated.UpdatedAt, template.UpdatedAt)
assert.Equal(t, req.Description, updated.Description)
assert.Equal(t, req.MaxTTLMillis, updated.MaxTTLMillis)
assert.Equal(t, req.MinAutostartIntervalMillis, updated.MinAutostartIntervalMillis)
// Extra paranoid: did it _really_ happen?
updated, err = client.Template(ctx, template.ID)
require.NoError(t, err)
assert.Greater(t, updated.UpdatedAt, template.UpdatedAt)
assert.Equal(t, req.Description, updated.Description)
assert.Equal(t, req.MaxTTLMillis, updated.MaxTTLMillis)
assert.Equal(t, req.MinAutostartIntervalMillis, updated.MinAutostartIntervalMillis)
})
t.Run("NotModified", func(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.Description = "original description"
ctr.MaxTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
ctr.MinAutostartIntervalMillis = ptr.Ref(time.Hour.Milliseconds())
})
req := codersdk.UpdateTemplateMeta{
Description: template.Description,
MaxTTLMillis: template.MaxTTLMillis,
MinAutostartIntervalMillis: template.MinAutostartIntervalMillis,
}
_, err := client.UpdateTemplateMeta(ctx, template.ID, req)
require.ErrorContains(t, err, "not modified")
updated, err := client.Template(ctx, template.ID)
require.NoError(t, err)
assert.Equal(t, updated.UpdatedAt, template.UpdatedAt)
assert.Equal(t, template.Description, updated.Description)
assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
assert.Equal(t, template.MinAutostartIntervalMillis, updated.MinAutostartIntervalMillis)
})
t.Run("Invalid", func(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.Description = "original description"
ctr.MaxTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
ctr.MinAutostartIntervalMillis = ptr.Ref(time.Hour.Milliseconds())
})
req := codersdk.UpdateTemplateMeta{
MaxTTLMillis: -int64(time.Hour),
MinAutostartIntervalMillis: -int64(time.Hour),
}
_, err := client.UpdateTemplateMeta(ctx, template.ID, req)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Contains(t, apiErr.Message, "Invalid request")
require.Len(t, apiErr.Validations, 2)
assert.Equal(t, apiErr.Validations[0].Field, "max_ttl_ms")
assert.Equal(t, apiErr.Validations[1].Field, "min_autostart_interval_ms")
updated, err := client.Template(ctx, template.ID)
require.NoError(t, err)
assert.WithinDuration(t, template.UpdatedAt, updated.UpdatedAt, time.Minute)
assert.Equal(t, template.Description, updated.Description)
assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
assert.Equal(t, template.MinAutostartIntervalMillis, updated.MinAutostartIntervalMillis)
})
}
func TestDeleteTemplate(t *testing.T) {
t.Parallel()