Skip to content

Commit 8cfe223

Browse files
authored
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
1 parent b65259f commit 8cfe223

File tree

12 files changed

+447
-3
lines changed

12 files changed

+447
-3
lines changed

cli/templateedit.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/spf13/cobra"
8+
"golang.org/x/xerrors"
9+
10+
"github.com/coder/coder/cli/cliui"
11+
"github.com/coder/coder/codersdk"
12+
)
13+
14+
func templateEdit() *cobra.Command {
15+
var (
16+
description string
17+
maxTTL time.Duration
18+
minAutostartInterval time.Duration
19+
)
20+
21+
cmd := &cobra.Command{
22+
Use: "edit <template> [flags]",
23+
Args: cobra.ExactArgs(1),
24+
Short: "Edit the metadata of a template by name.",
25+
RunE: func(cmd *cobra.Command, args []string) error {
26+
client, err := createClient(cmd)
27+
if err != nil {
28+
return xerrors.Errorf("create client: %w", err)
29+
}
30+
organization, err := currentOrganization(cmd, client)
31+
if err != nil {
32+
return xerrors.Errorf("get current organization: %w", err)
33+
}
34+
template, err := client.TemplateByName(cmd.Context(), organization.ID, args[0])
35+
if err != nil {
36+
return xerrors.Errorf("get workspace template: %w", err)
37+
}
38+
39+
// NOTE: coderd will ignore empty fields.
40+
req := codersdk.UpdateTemplateMeta{
41+
Description: description,
42+
MaxTTLMillis: maxTTL.Milliseconds(),
43+
MinAutostartIntervalMillis: minAutostartInterval.Milliseconds(),
44+
}
45+
46+
_, err = client.UpdateTemplateMeta(cmd.Context(), template.ID, req)
47+
if err != nil {
48+
return xerrors.Errorf("update template metadata: %w", err)
49+
}
50+
_, _ = fmt.Printf("Updated template metadata!\n")
51+
return nil
52+
},
53+
}
54+
55+
cmd.Flags().StringVarP(&description, "description", "", "", "Edit the template description")
56+
cmd.Flags().DurationVarP(&maxTTL, "max_ttl", "", 0, "Edit the template maximum time before shutdown")
57+
cmd.Flags().DurationVarP(&minAutostartInterval, "min_autostart_interval", "", 0, "Edit the template minimum autostart interval")
58+
cliui.AllowSkipPrompt(cmd)
59+
60+
return cmd
61+
}

