Skip to content

Code for extension install and delete #228

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
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ gen-demo:
lint: $(GOLANGCI_LINT)
$(GOLANGCI_LINT) --timeout 3m run

.PHONY: fix-lint-issues
lint-fix: $(GOLANGCI_LINT) #HELP Run golangci linter.
$(GOLANGCI_LINT) run --fix --timeout 3m

.PHONY: release
RELEASE_ARGS?=release --clean --snapshot
release: $(GORELEASER)
Expand Down
50 changes: 50 additions & 0 deletions internal/cmd/internal/olmv1/extension_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"
)

func NewExtensionDeleteCmd(cfg *action.Configuration) *cobra.Command {
e := v1action.NewExtensionDelete(cfg)
e.Logf = log.Printf

cmd := &cobra.Command{
Use: "extension [extension_name]",
Aliases: []string{"extensions [extension_name]"},
Short: "Delete an extension",
Long: `Warning: Permanently deletes the named cluster extension object.
If the extension contains CRDs, the CRDs will be deleted, which
cascades to the deletion of all operands.`,
Args: cobra.RangeArgs(0, 1),
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
extensions, err := e.Run(cmd.Context())
if err != nil {
log.Fatalf("failed deleting extension: %v", err)
}
for _, extn := range extensions {
log.Printf("extension %q deleted", extn)
}

return
}
e.ExtensionName = args[0]
_, errs := e.Run(cmd.Context())
if errs != nil {
log.Fatalf("delete extension: %v", errs)
}
log.Printf("deleted extension %q", e.ExtensionName)
},
}
bindExtensionDeleteFlags(cmd.Flags(), e)
return cmd
}

func bindExtensionDeleteFlags(fs *pflag.FlagSet, e *v1action.ExtensionDeletion) {
fs.BoolVarP(&e.DeleteAll, "all", "a", false, "delete all extensions")
}
19 changes: 16 additions & 3 deletions internal/cmd/internal/olmv1/extension_install.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package olmv1

import (
"time"

"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"
Expand All @@ -13,18 +16,28 @@ func NewExtensionInstallCmd(cfg *action.Configuration) *cobra.Command {
i.Logf = log.Printf

cmd := &cobra.Command{
Use: "install <extension>",
Use: "extension <extension_name>",
Short: "Install an extension",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
i.Package = args[0]
i.ExtensionName = args[0]
_, err := i.Run(cmd.Context())
if err != nil {
log.Fatalf("failed to install extension: %v", err)
}
log.Printf("extension %q created", i.Package)
log.Printf("extension %q created", i.ExtensionName)
},
}
bindOperatorInstallFlags(cmd.Flags(), i)

return cmd
}

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a replacement for the global timeout flag?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I continued the catalog create code pattern. Currently the OLMv1 code do not use https://github.com/operator-framework/kubectl-operator/blob/main/internal/cmd/root.go#L36-#L38 global timeout.

}
34 changes: 0 additions & 34 deletions internal/cmd/internal/olmv1/extension_uninstall.go

This file was deleted.

15 changes: 12 additions & 3 deletions internal/cmd/olmv1.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ func newOlmV1Cmd(cfg *action.Configuration) *cobra.Command {
Short: "Delete a resource",
Long: "Delete a resource",
}
deleteCmd.AddCommand(olmv1.NewCatalogDeleteCmd(cfg))
deleteCmd.AddCommand(
olmv1.NewCatalogDeleteCmd(cfg),
olmv1.NewExtensionDeleteCmd(cfg),
)

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

installCmd := &cobra.Command{
Use: "install",
Short: "Install a resource",
Long: "Install a resource",
}
installCmd.AddCommand(olmv1.NewExtensionInstallCmd(cfg))

