From 27efe6e9dc6575bec060e84ee5fa14cefe1ed961 Mon Sep 17 00:00:00 2001 From: Artur Zych <5843875+azych@users.noreply.github.com> Date: Wed, 26 Feb 2025 11:33:58 +0100 Subject: [PATCH] Add command to delete single/all existing olmv1 catalog(s) Signed-off-by: Artur Zych <5843875+azych@users.noreply.github.com> --- internal/cmd/internal/olmv1/catalog_delete.go | 50 ++++++++ internal/cmd/olmv1.go | 8 ++ internal/pkg/v1/action/action_suite_test.go | 19 +++ internal/pkg/v1/action/catalog_delete.go | 69 +++++++++++ internal/pkg/v1/action/catalog_delete_test.go | 110 ++++++++++++++++++ .../v1/action/catalog_installed_get_test.go | 17 --- internal/pkg/v1/action/errors.go | 8 ++ internal/pkg/v1/action/helpers.go | 17 +++ internal/pkg/v1/action/operator.go | 9 -- internal/pkg/v1/action/operator_uninstall.go | 21 ---- 10 files changed, 281 insertions(+), 47 deletions(-) create mode 100644 internal/cmd/internal/olmv1/catalog_delete.go create mode 100644 internal/pkg/v1/action/catalog_delete.go create mode 100644 internal/pkg/v1/action/catalog_delete_test.go create mode 100644 internal/pkg/v1/action/errors.go delete mode 100644 internal/pkg/v1/action/operator.go diff --git a/internal/cmd/internal/olmv1/catalog_delete.go b/internal/cmd/internal/olmv1/catalog_delete.go new file mode 100644 index 00000000..c7206228 --- /dev/null +++ b/internal/cmd/internal/olmv1/catalog_delete.go @@ -0,0 +1,50 @@ +package olmv1 + +import ( + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/operator-framework/kubectl-operator/internal/cmd/internal/log" + v1action "github.com/operator-framework/kubectl-operator/internal/pkg/v1/action" + "github.com/operator-framework/kubectl-operator/pkg/action" +) + +// NewCatalogDeleteCmd allows deleting either a single or all +// existing catalogs +func NewCatalogDeleteCmd(cfg *action.Configuration) *cobra.Command { + d := v1action.NewCatalogDelete(cfg) + d.Logf = log.Printf + + cmd := &cobra.Command{ + Use: "catalog [catalog_name]", + Aliases: []string{"catalogs [catalog_name]"}, + Args: cobra.RangeArgs(0, 1), + Short: "Delete either a single or all of the existing catalogs", + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + catalogs, err := d.Run(cmd.Context()) + if err != nil { + log.Fatalf("failed deleting catalogs: %v", err) + } + for _, catalog := range catalogs { + log.Printf("catalog %q deleted", catalog) + } + + return + } + + d.CatalogName = args[0] + if _, err := d.Run(cmd.Context()); err != nil { + log.Fatalf("failed to delete catalog %q: %v", d.CatalogName, err) + } + log.Printf("catalog %q deleted", d.CatalogName) + }, + } + bindCatalogDeleteFlags(cmd.Flags(), d) + + return cmd +} + +func bindCatalogDeleteFlags(fs *pflag.FlagSet, d *v1action.CatalogDelete) { + fs.BoolVar(&d.DeleteAll, "all", false, "delete all catalogs") +} diff --git a/internal/cmd/olmv1.go b/internal/cmd/olmv1.go index 738be075..8e7c39d3 100644 --- a/internal/cmd/olmv1.go +++ b/internal/cmd/olmv1.go @@ -31,11 +31,19 @@ func newOlmV1Cmd(cfg *action.Configuration) *cobra.Command { } createCmd.AddCommand(olmv1.NewCatalogCreateCmd(cfg)) + deleteCmd := &cobra.Command{ + Use: "delete", + Short: "Delete a resource", + Long: "Delete a resource", + } + deleteCmd.AddCommand(olmv1.NewCatalogDeleteCmd(cfg)) + cmd.AddCommand( olmv1.NewOperatorInstallCmd(cfg), olmv1.NewOperatorUninstallCmd(cfg), getCmd, createCmd, + deleteCmd, ) return cmd diff --git a/internal/pkg/v1/action/action_suite_test.go b/internal/pkg/v1/action/action_suite_test.go index 96abf84d..b33fdd4b 100644 --- a/internal/pkg/v1/action/action_suite_test.go +++ b/internal/pkg/v1/action/action_suite_test.go @@ -2,12 +2,16 @@ package action_test import ( "context" + "fmt" "testing" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" + + olmv1catalogd "github.com/operator-framework/catalogd/api/v1" ) func TestCommand(t *testing.T) { @@ -44,3 +48,18 @@ func (mg *mockGetter) Get(ctx context.Context, key client.ObjectKey, obj client. mg.getCalled++ return mg.getErr } + +func setupTestCatalogs(n int) []client.Object { + var result []client.Object + for i := 1; i <= n; i++ { + result = append(result, newClusterCatalog(fmt.Sprintf("cat%d", i))) + } + + return result +} + +func newClusterCatalog(name string) *olmv1catalogd.ClusterCatalog { + return &olmv1catalogd.ClusterCatalog{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + } +} diff --git a/internal/pkg/v1/action/catalog_delete.go b/internal/pkg/v1/action/catalog_delete.go new file mode 100644 index 00000000..160016ff --- /dev/null +++ b/internal/pkg/v1/action/catalog_delete.go @@ -0,0 +1,69 @@ +package action + +import ( + "context" + "errors" + "fmt" + + olmv1catalogd "github.com/operator-framework/catalogd/api/v1" + + "github.com/operator-framework/kubectl-operator/pkg/action" +) + +type CatalogDelete struct { + config *action.Configuration + CatalogName string + DeleteAll bool + + Logf func(string, ...interface{}) +} + +func NewCatalogDelete(cfg *action.Configuration) *CatalogDelete { + return &CatalogDelete{ + config: cfg, + Logf: func(string, ...interface{}) {}, + } +} + +func (cd *CatalogDelete) Run(ctx context.Context) ([]string, error) { + // validate + if cd.DeleteAll && cd.CatalogName != "" { + return nil, errNameAndSelector + } + + // delete single, specified catalog + if !cd.DeleteAll { + return nil, cd.deleteCatalog(ctx, cd.CatalogName) + } + + // delete all existing catalogs + var catatalogList olmv1catalogd.ClusterCatalogList + if err := cd.config.Client.List(ctx, &catatalogList); err != nil { + return nil, err + } + if len(catatalogList.Items) == 0 { + return nil, errNoResourcesFound + } + + errs := make([]error, 0, len(catatalogList.Items)) + names := make([]string, 0, len(catatalogList.Items)) + for _, catalog := range catatalogList.Items { + names = append(names, catalog.Name) + if err := cd.deleteCatalog(ctx, catalog.Name); err != nil { + errs = append(errs, fmt.Errorf("failed deleting catalog %q: %w", catalog.Name, err)) + } + } + + return names, errors.Join(errs...) +} + +func (cd *CatalogDelete) deleteCatalog(ctx context.Context, name string) error { + op := &olmv1catalogd.ClusterCatalog{} + op.SetName(name) + + if err := cd.config.Client.Delete(ctx, op); err != nil { + return err + } + + return waitForDeletion(ctx, cd.config.Client, op) +} diff --git a/internal/pkg/v1/action/catalog_delete_test.go b/internal/pkg/v1/action/catalog_delete_test.go new file mode 100644 index 00000000..9410c958 --- /dev/null +++ b/internal/pkg/v1/action/catalog_delete_test.go @@ -0,0 +1,110 @@ +package action_test + +import ( + "context" + "slices" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + olmv1catalogd "github.com/operator-framework/catalogd/api/v1" + + internalaction "github.com/operator-framework/kubectl-operator/internal/pkg/v1/action" + "github.com/operator-framework/kubectl-operator/pkg/action" +) + +var _ = Describe("CatalogDelete", func() { + setupEnv := func(catalogs ...client.Object) action.Configuration { + var cfg action.Configuration + + sch, err := action.NewScheme() + Expect(err).To(BeNil()) + + cl := fake.NewClientBuilder(). + WithObjects(catalogs...). + WithScheme(sch). + Build() + cfg.Scheme = sch + cfg.Client = cl + + return cfg + } + + It("fails because of both resource name and --all specifier being present", func() { + cfg := setupEnv(setupTestCatalogs(2)...) + + deleter := internalaction.NewCatalogDelete(&cfg) + deleter.CatalogName = "name" + deleter.DeleteAll = true + catNames, err := deleter.Run(context.TODO()) + Expect(err).NotTo(BeNil()) + Expect(catNames).To(BeEmpty()) + + validateExistingCatalogs(cfg.Client, []string{"cat1", "cat2"}) + }) + + It("fails deleting a non-existing catalog", func() { + cfg := setupEnv(setupTestCatalogs(2)...) + + deleter := internalaction.NewCatalogDelete(&cfg) + deleter.CatalogName = "does-not-exist" + catNames, err := deleter.Run(context.TODO()) + Expect(err).NotTo(BeNil()) + Expect(catNames).To(BeEmpty()) + + validateExistingCatalogs(cfg.Client, []string{"cat1", "cat2"}) + }) + + It("successfully deletes an existing catalog", func() { + cfg := setupEnv(setupTestCatalogs(3)...) + + deleter := internalaction.NewCatalogDelete(&cfg) + deleter.CatalogName = "cat2" + catNames, err := deleter.Run(context.TODO()) + Expect(err).To(BeNil()) + Expect(catNames).To(BeEmpty()) + + validateExistingCatalogs(cfg.Client, []string{"cat1", "cat3"}) + }) + + It("fails deleting catalogs because there are none", func() { + cfg := setupEnv() + + deleter := internalaction.NewCatalogDelete(&cfg) + deleter.DeleteAll = true + catNames, err := deleter.Run(context.TODO()) + Expect(err).NotTo(BeNil()) + Expect(catNames).To(BeEmpty()) + + validateExistingCatalogs(cfg.Client, []string{}) + }) + + It("successfully deletes all catalogs", func() { + cfg := setupEnv(setupTestCatalogs(3)...) + + deleter := internalaction.NewCatalogDelete(&cfg) + deleter.DeleteAll = true + catNames, err := deleter.Run(context.TODO()) + Expect(err).To(BeNil()) + Expect(catNames).To(ContainElements([]string{"cat1", "cat2", "cat3"})) + + validateExistingCatalogs(cfg.Client, []string{}) + }) +}) + +func validateExistingCatalogs(c client.Client, wantedNames []string) { + var catalogsList olmv1catalogd.ClusterCatalogList + err := c.List(context.TODO(), &catalogsList) + Expect(err).To(BeNil()) + + catalogs := catalogsList.Items + Expect(catalogs).To(HaveLen(len(wantedNames))) + for _, wantedName := range wantedNames { + Expect(slices.ContainsFunc(catalogs, func(cat olmv1catalogd.ClusterCatalog) bool { + return cat.Name == wantedName + })).To(BeTrue()) + } +} diff --git a/internal/pkg/v1/action/catalog_installed_get_test.go b/internal/pkg/v1/action/catalog_installed_get_test.go index b9a77f1d..77a83448 100644 --- a/internal/pkg/v1/action/catalog_installed_get_test.go +++ b/internal/pkg/v1/action/catalog_installed_get_test.go @@ -2,13 +2,11 @@ package action_test import ( "context" - "fmt" "slices" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" @@ -82,18 +80,3 @@ var _ = Describe("CatalogInstalledGet", func() { Expect(operators).To(BeEmpty()) }) }) - -func setupTestCatalogs(n int) []client.Object { - var result []client.Object - for i := 1; i <= n; i++ { - result = append(result, newClusterCatalog(fmt.Sprintf("cat%d", i))) - } - - return result -} - -func newClusterCatalog(name string) *olmv1catalogd.ClusterCatalog { - return &olmv1catalogd.ClusterCatalog{ - ObjectMeta: metav1.ObjectMeta{Name: name}, - } -} diff --git a/internal/pkg/v1/action/errors.go b/internal/pkg/v1/action/errors.go new file mode 100644 index 00000000..d8cea85c --- /dev/null +++ b/internal/pkg/v1/action/errors.go @@ -0,0 +1,8 @@ +package action + +import "errors" + +var ( + errNoResourcesFound = errors.New("no resources found") + errNameAndSelector = errors.New("name cannot be provided when a selector is specified") +) diff --git a/internal/pkg/v1/action/helpers.go b/internal/pkg/v1/action/helpers.go index a27ad405..66069b50 100644 --- a/internal/pkg/v1/action/helpers.go +++ b/internal/pkg/v1/action/helpers.go @@ -2,6 +2,7 @@ package action import ( "context" + "fmt" "slices" "time" @@ -45,6 +46,22 @@ func waitUntilCatalogStatusCondition( }) } +func waitForDeletion(ctx context.Context, cl client.Client, obj client.Object) error { + key := objectKeyForObject(obj) + if err := wait.PollUntilContextCancel(ctx, pollInterval, true, func(conditionCtx context.Context) (bool, error) { + if err := cl.Get(conditionCtx, key, obj); apierrors.IsNotFound(err) { + return true, nil + } else if err != nil { + return false, err + } + return false, nil + }); err != nil { + return fmt.Errorf("waiting for deletion: %w", err) + } + + return nil +} + func deleteWithTimeout(cl deleter, obj client.Object, timeout time.Duration) error { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() diff --git a/internal/pkg/v1/action/operator.go b/internal/pkg/v1/action/operator.go deleted file mode 100644 index 37204bf9..00000000 --- a/internal/pkg/v1/action/operator.go +++ /dev/null @@ -1,9 +0,0 @@ -package action - -import ( - "time" -) - -const ( - pollTimeout = 250 * time.Millisecond -) diff --git a/internal/pkg/v1/action/operator_uninstall.go b/internal/pkg/v1/action/operator_uninstall.go index 16c24949..a6d504c4 100644 --- a/internal/pkg/v1/action/operator_uninstall.go +++ b/internal/pkg/v1/action/operator_uninstall.go @@ -7,8 +7,6 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/wait" - "sigs.k8s.io/controller-runtime/pkg/client" olmv1 "github.com/operator-framework/operator-controller/api/v1" @@ -42,22 +40,3 @@ func (u *OperatorUninstall) Run(ctx context.Context) error { } return waitForDeletion(ctx, u.config.Client, op) } - -func waitForDeletion(ctx context.Context, cl client.Client, objs ...client.Object) error { - for _, obj := range objs { - obj := obj - lowerKind := strings.ToLower(obj.GetObjectKind().GroupVersionKind().Kind) - key := objectKeyForObject(obj) - if err := wait.PollUntilContextCancel(ctx, pollTimeout, true, func(conditionCtx context.Context) (bool, error) { - if err := cl.Get(conditionCtx, key, obj); apierrors.IsNotFound(err) { - return true, nil - } else if err != nil { - return false, err - } - return false, nil - }); err != nil { - return fmt.Errorf("wait for %s %q deleted: %v", lowerKind, key.Name, err) - } - } - return nil -}