cli/templateedit_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package cli_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/coder/coder/cli/clitest"
12+
"github.com/coder/coder/coderd/coderdtest"
13+
"github.com/coder/coder/coderd/util/ptr"
14+
"github.com/coder/coder/codersdk"
15+
)
16+
17+
func TestTemplateEdit(t *testing.T) {
18+
t.Parallel()
19+
20+
t.Run("Modified", func(t *testing.T) {
21+
t.Parallel()
22+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
23+
user := coderdtest.CreateFirstUser(t, client)
24+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
25+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
26+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
27+
ctr.Description = "original description"
28+
ctr.MaxTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
29+
ctr.MinAutostartIntervalMillis = ptr.Ref(time.Hour.Milliseconds())
30+
})
31+
32+
// Test the cli command.
33+
desc := "lorem ipsum dolor sit amet et cetera"
34+
maxTTL := 12 * time.Hour
35+
minAutostartInterval := time.Minute
36+
cmdArgs := []string{
37+
"templates",
38+
"edit",
39+
template.Name,
40+
"--description", desc,
41+
"--max_ttl", maxTTL.String(),
42+
"--min_autostart_interval", minAutostartInterval.String(),
43+
}
44+
cmd, root := clitest.New(t, cmdArgs...)
45+
clitest.SetupConfig(t, client, root)
46+
47+
err := cmd.Execute()
48+
49+
require.NoError(t, err)
50+
51+
// Assert that the template metadata changed.
52+
updated, err := client.Template(context.Background(), template.ID)
53+
require.NoError(t, err)
54+
assert.Equal(t, desc, updated.Description)
55+
assert.Equal(t, maxTTL.Milliseconds(), updated.MaxTTLMillis)
56+
assert.Equal(t, minAutostartInterval.Milliseconds(), updated.MinAutostartIntervalMillis)
57+
})
58+
59+
t.Run("NotModified", func(t *testing.T) {
60+
t.Parallel()
61+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
62+
user := coderdtest.CreateFirstUser(t, client)
63+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
64+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
65+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
66+
ctr.Description = "original description"
67+
ctr.MaxTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
68+
ctr.MinAutostartIntervalMillis = ptr.Ref(time.Hour.Milliseconds())
69+
})
70+
71+
// Test the cli command.
72+
cmdArgs := []string{
73+
"templates",
74+
"edit",
75+
template.Name,
76+
"--description", template.Description,
77+
"--max_ttl", (time.Duration(template.MaxTTLMillis) * time.Millisecond).String(),
78+
"--min_autostart_interval", (time.Duration(template.MinAutostartIntervalMillis) * time.Millisecond).String(),
79+
}
80+
cmd, root := clitest.New(t, cmdArgs...)
81+
clitest.SetupConfig(t, client, root)
82+
83+
err := cmd.Execute()
84+
85+
require.ErrorContains(t, err, "not modified")
86+
87+
// Assert that the template metadata did not change.
88+
updated, err := client.Template(context.Background(), template.ID)
89+
require.NoError(t, err)
90+
assert.Equal(t, template.Description, updated.Description)
91+
assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
92+
assert.Equal(t, template.MinAutostartIntervalMillis, updated.MinAutostartIntervalMillis)
93+
})
94+
}

