Skip to content

Commit d30d85c

Browse files
committed
Add command to delete an existing olmv1 catalog
Signed-off-by: Artur Zych <[email protected]>
1 parent c788f7c commit d30d85c

11 files changed

+307
-58
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package olmv1
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
"github.com/spf13/pflag"
6+
7+
"github.com/operator-framework/kubectl-operator/internal/cmd/internal/log"
8+
v1action "github.com/operator-framework/kubectl-operator/internal/pkg/v1/action"
9+
"github.com/operator-framework/kubectl-operator/pkg/action"
10+
)
11+
12+
// NewCatalogDeleteCmd allows deleting either a single or all
13+
// existing catalogs
14+
func NewCatalogDeleteCmd(cfg *action.Configuration) *cobra.Command {
15+
d := v1action.NewCatalogDelete(cfg)
16+
d.Logf = log.Printf
17+
18+
cmd := &cobra.Command{
19+
Use: "catalog [catalog_name]",
20+
Aliases: []string{"catalogs [catalog_name]"},
21+
Args: cobra.RangeArgs(0, 1),
22+
Short: "Delete single or all catalogs",
23+
Run: func(cmd *cobra.Command, args []string) {
24+
if len(args) == 0 {
25+
cats, err := d.Run(cmd.Context())
26+
if err != nil {
27+
log.Fatalf("failed deleting catalogs: %v", err)
28+
}
29+
for _, cat := range cats {
30+
log.Printf("catalog %q deleted", cat)
31+
}
32+
33+
return
34+
}
35+
36+
d.CatalogName = args[0]
37+
if _, err := d.Run(cmd.Context()); err != nil {
38+
log.Fatalf("failed to delete catalog %q: %v", d.CatalogName, err)
39+
}
40+
log.Printf("catalog %q deleted", d.CatalogName)
41+
},
42+
}
43+
bindCatalogCreateFlags(cmd.Flags(), d)
44+
45+
return cmd
46+
}
47+
48+
func bindCatalogCreateFlags(fs *pflag.FlagSet, d *v1action.CatalogDelete) {
49+
fs.BoolVar(&d.DeleteAll, "all", false, "delete all catalogs")
50+
}

internal/cmd/olmv1.go

