Skip to content

feat: archive template versions to hide them from the ui #10179

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 4 additions & 24 deletions cli/templatedelete.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,33 +48,13 @@ func (r *RootCmd) templateDelete() *clibase.Cmd {
templates = append(templates, template)
}
} else {
allTemplates, err := client.TemplatesByOrganization(ctx, organization.ID)
template, err := selectTemplate(inv, client, organization)
if err != nil {
return xerrors.Errorf("get templates by organization: %w", err)
return err
}

if len(allTemplates) == 0 {
return xerrors.Errorf("no templates exist in the current organization %q", organization.Name)
}

opts := make([]string, 0, len(allTemplates))
for _, template := range allTemplates {
opts = append(opts, template.Name)
}

selection, err := cliui.Select(inv, cliui.SelectOptions{
Options: opts,
})
if err != nil {
return xerrors.Errorf("select template: %w", err)
}

for _, template := range allTemplates {
if template.Name == selection {
templates = append(templates, template)
templateNames = append(templateNames, template.Name)
}
}
templates = append(templates, template)
templateNames = append(templateNames, template.Name)
}

// Confirm deletion of the template.
Expand Down
37 changes: 35 additions & 2 deletions cli/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package cli
import (
"time"

"github.com/google/uuid"

"github.com/coder/pretty"
"github.com/google/uuid"
"golang.org/x/xerrors"

"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
Expand Down Expand Up @@ -43,12 +43,45 @@ func (r *RootCmd) templates() *clibase.Cmd {
r.templateVersions(),
r.templateDelete(),
r.templatePull(),
r.archiveTemplateVersions(),
},
}

return cmd
}

func selectTemplate(inv *clibase.Invocation, client *codersdk.Client, organization codersdk.Organization) (codersdk.Template, error) {
var empty codersdk.Template
ctx := inv.Context()
allTemplates, err := client.TemplatesByOrganization(ctx, organization.ID)
if err != nil {
return empty, xerrors.Errorf("get templates by organization: %w", err)
}

if len(allTemplates) == 0 {
return empty, xerrors.Errorf("no templates exist in the current organization %q", organization.Name)
}

opts := make([]string, 0, len(allTemplates))
for _, template := range allTemplates {
opts = append(opts, template.Name)
}

selection, err := cliui.Select(inv, cliui.SelectOptions{
Options: opts,
})
if err != nil {
return empty, xerrors.Errorf("select template: %w", err)
}

for _, template := range allTemplates {
if template.Name == selection {
return template, nil
}
}
return empty, xerrors.Errorf("no template selected")
}

type templateTableRow struct {
// Used by json format:
Template codersdk.Template
Expand Down
184 changes: 184 additions & 0 deletions cli/templateversionarchive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package cli

import (
"encoding/json"
"fmt"
"strings"
"time"

"github.com/coder/pretty"
"golang.org/x/xerrors"

"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
)

func (r *RootCmd) unarchiveTemplateVersion() *clibase.Cmd {
return r.setArchiveTemplateVersion(false)
}

func (r *RootCmd) archiveTemplateVersion() *clibase.Cmd {
return r.setArchiveTemplateVersion(true)
}

//nolint:revive
func (r *RootCmd) setArchiveTemplateVersion(archive bool) *clibase.Cmd {
presentVerb := "archive"
pastVerb := "archived"
if !archive {
presentVerb = "unarchive"
pastVerb = "unarchived"
}

client := new(codersdk.Client)
cmd := &clibase.Cmd{
Use: presentVerb + " <template-name> [template-version-names...] ",
Short: strings.ToUpper(string(presentVerb[0])) + presentVerb[1:] + " a template version(s).",
Middleware: clibase.Chain(
r.InitClient(client),
),
Options: clibase.OptionSet{
cliui.SkipPromptOption(),
},
Handler: func(inv *clibase.Invocation) error {
var (
ctx = inv.Context()
versions []codersdk.TemplateVersion
)

organization, err := CurrentOrganization(inv, client)
if err != nil {
return err
}

if len(inv.Args) == 0 {
return xerrors.Errorf("missing template name")
}
if len(inv.Args) < 2 {
return xerrors.Errorf("missing template version name(s)")
}

templateName := inv.Args[0]
template, err := client.TemplateByName(ctx, organization.ID, templateName)
if err != nil {
return xerrors.Errorf("get template by name: %w", err)
}
for _, versionName := range inv.Args[1:] {
version, err := client.TemplateVersionByOrganizationAndName(ctx, organization.ID, template.Name, versionName)
if err != nil {
return xerrors.Errorf("get template version by name %q: %w", versionName, err)
}
versions = append(versions, version)
}

for _, version := range versions {
if version.Archived == archive {
_, _ = fmt.Fprintln(
inv.Stdout, fmt.Sprintf("Version "+pretty.Sprint(cliui.DefaultStyles.Keyword, version.Name)+" already "+pastVerb),
)
continue
}

err := client.SetArchiveTemplateVersion(ctx, version.ID, archive)
if err != nil {
return xerrors.Errorf("%s template version %q: %w", presentVerb, version.Name, err)
}

_, _ = fmt.Fprintln(
inv.Stdout, fmt.Sprintf("Version "+pretty.Sprint(cliui.DefaultStyles.Keyword, version.Name)+" "+pastVerb+" at "+cliui.Timestamp(time.Now())),
)
}
return nil
},
}

return cmd
}

func (r *RootCmd) archiveTemplateVersions() *clibase.Cmd {
var all clibase.Bool
client := new(codersdk.Client)
cmd := &clibase.Cmd{
Use: "archive [template-name...] ",
Short: "Archive unused or failed template versions from a given template(s)",
Middleware: clibase.Chain(
r.InitClient(client),
),
Options: clibase.OptionSet{
cliui.SkipPromptOption(),
clibase.Option{
Name: "all",
Description: "Include all unused template versions. By default, only failed template versions are archived.",
Flag: "all",
Value: &all,
},
},
Handler: func(inv *clibase.Invocation) error {
var (
ctx = inv.Context()
templateNames = []string{}
templates = []codersdk.Template{}
)

organization, err := CurrentOrganization(inv, client)
if err != nil {
return err
}

if len(inv.Args) > 0 {
templateNames = inv.Args

for _, templateName := range templateNames {
template, err := client.TemplateByName(ctx, organization.ID, templateName)
if err != nil {
return xerrors.Errorf("get template by name: %w", err)
}
templates = append(templates, template)
}
} else {
template, err := selectTemplate(inv, client, organization)
if err != nil {
return err
}

templates = append(templates, template)
templateNames = append(templateNames, template.Name)
}

// Confirm archive of the template.
_, err = cliui.Prompt(inv, cliui.PromptOptions{
Text: fmt.Sprintf("Archive template versions of these templates: %s?", pretty.Sprint(cliui.DefaultStyles.Code, strings.Join(templateNames, ", "))),
IsConfirm: true,
Default: cliui.ConfirmNo,
})
if err != nil {
return err
}

for _, template := range templates {
resp, err := client.ArchiveTemplateVersions(ctx, template.ID, all.Value())
if err != nil {
return xerrors.Errorf("archive template %q: %w", template.Name, err)
}

_, _ = fmt.Fprintln(
inv.Stdout, fmt.Sprintf("Archived %d versions from "+pretty.Sprint(cliui.DefaultStyles.Keyword, template.Name)+" at "+cliui.Timestamp(time.Now()), len(resp.ArchivedIDs)),
)

if ok, _ := inv.ParsedFlags().GetBool("verbose"); err == nil && ok {
data, err := json.Marshal(resp)
if err != nil {
return xerrors.Errorf("marshal verbose response: %w", err)
}
_, _ = fmt.Fprintln(
inv.Stdout, string(data),
)
}
}
return nil
},
}

return cmd
}
108 changes: 108 additions & 0 deletions cli/templateversionarchive_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package cli_test

import (
"testing"

"github.com/google/uuid"
"github.com/stretchr/testify/require"

"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/testutil"
)

func TestTemplateVersionsArchive(t *testing.T) {
t.Parallel()
t.Run("Archive-Unarchive", func(t *testing.T) {
t.Parallel()
ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, ownerClient)

client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
other := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil, func(request *codersdk.CreateTemplateVersionRequest) {
request.TemplateID = template.ID
})
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, other.ID)

// Archive
inv, root := clitest.New(t, "templates", "versions", "archive", template.Name, other.Name, "-y")
clitest.SetupConfig(t, client, root)
w := clitest.StartWithWaiter(t, inv)
w.RequireSuccess()

// Verify archived
ctx := testutil.Context(t, testutil.WaitMedium)
found, err := client.TemplateVersion(ctx, other.ID)
require.NoError(t, err)
require.True(t, found.Archived, "expect archived")

// Unarchive
inv, root = clitest.New(t, "templates", "versions", "unarchive", template.Name, other.Name, "-y")
clitest.SetupConfig(t, client, root)
w = clitest.StartWithWaiter(t, inv)
w.RequireSuccess()

// Verify unarchived
ctx = testutil.Context(t, testutil.WaitMedium)
found, err = client.TemplateVersion(ctx, other.ID)
require.NoError(t, err)
require.False(t, found.Archived, "expect unarchived")
})

t.Run("ArchiveMany", func(t *testing.T) {
t.Parallel()
ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, ownerClient)

client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

// Add a failed
expArchived := map[uuid.UUID]bool{}
failed := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyFailed,
ProvisionPlan: echo.PlanFailed,
}, func(request *codersdk.CreateTemplateVersionRequest) {
request.TemplateID = template.ID
})
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, failed.ID)
expArchived[failed.ID] = true
// Add unused
unused := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil, func(request *codersdk.CreateTemplateVersionRequest) {
request.TemplateID = template.ID
})
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, unused.ID)
expArchived[unused.ID] = true

// Archive all unused versions
inv, root := clitest.New(t, "templates", "archive", template.Name, "-y", "--all")
clitest.SetupConfig(t, client, root)
w := clitest.StartWithWaiter(t, inv)
w.RequireSuccess()

ctx := testutil.Context(t, testutil.WaitMedium)
all, err := client.TemplateVersionsByTemplate(ctx, codersdk.TemplateVersionsByTemplateRequest{
TemplateID: template.ID,
IncludeArchived: true,
})
require.NoError(t, err, "query all versions")
for _, v := range all {
if _, ok := expArchived[v.ID]; ok {
require.True(t, v.Archived, "expect archived")
delete(expArchived, v.ID)
} else {
require.False(t, v.Archived, "expect unarchived")
}
}
require.Len(t, expArchived, 0, "expect all archived")
})
}
Loading