Skip to content

Commit 19f30e1

Browse files
Code for extension install and delete
Signed-off-by: Lalatendu Mohanty <[email protected]>
1 parent b496b10 commit 19f30e1

9 files changed

+366
-119
lines changed

Makefile

+4
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ gen-demo:
6060
lint: $(GOLANGCI_LINT)
6161
$(GOLANGCI_LINT) --timeout 3m run
6262

63+
.PHONY: fix-lint-issues
64+
lint-fix: $(GOLANGCI_LINT) #HELP Run golangci linter.
65+
$(GOLANGCI_LINT) run --fix --timeout 3m
66+
6367
.PHONY: release
6468
RELEASE_ARGS?=release --clean --snapshot
6569
release: $(GORELEASER)
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+
func NewExtensionDeleteCmd(cfg *action.Configuration) *cobra.Command {
13+
e := v1action.NewExtensionDelete(cfg)
14+
e.Logf = log.Printf
15+
16+
cmd := &cobra.Command{
17+
Use: "extension [extension_name]",
18+
Aliases: []string{"extensions [extension_name]"},
19+
Short: "Delete an extension",
20+
Long: `Warning: Permanently deletes the named cluster extension object.
21+
If the extension contains CRDs, the CRDs will be deleted, which
22+
cascades to the deletion of all operands.`,
23+
Args: cobra.RangeArgs(0, 1),
24+
Run: func(cmd *cobra.Command, args []string) {
25+
if len(args) == 0 {
26+
extensions, err := e.Run(cmd.Context())
27+
if err != nil {
28+
log.Fatalf("failed deleting extension: %v", err)
29+
}
30+
for _, extn := range extensions {
31+
log.Printf("extension %q deleted", extn)
32+
}
33+
34+
return
35+
}
36+
e.ExtensionName = args[0]
37+
_, errs := e.Run(cmd.Context())
38+
if errs != nil {
39+
log.Fatalf("delete extension: %v", errs)
40+
}
41+
log.Printf("deleted extension %q", e.ExtensionName)
42+
},
43+
}
44+
bindExtensionDeleteFlags(cmd.Flags(), e)
45+
return cmd
46+
}
47+
48+
func bindExtensionDeleteFlags(fs *pflag.FlagSet, e *v1action.ExtensionDeletion) {
49+
fs.BoolVarP(&e.DeleteAll, "all", "a", false, "delete all extensions")
50+
}

internal/cmd/internal/olmv1/extension_install.go

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package olmv1
22