cmd.AddCommand(
olmv1.NewExtensionInstallCmd(cfg),
olmv1.NewExtensionUninstallCmd(cfg),
installCmd,
getCmd,
createCmd,
deleteCmd,
Expand Down
81 changes: 81 additions & 0 deletions internal/pkg/v1/action/extension_delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package action

import (
"context"
"errors"
"fmt"
"strings"

apierrors "k8s.io/apimachinery/pkg/api/errors"

olmv1 "github.com/operator-framework/operator-controller/api/v1"

"github.com/operator-framework/kubectl-operator/pkg/action"
)

// ExtensionDeletion deletes an extension or all extensions in the cluster
type ExtensionDeletion struct {
config *action.Configuration
ExtensionName string
DeleteAll bool
Logf func(string, ...interface{})
}

// NewExtensionDelete creates a new ExtensionDeletion action
// with the given configuration
// and a logger function that can be used to log messages
func NewExtensionDelete(cfg *action.Configuration) *ExtensionDeletion {
return &ExtensionDeletion{
config: cfg,
Logf: func(string, ...interface{}) {},
}
}

func (u *ExtensionDeletion) Run(ctx context.Context) ([]string, error) {
if u.DeleteAll && u.ExtensionName != "" {
return nil, fmt.Errorf("cannot specify both --all and an extension name")
}
if !u.DeleteAll {
return u.deleteExtension(ctx, u.ExtensionName)
}

// delete all existing extensions
return u.deleteAllExtensions(ctx)
}

// deleteExtension deletes a single extension in the cluster
func (u *ExtensionDeletion) deleteExtension(ctx context.Context, extName string) ([]string, error) {
op := &olmv1.ClusterExtension{}
op.SetName(extName)
op.SetGroupVersionKind(olmv1.GroupVersion.WithKind("ClusterExtension"))
lowerKind := strings.ToLower(op.GetObjectKind().GroupVersionKind().Kind)
err := u.config.Client.Delete(ctx, op)
if err != nil {
if !apierrors.IsNotFound(err) {
return []string{u.ExtensionName}, fmt.Errorf("delete %s %q: %v", lowerKind, op.GetName(), err)
}
return nil, err
}
// wait for deletion
return []string{u.ExtensionName}, waitForDeletion(ctx, u.config.Client, op)
}

// deleteAllExtensions deletes all extensions in the cluster
func (u *ExtensionDeletion) deleteAllExtensions(ctx context.Context) ([]string, error) {
var extensionList olmv1.ClusterExtensionList
if err := u.config.Client.List(ctx, &extensionList); err != nil {
return nil, err
}
if len(extensionList.Items) == 0 {
return nil, ErrNoResourcesFound
}
errs := make([]error, 0, len(extensionList.Items))
names := make([]string, 0, len(extensionList.Items))
for _, extension := range extensionList.Items {
names = append(names, extension.Name)
if _, err := u.deleteExtension(ctx, extension.Name); err != nil {
errs = append(errs, fmt.Errorf("failed deleting extension %q: %w", extension.Name, err))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Standard kubectl behavior is to treat deleting non-existent resources as a failed operation, except for delete --all. That being said, I'm fine with this to maintain consistency with the current implementation of catalog delete.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I am fine with either way. The catalog and extension create/install uses the same pattern now.

}
}
return names, errors.Join(errs...)
}
111 changes: 111 additions & 0 deletions internal/pkg/v1/action/extension_delete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
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"

olmv1 "github.com/operator-framework/operator-controller/api/v1"

internalaction "github.com/operator-framework/kubectl-operator/internal/pkg/v1/action"
"github.com/operator-framework/kubectl-operator/pkg/action"
)

var _ = Describe("ExtensionDelete", func() {
setupEnv := func(extensions ...client.Object) action.Configuration {
var cfg action.Configuration

sch, err := action.NewScheme()
Expect(err).To(BeNil())

cl := fake.NewClientBuilder().
WithObjects(extensions...).
WithScheme(sch).
Build()
cfg.Scheme = sch
cfg.Client = cl

return cfg
}

It("fails because of both extension name and --all specifier being present", func() {
cfg := setupEnv(setupTestExtensions(2)...)

deleter := internalaction.NewExtensionDelete(&cfg)
deleter.ExtensionName = "foo"
deleter.DeleteAll = true
extNames, err := deleter.Run(context.TODO())
Expect(err).NotTo(BeNil())
Expect(extNames).To(BeEmpty())

validateExistingExtensions(cfg.Client, []string{"ext1", "ext2"})
})

It("fails deleting a non-existent extensions", func() {
cfg := setupEnv(setupTestExtensions(2)...)

deleter := internalaction.NewExtensionDelete(&cfg)
deleter.ExtensionName = "does-not-exist"
extNames, err := deleter.Run(context.TODO())
Expect(err).NotTo(BeNil())
Expect(extNames).To(BeEmpty())

validateExistingExtensions(cfg.Client, []string{"ext1", "ext2"})
})

It("successfully deletes an existing extension", func() {
cfg := setupEnv(setupTestExtensions(3)...)

deleter := internalaction.NewExtensionDelete(&cfg)
deleter.ExtensionName = "ext2"
_, err := deleter.Run(context.TODO())
Expect(err).To(BeNil())

validateExistingExtensions(cfg.Client, []string{"ext1", "ext3"})
})

It("fails deleting all extensions because there are none", func() {
cfg := setupEnv()

deleter := internalaction.NewExtensionDelete(&cfg)
deleter.DeleteAll = true
extNames, err := deleter.Run(context.TODO())
Expect(err).NotTo(BeNil())
Expect(extNames).To(BeEmpty())

validateExistingExtensions(cfg.Client, []string{})
})

It("successfully deletes all extensions", func() {
cfg := setupEnv(setupTestExtensions(3)...)

deleter := internalaction.NewExtensionDelete(&cfg)
deleter.DeleteAll = true
extNames, err := deleter.Run(context.TODO())
Expect(err).To(BeNil())
Expect(extNames).To(ContainElements([]string{"ext1", "ext2", "ext3"}))

validateExistingExtensions(cfg.Client, []string{})
})
})

// validateExistingExtensions compares the names of the existing extensions with the wanted names
// and ensures that all wanted names are present in the existing extensions
func validateExistingExtensions(c client.Client, wantedNames []string) {
var extensionList olmv1.ClusterExtensionList
err := c.List(context.TODO(), &extensionList)
Expect(err).To(BeNil())

extensions := extensionList.Items
Expect(extensions).To(HaveLen(len(wantedNames)))
for _, wantedName := range wantedNames {
Expect(slices.ContainsFunc(extensions, func(ext olmv1.ClusterExtension) bool {
return ext.Name == wantedName
})).To(BeTrue())
}
}
Loading