Skip to content

Commit 1e950fa

Browse files
authored
feat: archive template versions to hide them from the ui (#10179)
* api + cli implementation
1 parent edbd519 commit 1e950fa

35 files changed

+1472
-38
lines changed

cli/templatedelete.go

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -48,33 +48,13 @@ func (r *RootCmd) templateDelete() *clibase.Cmd {
4848
templates = append(templates, template)
4949
}
5050
} else {
51-
allTemplates, err := client.TemplatesByOrganization(ctx, organization.ID)
51+
template, err := selectTemplate(inv, client, organization)
5252
if err != nil {
53-
return xerrors.Errorf("get templates by organization: %w", err)
53+
return err
5454
}
5555

56-
if len(allTemplates) == 0 {
57-
return xerrors.Errorf("no templates exist in the current organization %q", organization.Name)
58-
}
59-
60-
opts := make([]string, 0, len(allTemplates))
61-
for _, template := range allTemplates {
62-
opts = append(opts, template.Name)
63-
}
64-
65-
selection, err := cliui.Select(inv, cliui.SelectOptions{
66-
Options: opts,
67-
})
68-
if err != nil {
69-
return xerrors.Errorf("select template: %w", err)
70-
}
71-
72-
for _, template := range allTemplates {
73-
if template.Name == selection {
74-
templates = append(templates, template)
75-
templateNames = append(templateNames, template.Name)
76-
}
77-
}
56+
templates = append(templates, template)
57+
templateNames = append(templateNames, template.Name)
7858
}
7959

8060
// Confirm deletion of the template.

cli/templates.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ package cli
33
import (
44
"time"
55

6-
"github.com/google/uuid"
7-
86
"github.com/coder/pretty"
7+
"github.com/google/uuid"
8+
"golang.org/x/xerrors"
99

1010
"github.com/coder/coder/v2/cli/clibase"
1111
"github.com/coder/coder/v2/cli/cliui"
@@ -43,12 +43,45 @@ func (r *RootCmd) templates() *clibase.Cmd {
4343
r.templateVersions(),
4444
r.templateDelete(),
4545
r.templatePull(),
46+
r.archiveTemplateVersions(),
4647
},
4748
}
4849

4950
return cmd
5051
}
5152