33
import (
4+
"time"
5+
46
"github.com/spf13/cobra"
7+
"github.com/spf13/pflag"
58

69
"github.com/operator-framework/kubectl-operator/internal/cmd/internal/log"
710
v1action "github.com/operator-framework/kubectl-operator/internal/pkg/v1/action"
@@ -13,18 +16,28 @@ func NewExtensionInstallCmd(cfg *action.Configuration) *cobra.Command {
1316
i.Logf = log.Printf
1417

1518
cmd := &cobra.Command{
16-
Use: "install <extension>",
19+
Use: "extension <extension_name>",
1720
Short: "Install an extension",
1821
Args: cobra.ExactArgs(1),
1922
Run: func(cmd *cobra.Command, args []string) {
20-
i.Package = args[0]
23+
i.ExtensionName = args[0]
2124
_, err := i.Run(cmd.Context())
2225
if err != nil {
2326
log.Fatalf("failed to install extension: %v", err)
2427
}
25-
log.Printf("extension %q created", i.Package)
28+
log.Printf("extension %q created", i.ExtensionName)
2629
},
2730
}
31+
bindOperatorInstallFlags(cmd.Flags(), i)
2832

2933
return cmd
3034
}
35+
36+
func bindOperatorInstallFlags(fs *pflag.FlagSet, i *v1action.ExtensionInstall) {
37+
fs.StringVarP(&i.Namespace.Name, "namespace", "n", "", "namespace to install the operator in")
38+
fs.StringVarP(&i.PackageName, "package-name", "p", "", "package name of the operator to install")
39+
fs.StringSliceVarP(&i.Channels, "channels", "c", []string{}, "channels which would be to used for getting updates e.g --channels \"stable,dev-preview,preview\"")
40+
fs.StringVarP(&i.Version, "version", "v", "", "version (or version range) from which to resolve bundles")
41+
fs.StringVarP(&i.ServiceAccount, "service-account", "s", "default", "service account name to use for the extension installation")
42+
fs.DurationVarP(&i.CleanupTimeout, "cleanup-timeout", "d", time.Minute, "the amount of time to wait before cancelling cleanup after a failed creation attempt")
43+
}

internal/cmd/internal/olmv1/extension_uninstall.go

-34
This file was deleted.

internal/cmd/olmv1.go

+12-3
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ func newOlmV1Cmd(cfg *action.Configuration) *cobra.Command {
3636
Short: "Delete a resource",
3737
Long: "Delete a resource",
3838
}
39-
deleteCmd.AddCommand(olmv1.NewCatalogDeleteCmd(cfg))
39+
deleteCmd.AddCommand(
40+
olmv1.NewCatalogDeleteCmd(cfg),
41+
olmv1.NewExtensionDeleteCmd(cfg),
42+
)
4043

4144
updateCmd := &cobra.Command{
4245
Use: "update",
@@ -47,9 +50,15 @@ func newOlmV1Cmd(cfg *action.Configuration) *cobra.Command {
4750
olmv1.NewExtensionUpdateCmd(cfg),
4851
)
4952

53+
installCmd := &cobra.Command{
54+
Use: "install",
55+
Short: "Install a resource",
56+
Long: "Install a resource",
57+
}
58+
installCmd.AddCommand(olmv1.NewExtensionInstallCmd(cfg))
59+
5060
cmd.AddCommand(
51-
olmv1.NewExtensionInstallCmd(cfg),
52-
olmv1.NewExtensionUninstallCmd(cfg),
61+
installCmd,
5362
getCmd,
5463
createCmd,
5564
deleteCmd,
+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package action
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"strings"
8+
9+
apierrors "k8s.io/apimachinery/pkg/api/errors"
10+
11+
olmv1 "github.com/operator-framework/operator-controller/api/v1"
12+
13+
"github.com/operator-framework/kubectl-operator/pkg/action"
14+
)
15+
16+
// ExtensionDeletion deletes an extension or all extensions in the cluster
17+
type ExtensionDeletion struct {
18+
config *action.Configuration
19+
ExtensionName string
20+
DeleteAll bool
21+
Logf func(string, ...interface{})
22+
}
23+
24+
// NewExtensionDelete creates a new ExtensionDeletion action
25+
// with the given configuration
26+
// and a logger function that can be used to log messages
27+
func NewExtensionDelete(cfg *action.Configuration) *ExtensionDeletion {
28+
return &ExtensionDeletion{
29+
config: cfg,
30+
Logf: func(string, ...interface{}) {},
31+
}
32+
}
33+
34+
func (u *ExtensionDeletion) Run(ctx context.Context) ([]string, error) {
35+
if u.DeleteAll && u.ExtensionName != "" {
36+
return nil, fmt.Errorf("cannot specify both --all and an extension name")
37+
}
38+
if !u.DeleteAll {
39+
return u.deleteExtension(ctx, u.ExtensionName)
40+
}
41+
42+
// delete all existing extensions
43+
return u.deleteAllExtensions(ctx)
44+
}
45+
46+
// deleteExtension deletes a single extension in the cluster
47+
func (u *ExtensionDeletion) deleteExtension(ctx context.Context, extName string) ([]string, error) {
48+
op := &olmv1.ClusterExtension{}
49+
op.SetName(extName)
50+
op.SetGroupVersionKind(olmv1.GroupVersion.WithKind("ClusterExtension"))
51+
lowerKind := strings.ToLower(op.GetObjectKind().GroupVersionKind().Kind)
52+
err := u.config.Client.Delete(ctx, op)
53+
if err != nil {
54+
if !apierrors.IsNotFound(err) {
55+
return []string{u.ExtensionName}, fmt.Errorf("delete %s %q: %v", lowerKind, op.GetName(), err)
56+
}
57+
return nil, err
58+
}
59+
// wait for deletion
60+
return []string{u.ExtensionName}, waitForDeletion(ctx, u.config.Client, op)
61+
}
62+
63+
// deleteAllExtensions deletes all extensions in the cluster
64+
func (u *ExtensionDeletion) deleteAllExtensions(ctx context.Context) ([]string, error) {
65+
var extensionList olmv1.ClusterExtensionList
66+
if err := u.config.Client.List(ctx, &extensionList); err != nil {
67+
return nil, err
68+
}
69+
if len(extensionList.Items) == 0 {
70+
return nil, ErrNoResourcesFound
71+
}
72+
errs := make([]error, 0, len(extensionList.Items))
73+
names := make([]string, 0, len(extensionList.Items))
74+
for _, extension := range extensionList.Items {
75+
names = append(names, extension.Name)
76+
if _, err := u.deleteExtension(ctx, extension.Name); err != nil {
77+
errs = append(errs, fmt.Errorf("failed deleting extension %q: %w", extension.Name, err))
78+
}
79+
}
80+
return names, errors.Join(errs...)
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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+
olmv1 "github.com/operator-framework/operator-controller/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("ExtensionDelete", func() {
20+
setupEnv := func(extensions ...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(extensions...).
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 extension name and --all specifier being present", func() {
37+
cfg := setupEnv(setupTestExtensions(2)...)
38+
39+
deleter := internalaction.NewExtensionDelete(&cfg)
40+
deleter.ExtensionName = "foo"
41+
deleter.DeleteAll = true
42+
extNames, err := deleter.Run(context.TODO())
43+
Expect(err).NotTo(BeNil())
44+
Expect(extNames).To(BeEmpty())
45+
46+
validateExistingExtensions(cfg.Client, []string{"ext1", "ext2"})
47+
})
48+
49+
It("fails deleting a non-existent extensions", func() {
50+
cfg := setupEnv(setupTestExtensions(2)...)
51+
52+
deleter := internalaction.NewExtensionDelete(&cfg)
53+
deleter.ExtensionName = "does-not-exist"
54+
extNames, err := deleter.Run(context.TODO())
55+
Expect(err).NotTo(BeNil())
56+
Expect(extNames).To(BeEmpty())
57+
58+
validateExistingExtensions(cfg.Client, []string{"ext1", "ext2"})
59+
})
60+
61+
It("successfully deletes an existing extension", func() {
62+
cfg := setupEnv(setupTestExtensions(3)...)
63+
64+
deleter := internalaction.NewExtensionDelete(&cfg)
65+
deleter.ExtensionName = "ext2"
66+
_, err := deleter.Run(context.TODO())
67+
Expect(err).To(BeNil())
68+
69+
validateExistingExtensions(cfg.Client, []string{"ext1", "ext3"})
70+
})
71+
72+
It("fails deleting all extensions because there are none", func() {
73+
cfg := setupEnv()
74+
75+
deleter := internalaction.NewExtensionDelete(&cfg)
76+
deleter.DeleteAll = true
77+
extNames, err := deleter.Run(context.TODO())
78+
Expect(err).NotTo(BeNil())
79+
Expect(extNames).To(BeEmpty())
80+
81+
validateExistingExtensions(cfg.Client, []string{})
82+
})
83+
84+
It("successfully deletes all extensions", func() {
85+
cfg := setupEnv(setupTestExtensions(3)...)
86+
87+
deleter := internalaction.NewExtensionDelete(&cfg)
88+
deleter.DeleteAll = true
89+
extNames, err := deleter.Run(context.TODO())
90+
Expect(err).To(BeNil())
91+
Expect(extNames).To(ContainElements([]string{"ext1", "ext2", "ext3"}))
92+
93+
validateExistingExtensions(cfg.Client, []string{})
94+
})
95+
})
96+
97+
// validateExistingExtensions compares the names of the existing extensions with the wanted names
98+
// and ensures that all wanted names are present in the existing extensions
99+
func validateExistingExtensions(c client.Client, wantedNames []string) {
100+
var extensionList olmv1.ClusterExtensionList
101+
err := c.List(context.TODO(), &extensionList)
102+
Expect(err).To(BeNil())
103+
104+
extensions := extensionList.Items
105+
Expect(extensions).To(HaveLen(len(wantedNames)))
106+
for _, wantedName := range wantedNames {
107+
Expect(slices.ContainsFunc(extensions, func(ext olmv1.ClusterExtension) bool {
108+
return ext.Name == wantedName
109+
})).To(BeTrue())
110+
}
111+
}

0 commit comments

Comments
 (0)