Skip to content

✨ Add command to delete an existing olmv1 catalog #219

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
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
50 changes: 50 additions & 0 deletions internal/cmd/internal/olmv1/catalog_delete.go
Original file line number Diff line number Diff line change
@@ -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")
}
8 changes: 8 additions & 0 deletions internal/cmd/olmv1.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions internal/pkg/v1/action/action_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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},
}
}
69 changes: 69 additions & 0 deletions internal/pkg/v1/action/catalog_delete.go
Original file line number Diff line number Diff line change
@@ -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)
}
110 changes: 110 additions & 0 deletions internal/pkg/v1/action/catalog_delete_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
}
17 changes: 0 additions & 17 deletions internal/pkg/v1/action/catalog_installed_get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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},
}
}
8 changes: 8 additions & 0 deletions internal/pkg/v1/action/errors.go
Original file line number Diff line number Diff line change
@@ -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")
)
17 changes: 17 additions & 0 deletions internal/pkg/v1/action/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package action

import (
"context"
"fmt"
"slices"
"time"

Expand Down Expand Up @@ -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()
Expand Down
9 changes: 0 additions & 9 deletions internal/pkg/v1/action/operator.go

This file was deleted.

Loading
Loading