Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 75904df

Browse files
johnstcnkylecarbs
authored andcommitted
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 55fd3e2 commit 75904df

File tree

12 files changed

+447
-3
lines changed

12 files changed

+447
-3
lines changed

cli/templateedit.go

+61
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

+94
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

+1
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

+1
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

+19
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

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

+33
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/templates.sql

+13
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

+96
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)