Skip to content

Commit 3c5b15a

Browse files
committed
feat: cli: allow editing template metadata
1 parent 945fa9d commit 3c5b15a

File tree

12 files changed

+401
-3
lines changed

12 files changed

+401
-3
lines changed

cli/templateedit.go

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

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: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,87 @@ 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+
count := uint32(0)
322+
var updated database.Template
323+
err := api.Database.InTx(func(s database.Store) error {
324+
// Fetch workspace counts
325+
workspaceCounts, err := s.GetWorkspaceOwnerCountsByTemplateIDs(r.Context(), []uuid.UUID{template.ID})
326+
if errors.Is(err, sql.ErrNoRows) {
327+
err = nil
328+
}
329+
if err != nil {
330+
return err
331+
}
332+
333+
if len(workspaceCounts) > 0 {
334+
count = uint32(workspaceCounts[0].Count)
335+
}
336+
337+
if req.Description == template.Description &&
338+
req.MaxTTLMillis == time.Duration(template.MaxTtl).Milliseconds() &&
339+
req.MinAutostartIntervalMillis == time.Duration(template.MinAutostartInterval).Milliseconds() {
340+
return nil
341+
}
342+
343+
// Update template metadata -- empty fields are not overwritten.
344+
desc := req.Description
345+
maxTTL := time.Duration(req.MaxTTLMillis) * time.Millisecond
346+
minAutostartInterval := time.Duration(req.MinAutostartIntervalMillis) * time.Millisecond
347+
348+
if desc == "" {
349+
desc = template.Description
350+
}
351+
if maxTTL == 0 {
352+
maxTTL = time.Duration(template.MaxTtl)
353+
}
354+
if minAutostartInterval == 0 {
355+
minAutostartInterval = time.Duration(template.MinAutostartInterval)
356+
}
357+
358+
if err := s.UpdateTemplateMetaByID(r.Context(), database.UpdateTemplateMetaByIDParams{
359+
ID: template.ID,
360+
UpdatedAt: database.Now(),
361+
Description: desc,
362+
MaxTtl: int64(maxTTL),
363+
MinAutostartInterval: int64(minAutostartInterval),
364+
}); err != nil {
365+
return err
366+
}
367+
368+
updated, err = s.GetTemplateByID(r.Context(), template.ID)
369+
if err != nil {
370+
return err
371+
}
372+
return nil
373+
})
374+
375+
if err != nil {
376+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
377+
Message: "Internal error updating template metadata.",
378+
Detail: err.Error(),
379+
})
380+
return
381+
}
382+
383+
if updated.UpdatedAt.IsZero() {
384+
httpapi.Write(rw, http.StatusNotModified, nil)
385+
return
386+
}
387+
388+
httpapi.Write(rw, http.StatusOK, convertTemplate(updated, count))
389+
}
390+
310391
func convertTemplates(templates []database.Template, workspaceCounts []database.GetWorkspaceOwnerCountsByTemplateIDsRow) []codersdk.Template {
311392
apiTemplates := make([]codersdk.Template, 0, len(templates))
312393
for _, template := range templates {

0 commit comments

Comments
 (0)