+10-2
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,26 @@ func newOlmV1Cmd(cfg *action.Configuration) *cobra.Command {
1616

1717
getCmd := &cobra.Command{
1818
Use: "get",
19-
Short: "Display one or many OLMv1-specific resource(s)",
20-
Long: "Display one or many OLMv1-specific resource(s)",
19+
Short: "Display one or many resource(s)",
20+
Long: "Display one or many resource(s)",
2121
}
2222
getCmd.AddCommand(
2323
olmv1.NewOperatorInstalledGetCmd(cfg),
2424
olmv1.NewCatalogInstalledGetCmd(cfg),
2525
)
2626

27+
deleteCmd := &cobra.Command{
28+
Use: "delete",
29+
Short: "Delete a resource",
30+
Long: "Delete a resource",
31+
}
32+
deleteCmd.AddCommand(olmv1.NewCatalogDeleteCmd(cfg))
33+
2734
cmd.AddCommand(
2835
olmv1.NewOperatorInstallCmd(cfg),
2936
olmv1.NewOperatorUninstallCmd(cfg),
3037
getCmd,
38+
deleteCmd,
3139
)
3240

3341
return cmd
+22-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,34 @@
11
package action_test
22

33
import (
4+
"fmt"
45
"testing"
56

67
. "github.com/onsi/ginkgo"
78
. "github.com/onsi/gomega"
9+
10+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
"sigs.k8s.io/controller-runtime/pkg/client"
12+
13+
olmv1catalogd "github.com/operator-framework/catalogd/api/v1"
814
)
915

1016
func TestCommand(t *testing.T) {
1117
RegisterFailHandler(Fail)
12-
RunSpecs(t, "Internal action Suite")
18+
RunSpecs(t, "Internal v1 action Suite")
19+
}
20+
21+
func setupTestCatalogs(n int) []client.Object {
22+
var result []client.Object
23+
for i := 1; i <= n; i++ {
24+
result = append(result, newClusterCatalog(fmt.Sprintf("cat%d", i)))
25+
}
26+
27+
return result
28+
}
29+
30+
func newClusterCatalog(name string) *olmv1catalogd.ClusterCatalog {
31+
return &olmv1catalogd.ClusterCatalog{
32+
ObjectMeta: metav1.ObjectMeta{Name: name},
33+
}
1334
}
+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package action
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
8+
olmv1catalogd "github.com/operator-framework/catalogd/api/v1"
9+
10+
"github.com/operator-framework/kubectl-operator/pkg/action"
11+
)
12+
13+
type CatalogDelete struct {
14+
config *action.Configuration
15+
CatalogName string
16+
DeleteAll bool
17+
18+
Logf func(string, ...interface{})
19+
}
20+
21+
func NewCatalogDelete(cfg *action.Configuration) *CatalogDelete {
22+
return &CatalogDelete{
23+
config: cfg,
24+
Logf: func(string, ...interface{}) {},
25+
}
26+
}
27+
28+
func (cd *CatalogDelete) Run(ctx context.Context) ([]string, error) {
29+
// validate
30+
if cd.DeleteAll && cd.CatalogName != "" {
31+
return nil, errNameAndSelector
32+
}
33+
34+
// delete single, specified catalog
35+
if !cd.DeleteAll {
36+
return nil, cd.deleteCatalog(ctx, cd.CatalogName)
37+
}
38+
39+
// delete all existing catalogs
40+
var catsList olmv1catalogd.ClusterCatalogList
41+
if err := cd.config.Client.List(ctx, &catsList); err != nil {
42+
return nil, err
43+
}
44+
if len(catsList.Items) == 0 {
45+
return nil, errNoResourcesFound
46+
}
47+
48+
errs := make([]error, 0, len(catsList.Items))
49+
names := make([]string, 0, len(catsList.Items))
50+
for _, cat := range catsList.Items {
51+
names = append(names, cat.Name)
52+
if err := cd.deleteCatalog(ctx, cat.Name); err != nil {
53+
errs = append(errs, fmt.Errorf("failed deleting catalog %q: %w", cat.Name, err))
54+
}
55+
}
56+
57+
return names, errors.Join(errs...)
58+
}
59+
60+
func (cd *CatalogDelete) deleteCatalog(ctx context.Context, name string) error {
61+
op := &olmv1catalogd.ClusterCatalog{}
62+
op.SetName(name)
63+
64+
if err := cd.config.Client.Delete(ctx, op); err != nil {
65+
return err
66+
}
67+
68+
return waitForDeletion(ctx, cd.config.Client, op)
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package action_test
2+
3+
import (
4+
"context"
5+
"slices"
6+
7+
. "github.com/onsi/ginkgo"
8+
. "github.com/onsi/gomega"
9+
10+
"sigs.k8s.io/controller-runtime/pkg/client"
11+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
12+
13+
olmv1catalogd "github.com/operator-framework/catalogd/api/v1"
14+
15+
internalaction "github.com/operator-framework/kubectl-operator/internal/pkg/v1/action"
16+
"github.com/operator-framework/kubectl-operator/pkg/action"
17+
)
18+
19+
var _ = Describe("CatalogDelete", func() {
20+
setupEnv := func(catalogs ...client.Object) action.Configuration {
21+
var cfg action.Configuration
22+
23+
sch, err := action.NewScheme()
24+
Expect(err).To(BeNil())
25+
26+
cl := fake.NewClientBuilder().
27+
WithObjects(catalogs...).
28+
WithScheme(sch).
29+
Build()
30+
cfg.Scheme = sch
31+
cfg.Client = cl
32+
33+
return cfg
34+
}
35+
36+
It("fails because of both resource name and --all specifier being present", func() {
37+
cfg := setupEnv(setupTestCatalogs(2)...)
38+
39+
deleter := internalaction.NewCatalogDelete(&cfg)
40+
deleter.CatalogName = "name"
41+
deleter.DeleteAll = true
42+
catNames, err := deleter.Run(context.TODO())
43+
Expect(err).NotTo(BeNil())
44+
Expect(catNames).To(BeEmpty())
45+
46+
validateExistingCatalogs(cfg.Client, []string{"cat1", "cat2"})
47+
})
48+
49+
It("fails deleting a non-existing catalog", func() {
50+
cfg := setupEnv(setupTestCatalogs(2)...)
51+
52+
deleter := internalaction.NewCatalogDelete(&cfg)
53+
deleter.CatalogName = "does-not-exist"
54+
catNames, err := deleter.Run(context.TODO())
55+
Expect(err).NotTo(BeNil())
56+
Expect(catNames).To(BeEmpty())
57+
58+
validateExistingCatalogs(cfg.Client, []string{"cat1", "cat2"})
59+
})
60+
61+
It("successfully deletes an existing catalog", func() {
62+
cfg := setupEnv(setupTestCatalogs(3)...)
63+
64+
deleter := internalaction.NewCatalogDelete(&cfg)
65+
deleter.CatalogName = "cat2"
66+
catNames, err := deleter.Run(context.TODO())
67+
Expect(err).To(BeNil())
68+
Expect(catNames).To(BeEmpty())
69+
70+
validateExistingCatalogs(cfg.Client, []string{"cat1", "cat3"})
71+
})
72+
73+
It("fails deleting catalogs because there are none", func() {
74+
cfg := setupEnv()
75+
76+
deleter := internalaction.NewCatalogDelete(&cfg)
77+
deleter.DeleteAll = true
78+
catNames, err := deleter.Run(context.TODO())
79+
Expect(err).NotTo(BeNil())
80+
Expect(catNames).To(BeEmpty())
81+
82+
validateExistingCatalogs(cfg.Client, []string{})
83+
})
84+
85+
It("successfully deletes all catalogs", func() {
86+
cfg := setupEnv(setupTestCatalogs(3)...)
87+
88+
deleter := internalaction.NewCatalogDelete(&cfg)
89+
deleter.DeleteAll = true
90+
catNames, err := deleter.Run(context.TODO())
91+
Expect(err).To(BeNil())
92+
Expect(catNames).To(ContainElements([]string{"cat1", "cat2", "cat3"}))
93+
94+
validateExistingCatalogs(cfg.Client, []string{})
95+
})
96+
})
97+
98+
func validateExistingCatalogs(c client.Client, wantedNames []string) {
99+
var catalogsList olmv1catalogd.ClusterCatalogList
100+
err := c.List(context.TODO(), &catalogsList)
101+
Expect(err).To(BeNil())
102+
103+
catalogs := catalogsList.Items
104+
Expect(catalogs).To(HaveLen(len(wantedNames)))
105+
for _, wantedName := range wantedNames {
106+
Expect(slices.ContainsFunc(catalogs, func(cat olmv1catalogd.ClusterCatalog) bool {
107+
return cat.Name == wantedName
108+
})).To(BeTrue())
109+
}
110+
}

internal/pkg/v1/action/catalog_installed_get_test.go

-17
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@ package action_test
22

33
import (
44
"context"
5-
"fmt"
65
"slices"
76

87
. "github.com/onsi/ginkgo"
98
. "github.com/onsi/gomega"
109

11-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1210
"sigs.k8s.io/controller-runtime/pkg/client"
1311
"sigs.k8s.io/controller-runtime/pkg/client/fake"
1412

@@ -82,18 +80,3 @@ var _ = Describe("CatalogInstalledGet", func() {
8280
Expect(operators).To(BeEmpty())
8381
})
8482
})
85-
86-
func setupTestCatalogs(n int) []client.Object {
87-
var result []client.Object
88-
for i := 1; i <= n; i++ {
89-
result = append(result, newClusterCatalog(fmt.Sprintf("cat%d", i)))
90-
}
91-
92-
return result
93-
}
94-
95-
func newClusterCatalog(name string) *olmv1catalogd.ClusterCatalog {
96-
return &olmv1catalogd.ClusterCatalog{
97-
ObjectMeta: metav1.ObjectMeta{Name: name},
98-
}
99-
}

