Skip to content

Commit

Permalink
operator uninstall with operands
Browse files Browse the repository at this point in the history
Signed-off-by: Daniel Sover <dsover@redhat.com>
  • Loading branch information
exdx committed Apr 19, 2021
1 parent 9e5901c commit 6c6ab5b
Show file tree
Hide file tree
Showing 5 changed files with 324 additions and 85 deletions.
13 changes: 13 additions & 0 deletions internal/cmd/cmd_suite_test.go
@@ -0,0 +1,13 @@
package cmd_test

import (
"testing"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

func TestCommand(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Command Suite")
}
28 changes: 20 additions & 8 deletions internal/cmd/operator_uninstall.go
Expand Up @@ -6,32 +6,44 @@ import (

"github.com/operator-framework/kubectl-operator/internal/cmd/internal/log"
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"
)

func newOperatorUninstallCmd(cfg *action.Configuration) *cobra.Command {
u := internalaction.NewOperatorUninstall(cfg)
u.Logf = log.Printf
var operandStrategy string

cmd := &cobra.Command{
Use: "uninstall <operator>",
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]
u.OperandStrategy = operand.DeletionStrategy(operandStrategy)
if err := u.Run(cmd.Context()); err != nil {
log.Fatalf("uninstall operator: %v", err)
}
log.Printf("operator %q uninstalled", u.Package)
},
}
bindOperatorUninstallFlags(cmd.Flags(), u)
bindOperatorUninstallFlags(cmd.Flags(), &operandStrategy)
return cmd
}

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)")
func bindOperatorUninstallFlags(fs *pflag.FlagSet, strategy *string) {
fs.StringVarP(strategy, "operand-strategy", "os", "cancel", "determines how to handle operands when deleting the relevant operator")
}
222 changes: 222 additions & 0 deletions internal/cmd/operator_uninstall_test.go
@@ -0,0 +1,222 @@
package cmd_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"
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() {
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.Error()).To(ContainSubstring("package " + `"` + "redis" + `"` + " not found"))
})

It("should fail due to missing csv", func() {
// switch to missing csv
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.Error()).To(ContainSubstring("get subscription CSV"))
})

It("should fail due to invalid operand deletion strategy", func() {
uninstaller := internalaction.NewOperatorUninstall(&cfg)
uninstaller.Package = "etcd"
uninstaller.OperandStrategy = "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 = operand.Cancel
err := uninstaller.Run(context.TODO())
Expect(err.Error()).To(ContainSubstring("requires a operand deletion 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 = 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).Error()).To(ContainSubstring("not found"))

csvKey := types.NamespacedName{Name: "etcdoperator.v0.9.4-clusterwide", Namespace: "etcd-namespace"}
csv := &v1alpha1.ClusterServiceVersion{}
Expect(cfg.Client.Get(context.TODO(), csvKey, csv).Error()).To(ContainSubstring("not found"))
})

It("should delete sub, csv, and operands when delete strategy is set", func() {
uninstaller := internalaction.NewOperatorUninstall(&cfg)
uninstaller.Package = "etcd"
uninstaller.OperandStrategy = 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).Error()).To(ContainSubstring("not found"))

csvKey := types.NamespacedName{Name: "etcdoperator.v0.9.4-clusterwide", Namespace: "etcd-namespace"}
csv := &v1alpha1.ClusterServiceVersion{}
Expect(cfg.Client.Get(context.TODO(), csvKey, csv).Error()).To(ContainSubstring("not found"))

etcd1Key := types.NamespacedName{Name: "cluster1", Namespace: "ns1"}
Expect(cfg.Client.Get(context.TODO(), etcd1Key, etcdcluster1).Error()).To(ContainSubstring("not found"))

etcd2Key := types.NamespacedName{Name: "cluster2", Namespace: "ns2"}
Expect(cfg.Client.Get(context.TODO(), etcd2Key, etcdcluster2).Error()).To(ContainSubstring("not found"))

etcd3Key := types.NamespacedName{Name: "cluster3"}
Expect(cfg.Client.Get(context.TODO(), etcd3Key, etcdcluster3).Error()).To(ContainSubstring("not found"))
})
})

0 comments on commit 6c6ab5b

Please sign in to comment.