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 27, 2021
1 parent 9e5901c commit 94f4d23
Show file tree
Hide file tree
Showing 8 changed files with 443 additions and 85 deletions.
13 changes: 0 additions & 13 deletions internal/cmd/operator_list_operands.go
Expand Up @@ -6,7 +6,6 @@ import (
"fmt"
"io"
"os"
"sort"
"strings"
"text/tabwriter"
"time"
Expand Down Expand Up @@ -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)
}
Expand Down
23 changes: 19 additions & 4 deletions internal/cmd/operator_uninstall.go
Expand Up @@ -15,8 +15,24 @@ func newOperatorUninstallCmd(cfg *action.Configuration) *cobra.Command {

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 as well as the
relevant operatorgroup.
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 if one is found. 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.
Setting delete-operator-groups to true will delete the operatorgroup in the provided namespace if no other active subscriptions
are currently in that namespace. The operatorgroup will be removed even if the operator is not found.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
u.Package = args[0]
if err := u.Run(cmd.Context()); err != nil {
Expand All @@ -30,8 +46,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)")
}
13 changes: 13 additions & 0 deletions 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")
}
1 change: 0 additions & 1 deletion internal/pkg/action/constants.go
Expand Up @@ -2,5 +2,4 @@ package action

const (
csvKind = "ClusterServiceVersion"
crdKind = "CustomResourceDefinition"
)
165 changes: 98 additions & 67 deletions internal/pkg/action/operator_uninstall.go
Expand Up @@ -9,23 +9,21 @@ 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"
)

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 {
Expand All @@ -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)
Expand All @@ -69,62 +62,45 @@ func (u *OperatorUninstall) Run(ctx context.Context) error {
csv, csvName, err := u.getSubscriptionCSV(ctx, sub)
if err != nil && !apierrors.IsNotFound(err) {
if csvName == "" {
return fmt.Errorf("get subscription CSV: %v", err)
return fmt.Errorf("get subscription csv: %v", err)
}
return fmt.Errorf("get subscription CSV %q: %v", csvName, err)
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
}
}

// 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 {
// 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 only like the operatorgroup.
if csv == nil {
u.Logf("csv for package %q not found", u.Package)
} else {
if err := u.deleteCSVRelatedResources(ctx, csv); err != nil {
return 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)
}
// 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
}
}
}
if err := u.deleteOperatorGroup(ctx); err != nil {
return fmt.Errorf("delete operatorgroup: %v", err)
}
}

return nil
}

Expand Down Expand Up @@ -165,28 +141,75 @@ func (u *OperatorUninstall) getSubscriptionCSV(ctx context.Context, subscription
return csv, name, nil
}

func csvNameFromSubscription(subscription *v1alpha1.Subscription) string {
if subscription.Status.InstalledCSV != "" {
return subscription.Status.InstalledCSV
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)
}
return subscription.Status.CurrentCSV

// 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
}

// 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)
func (u *OperatorUninstall) deleteCSVRelatedResources(ctx context.Context, csv *v1alpha1.ClusterServiceVersion) error {
// create lister and list operands
lister := action.NewOperatorListOperands(u.config)
operands, err := lister.Run(ctx, u.Package)
if err != nil {
return fmt.Errorf("list operands for operator %q: %v", u.Package, 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 command with a different operand strategy", len(operands.Items), operand.Cancel)
}
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
}
return

u.Logf("uninstalled %q operator", u.Package)
return nil
}

func csvNameFromSubscription(subscription *v1alpha1.Subscription) string {
if subscription.Status.InstalledCSV != "" {
return subscription.Status.InstalledCSV
}
return subscription.Status.CurrentCSV
}

func contains(haystack []string, needle string) bool {
Expand All @@ -197,3 +220,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()
}

0 comments on commit 94f4d23

Please sign in to comment.