-
Notifications
You must be signed in to change notification settings - Fork 43
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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") | ||
} |
This file was deleted.
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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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...) | ||
} |
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) { | ||
LalatenduMohanty marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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()) | ||
} | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.