internal/pkg/v1/action/errors.go

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package action
2+
3+
import "errors"
4+
5+
var (
6+
errNoResourcesFound = errors.New("no resources found")
7+
errNameAndSelector = errors.New("name cannot be provided when a selector is specified")
8+
)

internal/pkg/v1/action/helpers.go

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package action
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
apierrors "k8s.io/apimachinery/pkg/api/errors"
9+
"k8s.io/apimachinery/pkg/types"
10+
"k8s.io/apimachinery/pkg/util/wait"
11+
"sigs.k8s.io/controller-runtime/pkg/client"
12+
)
13+
14+
const pollInterval = 250 * time.Millisecond
15+
16+
func objectKeyForObject(obj client.Object) types.NamespacedName {
17+
return types.NamespacedName{
18+
Namespace: obj.GetNamespace(),
19+
Name: obj.GetName(),
20+
}
21+
}
22+
23+
func waitForDeletion(ctx context.Context, cl client.Client, obj client.Object) error {
24+
key := objectKeyForObject(obj)
25+
if err := wait.PollUntilContextCancel(ctx, pollInterval, true, func(conditionCtx context.Context) (bool, error) {
26+
if err := cl.Get(conditionCtx, key, obj); apierrors.IsNotFound(err) {
27+
return true, nil
28+
} else if err != nil {
29+
return false, err
30+
}
31+
return false, nil
32+
}); err != nil {
33+
return fmt.Errorf("waiting for deletion: %w", err)
34+
}
35+
36+
return nil
37+
}

internal/pkg/v1/action/operator.go

-9
This file was deleted.

internal/pkg/v1/action/operator_install.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func (i *OperatorInstall) Run(ctx context.Context) (*olmv1.ClusterExtension, err
5858
// All Types will exist, so Ready may have a false Status. So, wait until
5959
// Type=Ready,Status=True happens
6060

61-
if err := wait.PollUntilContextCancel(ctx, pollTimeout, true, func(conditionCtx context.Context) (bool, error) {
61+
if err := wait.PollUntilContextCancel(ctx, pollInterval, true, func(conditionCtx context.Context) (bool, error) {
6262
if err := i.config.Client.Get(conditionCtx, opKey, op); err != nil {
6363
return false, err
6464
}

0 commit comments

Comments
 (0)