53+
func selectTemplate(inv *clibase.Invocation, client *codersdk.Client, organization codersdk.Organization) (codersdk.Template, error) {
54+
var empty codersdk.Template
55+
ctx := inv.Context()
56+
allTemplates, err := client.TemplatesByOrganization(ctx, organization.ID)
57+
if err != nil {
58+
return empty, xerrors.Errorf("get templates by organization: %w", err)
59+
}
60+
61+
if len(allTemplates) == 0 {
62+
return empty, xerrors.Errorf("no templates exist in the current organization %q", organization.Name)
63+
}
64+
65+
opts := make([]string, 0, len(allTemplates))
66+
for _, template := range allTemplates {
67+
opts = append(opts, template.Name)
68+
}
69+
70+
selection, err := cliui.Select(inv, cliui.SelectOptions{
71+
Options: opts,
72+
})
73+
if err != nil {
74+
return empty, xerrors.Errorf("select template: %w", err)
75+
}
76+
77+
for _, template := range allTemplates {
78+
if template.Name == selection {
79+
return template, nil
80+
}
81+
}
82+
return empty, xerrors.Errorf("no template selected")
83+
}
84+
5285
type templateTableRow struct {
5386
// Used by json format:
5487
Template codersdk.Template

cli/templateversionarchive.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package cli
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strings"
7+
"time"
8+
9+
"github.com/coder/pretty"
10+
"golang.org/x/xerrors"
11+
12+
"github.com/coder/coder/v2/cli/clibase"
13+
"github.com/coder/coder/v2/cli/cliui"
14+
"github.com/coder/coder/v2/codersdk"
15+
)
16+
17+
func (r *RootCmd) unarchiveTemplateVersion() *clibase.Cmd {
18+
return r.setArchiveTemplateVersion(false)
19+
}
20+
21+
func (r *RootCmd) archiveTemplateVersion() *clibase.Cmd {
22+
return r.setArchiveTemplateVersion(true)
23+
}
24+
25+
//nolint:revive
26+
func (r *RootCmd) setArchiveTemplateVersion(archive bool) *clibase.Cmd {
27+
presentVerb := "archive"
28+
pastVerb := "archived"
29+
if !archive {
30+
presentVerb = "unarchive"
31+
pastVerb = "unarchived"
32+
}
33+
34+
client := new(codersdk.Client)
35+
cmd := &clibase.Cmd{
36+
Use: presentVerb + " <template-name> [template-version-names...] ",
37+
Short: strings.ToUpper(string(presentVerb[0])) + presentVerb[1:] + " a template version(s).",
38+
Middleware: clibase.Chain(
39+
r.InitClient(client),
40+
),
41+
Options: clibase.OptionSet{
42+
cliui.SkipPromptOption(),
43+
},
44+
Handler: func(inv *clibase.Invocation) error {
45+
var (
46+
ctx = inv.Context()
47+
versions []codersdk.TemplateVersion
48+
)
49+
50+
organization, err := CurrentOrganization(inv, client)
51+
if err != nil {
52+
return err
53+
}
54+
55+
if len(inv.Args) == 0 {
56+
return xerrors.Errorf("missing template name")
57+
}
58+
if len(inv.Args) < 2 {
59+
return xerrors.Errorf("missing template version name(s)")
60+
}
61+
62+
templateName := inv.Args[0]
63+
template, err := client.TemplateByName(ctx, organization.ID, templateName)
64+
if err != nil {
65+
return xerrors.Errorf("get template by name: %w", err)
66+
}
67+
for _, versionName := range inv.Args[1:] {
68+
version, err := client.TemplateVersionByOrganizationAndName(ctx, organization.ID, template.Name, versionName)
69+
if err != nil {
70+
return xerrors.Errorf("get template version by name %q: %w", versionName, err)
71+
}
72+
versions = append(versions, version)
73+
}
74+
75+
for _, version := range versions {
76+
if version.Archived == archive {
77+
_, _ = fmt.Fprintln(
78+
inv.Stdout, fmt.Sprintf("Version "+pretty.Sprint(cliui.DefaultStyles.Keyword, version.Name)+" already "+pastVerb),
79+
)
80+
continue
81+
}
82+
83+
err := client.SetArchiveTemplateVersion(ctx, version.ID, archive)
84+
if err != nil {
85+
return xerrors.Errorf("%s template version %q: %w", presentVerb, version.Name, err)
86+
}
87+
88+
_, _ = fmt.Fprintln(
89+
inv.Stdout, fmt.Sprintf("Version "+pretty.Sprint(cliui.DefaultStyles.Keyword, version.Name)+" "+pastVerb+" at "+cliui.Timestamp(time.Now())),
90+
)
91+
}
92+
return nil
93+
},
94+
}
95+
96+
return cmd
97+
}
98+
99+
func (r *RootCmd) archiveTemplateVersions() *clibase.Cmd {
100+
var all clibase.Bool
101+
client := new(codersdk.Client)
102+
cmd := &clibase.Cmd{
103+
Use: "archive [template-name...] ",
104+
Short: "Archive unused or failed template versions from a given template(s)",
105+
Middleware: clibase.Chain(
106+
r.InitClient(client),
107+
),
108+
Options: clibase.OptionSet{
109+
cliui.SkipPromptOption(),
110+
clibase.Option{
111+
Name: "all",
112+
Description: "Include all unused template versions. By default, only failed template versions are archived.",
113+
Flag: "all",
114+
Value: &all,
115+
},
116+
},
117+
Handler: func(inv *clibase.Invocation) error {
118+
var (
119+
ctx = inv.Context()
120+
templateNames = []string{}
121+
templates = []codersdk.Template{}
122+
)
123+
124+
organization, err := CurrentOrganization(inv, client)
125+
if err != nil {
126+
return err
127+
}
128+
129+
if len(inv.Args) > 0 {
130+
templateNames = inv.Args
131+
132+
for _, templateName := range templateNames {
133+
template, err := client.TemplateByName(ctx, organization.ID, templateName)
134+
if err != nil {
135+
return xerrors.Errorf("get template by name: %w", err)
136+
}
137+
templates = append(templates, template)
138+
}
139+
} else {
140+
template, err := selectTemplate(inv, client, organization)
141+
if err != nil {
142+
return err
143+
}
144+
145+
templates = append(templates, template)
146+
templateNames = append(templateNames, template.Name)
147+
}
148+
149+
// Confirm archive of the template.
150+
_, err = cliui.Prompt(inv, cliui.PromptOptions{
151+
Text: fmt.Sprintf("Archive template versions of these templates: %s?", pretty.Sprint(cliui.DefaultStyles.Code, strings.Join(templateNames, ", "))),
152+
IsConfirm: true,
153+
Default: cliui.ConfirmNo,
154+
})
155+
if err != nil {
156+
return err
157+
}
158+
159+
for _, template := range templates {
160+
resp, err := client.ArchiveTemplateVersions(ctx, template.ID, all.Value())
161+
if err != nil {
162+
return xerrors.Errorf("archive template %q: %w", template.Name, err)
163+
}
164+
165+
_, _ = fmt.Fprintln(
166+
inv.Stdout, fmt.Sprintf("Archived %d versions from "+pretty.Sprint(cliui.DefaultStyles.Keyword, template.Name)+" at "+cliui.Timestamp(time.Now()), len(resp.ArchivedIDs)),
167+
)
168+
169+
if ok, _ := inv.ParsedFlags().GetBool("verbose"); err == nil && ok {
170+
data, err := json.Marshal(resp)
171+
if err != nil {
172+
return xerrors.Errorf("marshal verbose response: %w", err)
173+
}
174+
_, _ = fmt.Fprintln(
175+
inv.Stdout, string(data),
176+
)
177+
}
178+
}
179+
return nil
180+
},
181+
}
182+
183+
return cmd
184+
}