cli/templates.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func templates() *cobra.Command {
2626
}
2727
cmd.AddCommand(
2828
templateCreate(),
29+
templateEdit(),
2930
templateInit(),
3031
templateList(),
3132
templatePlan(),

coderd/coderd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ func New(options *Options) *API {
199199

200200
r.Get("/", api.template)
201201
r.Delete("/", api.deleteTemplate)
202+
r.Patch("/", api.patchTemplateMeta)
202203
r.Route("/versions", func(r chi.Router) {
203204
r.Get("/", api.templateVersionsByTemplate)
204205
r.Patch("/", api.patchActiveTemplateVersion)

coderd/database/databasefake/databasefake.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,25 @@ func (q *fakeQuerier) GetTemplateByOrganizationAndName(_ context.Context, arg da
742742
return database.Template{}, sql.ErrNoRows
743743
}
744744

745+
func (q *fakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.UpdateTemplateMetaByIDParams) error {
746+
q.mutex.RLock()
747+
defer q.mutex.RUnlock()
748+
749+
for idx, tpl := range q.templates {
750+
if tpl.ID != arg.ID {
751+
continue
752+
}
753+
tpl.UpdatedAt = database.Now()
754+
tpl.Description = arg.Description
755+
tpl.MaxTtl = arg.MaxTtl
756+
tpl.MinAutostartInterval = arg.MinAutostartInterval
757+
q.templates[idx] = tpl
758+
return nil
759+
}
760+
761+
return sql.ErrNoRows
762+
}
763+
745764
func (q *fakeQuerier) GetTemplateVersionsByTemplateID(_ context.Context, arg database.GetTemplateVersionsByTemplateIDParams) (version []database.TemplateVersion, err error) {
746765
q.mutex.RLock()
747766
defer q.mutex.RUnlock()

coderd/database/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/templates.sql

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,16 @@ SET
6969
deleted = $2
7070
WHERE
7171
id = $1;
72+
73+
-- name: UpdateTemplateMetaByID :exec
74+
UPDATE
75+
templates
76+
SET
77+
updated_at = $2,
78+
description = $3,
79+
max_ttl = $4,
80+
min_autostart_interval = $5
81+
WHERE
82+
id = $1
83+
RETURNING
84+
*;

coderd/templates.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,102 @@ func (api *API) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Re
307307
httpapi.Write(rw, http.StatusOK, convertTemplate(template, count))
308308
}
309309

310+
func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
311+
template := httpmw.TemplateParam(r)
312+
if !api.Authorize(rw, r, rbac.ActionUpdate, template) {
313+
return
314+
}
315+
316+
var req codersdk.UpdateTemplateMeta
317+
if !httpapi.Read(rw, r, &req) {
318+
return
319+
}
320+
321+
var validErrs []httpapi.Error
322+
if req.MaxTTLMillis < 0 {
323+
validErrs = append(validErrs, httpapi.Error{Field: "max_ttl_ms", Detail: "Must be a positive integer."})
324+
}
325+
if req.MinAutostartIntervalMillis < 0 {
326+
validErrs = append(validErrs, httpapi.Error{Field: "min_autostart_interval_ms", Detail: "Must be a positive integer."})
327+
}
328+
329+
if len(validErrs) > 0 {
330+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
331+
Message: "Invalid request to update template metadata!",
332+
Validations: validErrs,
333+
})
334+
return
335+
}
336+
337+
count := uint32(0)
338+
var updated database.Template
339+
err := api.Database.InTx(func(s database.Store) error {
340+
// Fetch workspace counts
341+
workspaceCounts, err := s.GetWorkspaceOwnerCountsByTemplateIDs(r.Context(), []uuid.UUID{template.ID})
342+
if xerrors.Is(err, sql.ErrNoRows) {
343+
err = nil
344+
}
345+
if err != nil {
346+
return err
347+
}
348+
349+
if len(workspaceCounts) > 0 {
350+
count = uint32(workspaceCounts[0].Count)
351+
}
352+
353+
if req.Description == template.Description &&
354+
req.MaxTTLMillis == time.Duration(template.MaxTtl).Milliseconds() &&
355+
req.MinAutostartIntervalMillis == time.Duration(template.MinAutostartInterval).Milliseconds() {
356+
return nil
357+
}
358+
359+
// Update template metadata -- empty fields are not overwritten.
360+
desc := req.Description
361+
maxTTL := time.Duration(req.MaxTTLMillis) * time.Millisecond
362+
minAutostartInterval := time.Duration(req.MinAutostartIntervalMillis) * time.Millisecond
363+
364+
if desc == "" {
365+
desc = template.Description
366+
}
367+
if maxTTL == 0 {
368+
maxTTL = time.Duration(template.MaxTtl)
369+
}
370+
if minAutostartInterval == 0 {
371+
minAutostartInterval = time.Duration(template.MinAutostartInterval)
372+
}
373+
374+
if err := s.UpdateTemplateMetaByID(r.Context(), database.UpdateTemplateMetaByIDParams{
375+
ID: template.ID,
376+
UpdatedAt: database.Now(),
377+
Description: desc,
378+
MaxTtl: int64(maxTTL),
379+
MinAutostartInterval: int64(minAutostartInterval),
380+
}); err != nil {
381+
return err
382+
}
383+
384+
updated, err = s.GetTemplateByID(r.Context(), template.ID)
385+
if err != nil {
386+
return err
387+
}
388+
return nil
389+
})
390+
if err != nil {
391+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
392+
Message: "Internal error updating template metadata.",
393+
Detail: err.Error(),
394+
})
395+
return
396+
}
397+
398+
if updated.UpdatedAt.IsZero() {
399+
httpapi.Write(rw, http.StatusNotModified, nil)
400+
return
401+
}
402+
403+
httpapi.Write(rw, http.StatusOK, convertTemplate(updated, count))
404+
}
405+
310406
func convertTemplates(templates []database.Template, workspaceCounts []database.GetWorkspaceOwnerCountsByTemplateIDsRow) []codersdk.Template {
311407
apiTemplates := make([]codersdk.Template, 0, len(templates))
312408
for _, template := range templates {

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy