diff --git a/cli/templatedelete.go b/cli/templatedelete.go new file mode 100644 index 0000000000000..26ba6ca2a01f1 --- /dev/null +++ b/cli/templatedelete.go @@ -0,0 +1,85 @@ +package cli + +import ( + "fmt" + + "github.com/spf13/cobra" + "golang.org/x/xerrors" + + "github.com/coder/coder/cli/cliui" + "github.com/coder/coder/codersdk" +) + +func templateDelete() *cobra.Command { + return &cobra.Command{ + Use: "delete [name...]", + Short: "Delete templates", + RunE: func(cmd *cobra.Command, args []string) error { + var ( + ctx = cmd.Context() + templateNames = []string{} + templates = []codersdk.Template{} + ) + + client, err := createClient(cmd) + if err != nil { + return err + } + organization, err := currentOrganization(cmd, client) + if err != nil { + return err + } + + if len(args) > 0 { + templateNames = args + } else { + allTemplates, err := client.TemplatesByOrganization(ctx, organization.ID) + if err != nil { + return xerrors.Errorf("get templates by organization: %w", 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(cmd, 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) + } + } + } + + 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) + } + + for _, template := range templates { + err := client.DeleteTemplate(ctx, template.ID) + if err != nil { + return xerrors.Errorf("delete template %q: %w", template.Name, err) + } + + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Deleted template "+cliui.Styles.Code.Render(template.Name)+"!") + } + + return nil + }, + } +} diff --git a/cli/templatedelete_test.go b/cli/templatedelete_test.go new file mode 100644 index 0000000000000..d374585650afc --- /dev/null +++ b/cli/templatedelete_test.go @@ -0,0 +1,91 @@ +package cli_test + +import ( + "context" + "testing" + + "github.com/coder/coder/cli/clitest" + "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/codersdk" + "github.com/coder/coder/pty/ptytest" + "github.com/stretchr/testify/require" +) + +func TestTemplateDelete(t *testing.T) { + t.Parallel() + + t.Run("Ok", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + _ = coderdtest.NewProvisionerDaemon(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + cmd, root := clitest.New(t, "templates", "delete", template.Name) + clitest.SetupConfig(t, client, root) + require.NoError(t, cmd.Execute()) + + _, err := client.Template(context.Background(), template.ID) + require.Error(t, err, "template should not exist") + }) + + t.Run("Multiple", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + _ = coderdtest.NewProvisionerDaemon(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + templates := []codersdk.Template{ + coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID), + coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID), + coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID), + } + templateNames := []string{} + for _, template := range templates { + templateNames = append(templateNames, template.Name) + } + + cmd, root := clitest.New(t, append([]string{"templates", "delete"}, templateNames...)...) + clitest.SetupConfig(t, client, root) + require.NoError(t, cmd.Execute()) + + for _, template := range templates { + _, err := client.Template(context.Background(), template.ID) + require.Error(t, err, "template should not exist") + } + }) + + t.Run("Selector", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + _ = coderdtest.NewProvisionerDaemon(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + cmd, root := clitest.New(t, "templates", "delete") + clitest.SetupConfig(t, client, root) + + pty := ptytest.New(t) + cmd.SetIn(pty.Input()) + cmd.SetOut(pty.Output()) + + execDone := make(chan error) + go func() { + execDone <- cmd.Execute() + }() + + pty.WriteLine("docker-local") + require.NoError(t, <-execDone) + + _, err := client.Template(context.Background(), template.ID) + require.Error(t, err, "template should not exist") + }) +} diff --git a/cli/templates.go b/cli/templates.go index de1a9bcc1478d..45f6224369ba8 100644 --- a/cli/templates.go +++ b/cli/templates.go @@ -30,6 +30,7 @@ func templates() *cobra.Command { templatePlan(), templateUpdate(), templateVersions(), + templateDelete(), ) return cmd diff --git a/coderd/httpmw/templateparam.go b/coderd/httpmw/templateparam.go index 48adb95ff2ae8..f5d1ffcef127a 100644 --- a/coderd/httpmw/templateparam.go +++ b/coderd/httpmw/templateparam.go @@ -45,6 +45,13 @@ func ExtractTemplateParam(db database.Store) func(http.Handler) http.Handler { return } + if template.Deleted { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: fmt.Sprintf("template %q does not exist", templateID), + }) + return + } + ctx := context.WithValue(r.Context(), templateParamContextKey{}, template) chi.RouteContext(ctx).URLParams.Add("organization", template.OrganizationID.String()) next.ServeHTTP(rw, r.WithContext(ctx)) diff --git a/coderd/templates.go b/coderd/templates.go index 153c8aba9c122..12958c0860070 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -18,6 +18,7 @@ import ( // Returns a single template. func (api *api) template(rw http.ResponseWriter, r *http.Request) { template := httpmw.TemplateParam(r) + workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByTemplateIDs(r.Context(), []uuid.UUID{template.ID}) if errors.Is(err, sql.ErrNoRows) { err = nil