cli/templateversionarchive_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package cli_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/google/uuid"
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/coder/coder/v2/cli/clitest"
10+
"github.com/coder/coder/v2/coderd/coderdtest"
11+
"github.com/coder/coder/v2/coderd/rbac"
12+
"github.com/coder/coder/v2/codersdk"
13+
"github.com/coder/coder/v2/provisioner/echo"
14+
"github.com/coder/coder/v2/testutil"
15+
)
16+
17+
func TestTemplateVersionsArchive(t *testing.T) {
18+
t.Parallel()
19+
t.Run("Archive-Unarchive", func(t *testing.T) {
20+
t.Parallel()
21+
ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
22+
owner := coderdtest.CreateFirstUser(t, ownerClient)
23+
24+
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
25+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
26+
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
27+
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
28+
other := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil, func(request *codersdk.CreateTemplateVersionRequest) {
29+
request.TemplateID = template.ID
30+
})
31+
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, other.ID)
32+
33+
// Archive
34+
inv, root := clitest.New(t, "templates", "versions", "archive", template.Name, other.Name, "-y")
35+
clitest.SetupConfig(t, client, root)
36+
w := clitest.StartWithWaiter(t, inv)
37+
w.RequireSuccess()
38+
39+
// Verify archived
40+
ctx := testutil.Context(t, testutil.WaitMedium)
41+
found, err := client.TemplateVersion(ctx, other.ID)
42+
require.NoError(t, err)
43+
require.True(t, found.Archived, "expect archived")
44+
45+
// Unarchive
46+
inv, root = clitest.New(t, "templates", "versions", "unarchive", template.Name, other.Name, "-y")
47+
clitest.SetupConfig(t, client, root)
48+
w = clitest.StartWithWaiter(t, inv)
49+
w.RequireSuccess()
50+
51+
// Verify unarchived
52+
ctx = testutil.Context(t, testutil.WaitMedium)
53+
found, err = client.TemplateVersion(ctx, other.ID)
54+
require.NoError(t, err)
55+
require.False(t, found.Archived, "expect unarchived")
56+
})
57+
58+
t.Run("ArchiveMany", func(t *testing.T) {
59+
t.Parallel()
60+
ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
61+
owner := coderdtest.CreateFirstUser(t, ownerClient)
62+
63+
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
64+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
65+
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
66+
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
67+
68+
// Add a failed
69+
expArchived := map[uuid.UUID]bool{}
70+
failed := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
71+
Parse: echo.ParseComplete,
72+
ProvisionApply: echo.ApplyFailed,
73+
ProvisionPlan: echo.PlanFailed,
74+
}, func(request *codersdk.CreateTemplateVersionRequest) {
75+
request.TemplateID = template.ID
76+
})
77+
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, failed.ID)
78+
expArchived[failed.ID] = true
79+
// Add unused
80+
unused := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil, func(request *codersdk.CreateTemplateVersionRequest) {
81+
request.TemplateID = template.ID
82+
})
83+
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, unused.ID)
84+
expArchived[unused.ID] = true
85+
86+
// Archive all unused versions
87+
inv, root := clitest.New(t, "templates", "archive", template.Name, "-y", "--all")
88+
clitest.SetupConfig(t, client, root)
89+
w := clitest.StartWithWaiter(t, inv)
90+
w.RequireSuccess()
91+
92+
ctx := testutil.Context(t, testutil.WaitMedium)
93+
all, err := client.TemplateVersionsByTemplate(ctx, codersdk.TemplateVersionsByTemplateRequest{
94+
TemplateID: template.ID,
95+
IncludeArchived: true,
96+
})
97+
require.NoError(t, err, "query all versions")
98+
for _, v := range all {
99+
if _, ok := expArchived[v.ID]; ok {
100+
require.True(t, v.Archived, "expect archived")
101+
delete(expArchived, v.ID)
102+
} else {
103+
require.False(t, v.Archived, "expect unarchived")
104+
}
105+
}
106+
require.Len(t, expArchived, 0, "expect all archived")
107+
})
108+
}

0 commit comments

Comments
 (0)