From 8d6b086d7ba2be61c7e821b807c8b3834c6566a3 Mon Sep 17 00:00:00 2001 From: Daniel Sover Date: Fri, 16 Apr 2021 16:30:16 -0400 Subject: [PATCH] operator uninstall with operands Signed-off-by: Daniel Sover --- internal/cmd/operator_list_operands.go | 13 - internal/cmd/operator_uninstall.go | 19 +- internal/pkg/action/action_suite_test.go | 13 + internal/pkg/action/constants.go | 1 - internal/pkg/action/operator_uninstall.go | 154 ++++++----- .../pkg/action/operator_uninstall_test.go | 248 ++++++++++++++++++ internal/pkg/operand/strategy.go | 45 ++++ pkg/action/operator_list_operands.go | 16 ++ 8 files changed, 428 insertions(+), 81 deletions(-) create mode 100644 internal/pkg/action/action_suite_test.go create mode 100644 internal/pkg/action/operator_uninstall_test.go create mode 100644 internal/pkg/operand/strategy.go diff --git a/internal/cmd/operator_list_operands.go b/internal/cmd/operator_list_operands.go index 6a5255d..cd8079a 100644 --- a/internal/cmd/operator_list_operands.go +++ b/internal/cmd/operator_list_operands.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "os" - "sort" "strings" "text/tabwriter" "time" @@ -64,18 +63,6 @@ the operator's ClusterServiceVersion.`, return } - sort.Slice(operands.Items, func(i, j int) bool { - if operands.Items[i].GetAPIVersion() != operands.Items[j].GetAPIVersion() { - return operands.Items[i].GetAPIVersion() < operands.Items[j].GetAPIVersion() - } - if operands.Items[i].GetKind() != operands.Items[j].GetKind() { - return operands.Items[i].GetKind() < operands.Items[j].GetKind() - } - if operands.Items[i].GetNamespace() != operands.Items[j].GetNamespace() { - return operands.Items[i].GetNamespace() < operands.Items[j].GetNamespace() - } - return operands.Items[i].GetName() < operands.Items[j].GetName() - }) if err := writeOutput(os.Stdout, operands); err != nil { log.Fatal(err) } diff --git a/internal/cmd/operator_uninstall.go b/internal/cmd/operator_uninstall.go index 4e887a0..a060e4c 100644 --- a/internal/cmd/operator_uninstall.go +++ b/internal/cmd/operator_uninstall.go @@ -15,8 +15,20 @@ func newOperatorUninstallCmd(cfg *action.Configuration) *cobra.Command { cmd := &cobra.Command{ Use: "uninstall ", - Short: "Uninstall an operator", - Args: cobra.ExactArgs(1), + Short: "Uninstall an operator and operands", + Long: `Uninstall removes the subscription, operator and optionally operands managed by the operator. + +This command first finds and deletes the subscription associated with the operator. It then +lists all operands found throughout the cluster for the operator +specified on the command line. Since the scope of an operator is restricted by +its operator group, the this search will include namespace-scoped operands from the +operator group's target namespaces and all cluster-scoped operands. + +The operand-deletion strategy is then considered if any operands are found on-cluster. One of cancel|ignore|delete. +By default, the strategy is "cancel", which means that if any operands are found when deleting the operator abort the uninstall. +The "ignore" strategy keeps the operands on cluster and only deletes the operator itself. +The "delete" strategy deletes both the operands, and after they have finished finalizing, the operator itself.`, + Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { u.Package = args[0] if err := u.Run(cmd.Context()); err != nil { @@ -30,8 +42,7 @@ func newOperatorUninstallCmd(cfg *action.Configuration) *cobra.Command { } func bindOperatorUninstallFlags(fs *pflag.FlagSet, u *internalaction.OperatorUninstall) { - fs.BoolVarP(&u.DeleteAll, "delete-all", "X", false, "enable all delete flags") - fs.BoolVar(&u.DeleteCRDs, "delete-crds", false, "delete all owned CRDs and all CRs") fs.BoolVar(&u.DeleteOperatorGroups, "delete-operator-groups", false, "delete operator groups if no other operators remain") fs.StringSliceVar(&u.DeleteOperatorGroupNames, "delete-operator-group-names", nil, "specific operator group names to delete (only effective with --delete-operator-groups)") + fs.VarP(&u.OperandStrategy, "operand-strategy", "os", "determines how to handle operands when deleting the operator, one of cancel|ignore|delete (default cancel)") } diff --git a/internal/pkg/action/action_suite_test.go b/internal/pkg/action/action_suite_test.go new file mode 100644 index 0000000..40957f3 --- /dev/null +++ b/internal/pkg/action/action_suite_test.go @@ -0,0 +1,13 @@ +package action_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestCommand(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Internal action Suite") +} diff --git a/internal/pkg/action/constants.go b/internal/pkg/action/constants.go index 15b7310..9a1f366 100644 --- a/internal/pkg/action/constants.go +++ b/internal/pkg/action/constants.go @@ -2,5 +2,4 @@ package action const ( csvKind = "ClusterServiceVersion" - crdKind = "CustomResourceDefinition" ) diff --git a/internal/pkg/action/operator_uninstall.go b/internal/pkg/action/operator_uninstall.go index 1c70e56..c859e16 100644 --- a/internal/pkg/action/operator_uninstall.go +++ b/internal/pkg/action/operator_uninstall.go @@ -9,10 +9,10 @@ import ( "github.com/operator-framework/api/pkg/operators/v1alpha1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/operator-framework/kubectl-operator/internal/pkg/operand" "github.com/operator-framework/kubectl-operator/pkg/action" ) @@ -20,12 +20,10 @@ type OperatorUninstall struct { config *action.Configuration Package string - DeleteAll bool - DeleteCRDs bool + OperandStrategy operand.DeletionStrategy DeleteOperatorGroups bool DeleteOperatorGroupNames []string - - Logf func(string, ...interface{}) + Logf func(string, ...interface{}) } func NewOperatorUninstall(cfg *action.Configuration) *OperatorUninstall { @@ -44,11 +42,6 @@ func (e ErrPackageNotFound) Error() string { } func (u *OperatorUninstall) Run(ctx context.Context) error { - if u.DeleteAll { - u.DeleteCRDs = true - u.DeleteOperatorGroups = true - } - subs := v1alpha1.SubscriptionList{} if err := u.config.Client.List(ctx, &subs, client.InNamespace(u.config.Namespace)); err != nil { return fmt.Errorf("list subscriptions: %v", err) @@ -74,57 +67,77 @@ func (u *OperatorUninstall) Run(ctx context.Context) error { return fmt.Errorf("get subscription CSV %q: %v", csvName, err) } - // Deletion order: - // - // 1. Subscription to prevent further installs or upgrades of the operator while cleaning up. - // 2. CustomResourceDefinitions so the operator has a chance to handle CRs that have finalizers. - // 3. ClusterServiceVersion. OLM puts an ownerref on every namespaced resource to the CSV, - // and an owner label on every cluster scoped resource so they get gc'd on deletion. + /* + Deletion order: + 1. Subscription to prevent further installs or upgrades of the operator while cleaning up. + + If the CSV exists: + 2. Operands so the operator has a chance to handle CRs that have finalizers. + Note: the correct strategy must be chosen in order to process an opertor delete with operand on-cluster. + 3. ClusterServiceVersion. OLM puts an ownerref on every namespaced resource to the CSV, + and an owner label on every cluster scoped resource so they get gc'd on deletion. + + 4. OperatorGroup in the namespace if no other subscriptions are in that namespace and OperatorGroup deletion is specified + */ // Subscriptions can be deleted asynchronously. if err := u.deleteObjects(ctx, sub); err != nil { return err } - if csv != nil { - // Ensure CustomResourceDefinitions are deleted next, so that the operator - // has a chance to handle CRs that have finalizers. - if u.DeleteCRDs { - crds := getCRDs(csv) - if err := u.deleteObjects(ctx, crds...); err != nil { - return err + // If we could not find a csv associated with the subscription, that likely + // means there is no CSV associated with it yet. Delete non-CSV related items and return early. + if csv == nil { + u.Logf("csv for package %q not found", u.Package) + if u.DeleteOperatorGroups { + if err := u.deleteOperatorGroup(ctx); err != nil { + return fmt.Errorf("deleting operatorgroup: %v", err) } } + return nil + } - // OLM puts an ownerref on every namespaced resource to the CSV, - // and an owner label on every cluster scoped resource. When CSV is deleted - // kube and olm gc will remove all the referenced resources. - if err := u.deleteObjects(ctx, csv); err != nil { - return err - } + // create lister and list operands + lister := action.NewOperatorListOperands(u.config) + operands, err := lister.Run(ctx, u.Package) + if err != nil { + return fmt.Errorf("listing operands associated with operator %q: %v", u.Package, err) } - if u.DeleteOperatorGroups { - subs := v1alpha1.SubscriptionList{} - if err := u.config.Client.List(ctx, &subs, client.InNamespace(u.config.Namespace)); err != nil { - return fmt.Errorf("list subscriptions: %v", err) + switch u.OperandStrategy.Kind { + case operand.Cancel: + if len(operands.Items) > 0 { + return fmt.Errorf("%d operands exist and operand strategy %q is in use; "+ + "delete operands or re-run with a different operand strategy", len(operands.Items), operand.Cancel) } - // If there are no subscriptions left, delete the operator group(s). - if len(subs.Items) == 0 { - ogs := v1.OperatorGroupList{} - if err := u.config.Client.List(ctx, &ogs, client.InNamespace(u.config.Namespace)); err != nil { - return fmt.Errorf("list operatorgroups: %v", err) - } - for _, og := range ogs.Items { - og := og - if len(u.DeleteOperatorGroupNames) == 0 || contains(u.DeleteOperatorGroupNames, og.GetName()) { - if err := u.deleteObjects(ctx, &og); err != nil { - return err - } - } + case operand.Ignore: + for _, op := range operands.Items { + u.Logf("%s %q orphaned", strings.ToLower(op.GetKind()), prettyPrint(op)) + } + case operand.Delete: + for _, op := range operands.Items { + op := op + if err := u.deleteObjects(ctx, &op); err != nil { + return err } } + default: + return fmt.Errorf("unknown operand deletion strategy %q", u.OperandStrategy) + } + + // OLM puts an ownerref on every namespaced resource to the CSV, + // and an owner label on every cluster scoped resource. When CSV is deleted + // kube and olm gc will remove all the referenced resources. + if err := u.deleteObjects(ctx, csv); err != nil { + return err + } + + if u.DeleteOperatorGroups { + if err := u.deleteOperatorGroup(ctx); err != nil { + return fmt.Errorf("deleting operatorgroup: %v", err) + } } + return nil } @@ -165,6 +178,30 @@ func (u *OperatorUninstall) getSubscriptionCSV(ctx context.Context, subscription return csv, name, nil } +func (u *OperatorUninstall) deleteOperatorGroup(ctx context.Context) error { + subs := v1alpha1.SubscriptionList{} + if err := u.config.Client.List(ctx, &subs, client.InNamespace(u.config.Namespace)); err != nil { + return fmt.Errorf("list subscriptions: %v", err) + } + + // If there are no subscriptions left, delete the operator group(s). + if len(subs.Items) == 0 { + ogs := v1.OperatorGroupList{} + if err := u.config.Client.List(ctx, &ogs, client.InNamespace(u.config.Namespace)); err != nil { + return fmt.Errorf("list operatorgroups: %v", err) + } + for _, og := range ogs.Items { + og := og + if len(u.DeleteOperatorGroupNames) == 0 || contains(u.DeleteOperatorGroupNames, og.GetName()) { + if err := u.deleteObjects(ctx, &og); err != nil { + return err + } + } + } + } + return nil +} + func csvNameFromSubscription(subscription *v1alpha1.Subscription) string { if subscription.Status.InstalledCSV != "" { return subscription.Status.InstalledCSV @@ -172,23 +209,6 @@ func csvNameFromSubscription(subscription *v1alpha1.Subscription) string { return subscription.Status.CurrentCSV } -// getCRDs returns the list of CRDs required by a CSV. -func getCRDs(csv *v1alpha1.ClusterServiceVersion) (crds []client.Object) { - for _, resource := range csv.Status.RequirementStatus { - if resource.Kind == crdKind { - obj := &unstructured.Unstructured{} - obj.SetGroupVersionKind(schema.GroupVersionKind{ - Group: resource.Group, - Version: resource.Version, - Kind: resource.Kind, - }) - obj.SetName(resource.Name) - crds = append(crds, obj) - } - } - return -} - func contains(haystack []string, needle string) bool { for _, n := range haystack { if n == needle { @@ -197,3 +217,11 @@ func contains(haystack []string, needle string) bool { } return false } + +func prettyPrint(op unstructured.Unstructured) string { + namespaced := op.GetNamespace() != "" + if namespaced { + return fmt.Sprint(op.GetName() + "/" + op.GetNamespace()) + } + return op.GetName() +} diff --git a/internal/pkg/action/operator_uninstall_test.go b/internal/pkg/action/operator_uninstall_test.go new file mode 100644 index 0000000..d05a85f --- /dev/null +++ b/internal/pkg/action/operator_uninstall_test.go @@ -0,0 +1,248 @@ +package action_test + +import ( + "context" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + v1 "github.com/operator-framework/api/pkg/operators/v1" + "github.com/operator-framework/api/pkg/operators/v1alpha1" + corev1 "k8s.io/api/core/v1" + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + internalaction "github.com/operator-framework/kubectl-operator/internal/pkg/action" + "github.com/operator-framework/kubectl-operator/internal/pkg/operand" + "github.com/operator-framework/kubectl-operator/pkg/action" +) + +var _ = Describe("OperatorUninstall", func() { + const etcd = "etcd" + var ( + cfg action.Configuration + operator *v1.Operator + csv *v1alpha1.ClusterServiceVersion + crd *apiextv1.CustomResourceDefinition + og *v1.OperatorGroup + sub *v1alpha1.Subscription + etcdcluster1 *unstructured.Unstructured + etcdcluster2 *unstructured.Unstructured + etcdcluster3 *unstructured.Unstructured + ) + + BeforeEach(func() { + sch, err := action.NewScheme() + Expect(err).To(BeNil()) + + etcdclusterGVK := schema.GroupVersionKind{ + Group: "etcd.database.coreos.com", + Version: "v1beta2", + Kind: "EtcdCluster", + } + + sch.AddKnownTypeWithName(etcdclusterGVK, &unstructured.Unstructured{}) + sch.AddKnownTypeWithName(schema.GroupVersionKind{ + Group: "etcd.database.coreos.com", + Version: "v1beta2", + Kind: "EtcdClusterList", + }, &unstructured.UnstructuredList{}) + + operator = &v1.Operator{ + ObjectMeta: metav1.ObjectMeta{Name: "etcd.etcd-namespace"}, + Status: v1.OperatorStatus{ + Components: &v1.Components{ + Refs: []v1.RichReference{ + { + ObjectReference: &corev1.ObjectReference{ + APIVersion: "operators.coreos.com/v1alpha1", + Kind: "ClusterServiceVersion", + Name: "etcdoperator.v0.9.4-clusterwide", + Namespace: "etcd-namespace", + }, + }, + }, + }, + }, + } + + csv = &v1alpha1.ClusterServiceVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "etcdoperator.v0.9.4-clusterwide", + Namespace: "etcd-namespace", + }, + Spec: v1alpha1.ClusterServiceVersionSpec{ + CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ + Owned: []v1alpha1.CRDDescription{ + { + Name: "etcdclusters.etcd.database.coreos.com", + Version: "v1beta2", + Kind: "EtcdCluster", + }, + }, + }, + }, + Status: v1alpha1.ClusterServiceVersionStatus{Phase: v1alpha1.CSVPhaseSucceeded}, + } + + og = &v1.OperatorGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "etcd", + Namespace: "etcd-namespace", + }, + Status: v1.OperatorGroupStatus{Namespaces: []string{""}}, + } + + sub = &v1alpha1.Subscription{ + ObjectMeta: metav1.ObjectMeta{ + Name: "etcd-sub", + Namespace: "etcd-namespace", + }, + Spec: &v1alpha1.SubscriptionSpec{ + Package: "etcd", + }, + Status: v1alpha1.SubscriptionStatus{ + InstalledCSV: "etcdoperator.v0.9.4-clusterwide", + }, + } + + crd = &apiextv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "etcdclusters.etcd.database.coreos.com", + }, + Spec: apiextv1.CustomResourceDefinitionSpec{ + Group: "etcd.database.coreos.com", + Names: apiextv1.CustomResourceDefinitionNames{ + ListKind: "EtcdClusterList", + }, + }, + } + etcdcluster1 = &unstructured.Unstructured{} + etcdcluster1.SetGroupVersionKind(etcdclusterGVK) + etcdcluster1.SetNamespace("ns1") + etcdcluster1.SetName("cluster1") + + etcdcluster2 = &unstructured.Unstructured{} + etcdcluster2.SetGroupVersionKind(etcdclusterGVK) + etcdcluster2.SetNamespace("ns2") + etcdcluster2.SetName("cluster2") + + etcdcluster3 = &unstructured.Unstructured{} + etcdcluster3.SetGroupVersionKind(etcdclusterGVK) + // Empty namespace to simulate cluster-scoped object. + etcdcluster3.SetNamespace("") + etcdcluster3.SetName("cluster3") + + cl := fake.NewClientBuilder(). + WithObjects(operator, csv, og, sub, crd, etcdcluster1, etcdcluster2, etcdcluster3). + WithScheme(sch). + Build() + cfg.Scheme = sch + cfg.Client = cl + cfg.Namespace = "etcd-namespace" + }) + + It("should fail due to missing subscription", func() { + uninstaller := internalaction.NewOperatorUninstall(&cfg) + // switch to package without a subscription for it + uninstaller.Package = "redis" + err := uninstaller.Run(context.TODO()) + Expect(err).To(MatchError(&internalaction.ErrPackageNotFound{PackageName: "redis"})) + }) + + It("should fail due to missing csv", func() { + // switch to missing csv + // this is not an error condition, we simply delete the subscription and exit + sub.Status.InstalledCSV = "" + Expect(cfg.Client.Update(context.TODO(), sub)).To(Succeed()) + + uninstaller := internalaction.NewOperatorUninstall(&cfg) + uninstaller.Package = etcd + err := uninstaller.Run(context.TODO()) + Expect(err).To(BeNil()) + + subKey := types.NamespacedName{Name: "etcd-sub", Namespace: "etcd-namespace"} + s := &v1alpha1.Subscription{} + Expect(cfg.Client.Get(context.TODO(), subKey, s)).To(WithTransform(apierrors.IsNotFound, BeTrue())) + }) + + It("should fail due to invalid operand deletion strategy", func() { + uninstaller := internalaction.NewOperatorUninstall(&cfg) + uninstaller.Package = etcd + uninstaller.OperandStrategy.Kind = "foo" + err := uninstaller.Run(context.TODO()) + Expect(err.Error()).To(ContainSubstring("unknown operand deletion strategy")) + }) + + It("should error with operands on cluster when default cancel strategy is set", func() { + uninstaller := internalaction.NewOperatorUninstall(&cfg) + uninstaller.Package = etcd + uninstaller.OperandStrategy.Kind = operand.Cancel + err := uninstaller.Run(context.TODO()) + Expect(err.Error()).To(ContainSubstring("delete operands or re-run with a different operand strategy")) + }) + + It("should ignore operands and delete sub and csv when ignore strategy is set", func() { + uninstaller := internalaction.NewOperatorUninstall(&cfg) + uninstaller.Package = etcd + uninstaller.OperandStrategy.Kind = operand.Ignore + err := uninstaller.Run(context.TODO()) + Expect(err).To(BeNil()) + + subKey := types.NamespacedName{Name: "etcd-sub", Namespace: "etcd-namespace"} + s := &v1alpha1.Subscription{} + Expect(cfg.Client.Get(context.TODO(), subKey, s)).To(WithTransform(apierrors.IsNotFound, BeTrue())) + + csvKey := types.NamespacedName{Name: "etcdoperator.v0.9.4-clusterwide", Namespace: "etcd-namespace"} + csv := &v1alpha1.ClusterServiceVersion{} + Expect(cfg.Client.Get(context.TODO(), csvKey, csv)).To(WithTransform(apierrors.IsNotFound, BeTrue())) + }) + + It("should delete sub, csv, and operands when delete strategy is set", func() { + uninstaller := internalaction.NewOperatorUninstall(&cfg) + uninstaller.Package = etcd + uninstaller.OperandStrategy.Kind = operand.Delete + err := uninstaller.Run(context.TODO()) + Expect(err).To(BeNil()) + + subKey := types.NamespacedName{Name: "etcd-sub", Namespace: "etcd-namespace"} + s := &v1alpha1.Subscription{} + Expect(cfg.Client.Get(context.TODO(), subKey, s)).To(WithTransform(apierrors.IsNotFound, BeTrue())) + + csvKey := types.NamespacedName{Name: "etcdoperator.v0.9.4-clusterwide", Namespace: "etcd-namespace"} + csv := &v1alpha1.ClusterServiceVersion{} + Expect(cfg.Client.Get(context.TODO(), csvKey, csv)).To(WithTransform(apierrors.IsNotFound, BeTrue())) + + etcd1Key := types.NamespacedName{Name: "cluster1", Namespace: "ns1"} + Expect(cfg.Client.Get(context.TODO(), etcd1Key, etcdcluster1)).To(WithTransform(apierrors.IsNotFound, BeTrue())) + + etcd2Key := types.NamespacedName{Name: "cluster2", Namespace: "ns2"} + Expect(cfg.Client.Get(context.TODO(), etcd2Key, etcdcluster2)).To(WithTransform(apierrors.IsNotFound, BeTrue())) + + etcd3Key := types.NamespacedName{Name: "cluster3"} + Expect(cfg.Client.Get(context.TODO(), etcd3Key, etcdcluster3)).To(WithTransform(apierrors.IsNotFound, BeTrue())) + }) + It("should delete sub and operatorgroup when no CSV is found", func() { + uninstaller := internalaction.NewOperatorUninstall(&cfg) + uninstaller.Package = etcd + uninstaller.DeleteOperatorGroups = true + + sub.Status.InstalledCSV = "foo" // returns nil CSV + Expect(cfg.Client.Update(context.TODO(), sub)).To(Succeed()) + + err := uninstaller.Run(context.TODO()) + Expect(err).To(BeNil()) + + subKey := types.NamespacedName{Name: "etcd-sub", Namespace: "etcd-namespace"} + s := &v1alpha1.Subscription{} + Expect(cfg.Client.Get(context.TODO(), subKey, s)).To(WithTransform(apierrors.IsNotFound, BeTrue())) + + ogKey := types.NamespacedName{Name: "etcd", Namespace: "etcd-namespace"} + og := &v1.OperatorGroup{} + Expect(cfg.Client.Get(context.TODO(), ogKey, og)).To(WithTransform(apierrors.IsNotFound, BeTrue())) + }) +}) diff --git a/internal/pkg/operand/strategy.go b/internal/pkg/operand/strategy.go new file mode 100644 index 0000000..72440b9 --- /dev/null +++ b/internal/pkg/operand/strategy.go @@ -0,0 +1,45 @@ +package operand + +import ( + "flag" + "fmt" +) + +// DeletionStrategy describes how to handle operands on-cluster when deleting the associated operator. +type DeletionStrategy struct { + Kind DeletionStrategyKind +} + +var _ flag.Value = &DeletionStrategy{} + +type DeletionStrategyKind string + +const ( + // Cancel is the default deletion strategy: it will cancel the deletion operation if operands are on-cluster. + Cancel DeletionStrategyKind = "cancel" + // Ignore will ignore the operands when deleting the operator, in effect orphaning them. + Ignore DeletionStrategyKind = "ignore" + // Delete will delete the operands associated with the operator before deleting the operator, allowing finalizers to run. + Delete DeletionStrategyKind = "delete" +) + +func (d *DeletionStrategy) Set(str string) error { + d.Kind = DeletionStrategyKind(str) + return d.Valid() +} + +func (d *DeletionStrategy) String() string { + return string(d.Kind) +} + +func (d DeletionStrategy) Valid() error { + switch d.Kind { + case Cancel, Ignore, Delete: + return nil + } + return fmt.Errorf("unknown deletion strategy %q", d.Kind) +} + +func (d DeletionStrategy) Type() string { + return "DeletionStrategy" +} diff --git a/pkg/action/operator_list_operands.go b/pkg/action/operator_list_operands.go index 874ab87..38d5cb4 100644 --- a/pkg/action/operator_list_operands.go +++ b/pkg/action/operator_list_operands.go @@ -3,6 +3,7 @@ package action import ( "context" "fmt" + "sort" v1 "github.com/operator-framework/api/pkg/operators/v1" "github.com/operator-framework/api/pkg/operators/v1alpha1" @@ -166,6 +167,21 @@ func (o *OperatorListOperands) listAll(ctx context.Context, opKey types.Namespac } result.Items = append(result.Items, list.Items...) } + + // sort results + sort.Slice(result.Items, func(i, j int) bool { + if result.Items[i].GetAPIVersion() != result.Items[j].GetAPIVersion() { + return result.Items[i].GetAPIVersion() < result.Items[j].GetAPIVersion() + } + if result.Items[i].GetKind() != result.Items[j].GetKind() { + return result.Items[i].GetKind() < result.Items[j].GetKind() + } + if result.Items[i].GetNamespace() != result.Items[j].GetNamespace() { + return result.Items[i].GetNamespace() < result.Items[j].GetNamespace() + } + return result.Items[i].GetName() < result.Items[j].GetName() + }) + return &result, nil }