Skip to content

Commit f12f147

Browse files
authored
operator uninstall with operands (#45)
Signed-off-by: Daniel Sover <dsover@redhat.com>
1 parent 1a19c3e commit f12f147

File tree

10 files changed

+479
-86
lines changed

10 files changed

+479
-86
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.16
55
require (
66
github.com/containerd/containerd v1.4.3
77
github.com/onsi/ginkgo v1.14.1
8-
github.com/onsi/gomega v1.10.2
8+
github.com/onsi/gomega v1.11.0
99
github.com/opencontainers/image-spec v1.0.2-0.20190823105129-775207bd45b6
1010
github.com/operator-framework/api v0.7.1
1111
github.com/operator-framework/operator-lifecycle-manager v0.0.0-20200521062108-408ca95d458f

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,8 @@ github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoT
583583
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
584584
github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs=
585585
github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
586+
github.com/onsi/gomega v1.11.0 h1:+CqWgvj0OZycCaqclBD1pxKHAU+tOkHmQIWvDHq2aug=
587+
github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg=
586588
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
587589
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
588590
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
@@ -898,6 +900,8 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
898900
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
899901
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
900902
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
903+
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U=
904+
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
901905
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
902906
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
903907
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1145,6 +1149,8 @@ gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
11451149
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
11461150
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
11471151
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
1152+
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
1153+
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
11481154
gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
11491155
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
11501156
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/cmd/operator_list_operands.go

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"fmt"
77
"io"
88
"os"
9-
"sort"
109
"strings"
1110
"text/tabwriter"
1211
"time"
@@ -64,18 +63,6 @@ the operator's ClusterServiceVersion.`,
6463
return
6564
}
6665

67-
sort.Slice(operands.Items, func(i, j int) bool {
68-
if operands.Items[i].GetAPIVersion() != operands.Items[j].GetAPIVersion() {
69-
return operands.Items[i].GetAPIVersion() < operands.Items[j].GetAPIVersion()
70-
}
71-
if operands.Items[i].GetKind() != operands.Items[j].GetKind() {
72-
return operands.Items[i].GetKind() < operands.Items[j].GetKind()
73-
}
74-
if operands.Items[i].GetNamespace() != operands.Items[j].GetNamespace() {
75-
return operands.Items[i].GetNamespace() < operands.Items[j].GetNamespace()
76-
}
77-
return operands.Items[i].GetName() < operands.Items[j].GetName()
78-
})
7966
if err := writeOutput(os.Stdout, operands); err != nil {
8067
log.Fatal(err)
8168
}

internal/cmd/operator_uninstall.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,29 @@ func newOperatorUninstallCmd(cfg *action.Configuration) *cobra.Command {
1515

1616
cmd := &cobra.Command{
1717
Use: "uninstall <operator>",
18-
Short: "Uninstall an operator",
19-
Args: cobra.ExactArgs(1),
18+
Short: "Uninstall an operator and operands",
19+
Long: `Uninstall removes the subscription, operator and optionally operands managed by the operator as well as
20+
the relevant operatorgroup.
21+
22+
Warning: this command permanently deletes objects from the cluster. Running uninstall concurrently with other operations
23+
could result in undefined behavior.
24+
25+
The uninstall command first checks to find the subscription associated with the operator. It then
26+
lists all operands found throughout the cluster for the operator
27+
specified if one is found. Since the scope of an operator is restricted by
28+
its operator group, this search will include namespace-scoped operands from the
29+
operator group's target namespaces and all cluster-scoped operands.
30+
31+
The operand-deletion strategy is then considered if any operands are found on-cluster. One of cancel|ignore|delete.
32+
By default, the strategy is "cancel", which means that if any operands are found when deleting the operator abort the
33+
uninstall without deleting anything.
34+
The "ignore" strategy keeps the operands on cluster and deletes the subscription and the operator.
35+
The "delete" strategy deletes the subscription, operands, and after they have finished finalizing, the operator itself.
36+
37+
Setting --delete-operator-groups to true will delete the operatorgroup in the provided namespace if no other active
38+
subscriptions are currently in that namespace, after removing the operator. The subscription and operatorgroup will be
39+
removed even if the operator is not found.`,
40+
Args: cobra.ExactArgs(1),
2041
Run: func(cmd *cobra.Command, args []string) {
2142
u.Package = args[0]
2243
if err := u.Run(cmd.Context()); err != nil {
@@ -30,8 +51,7 @@ func newOperatorUninstallCmd(cfg *action.Configuration) *cobra.Command {
3051
}
3152

3253
func bindOperatorUninstallFlags(fs *pflag.FlagSet, u *internalaction.OperatorUninstall) {
33-
fs.BoolVarP(&u.DeleteAll, "delete-all", "X", false, "enable all delete flags")
34-
fs.BoolVar(&u.DeleteCRDs, "delete-crds", false, "delete all owned CRDs and all CRs")
3554
fs.BoolVar(&u.DeleteOperatorGroups, "delete-operator-groups", false, "delete operator groups if no other operators remain")
3655
fs.StringSliceVar(&u.DeleteOperatorGroupNames, "delete-operator-group-names", nil, "specific operator group names to delete (only effective with --delete-operator-groups)")
56+
fs.VarP(&u.OperandStrategy, "operand-strategy", "s", "determines how to handle operands when deleting the operator, one of cancel|ignore|delete")
3757
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package action_test
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/ginkgo"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
func TestCommand(t *testing.T) {
11+
RegisterFailHandler(Fail)
12+
RunSpecs(t, "Internal action Suite")
13+
}

internal/pkg/action/constants.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,4 @@ package action
22

33
const (
44
csvKind = "ClusterServiceVersion"
5-
crdKind = "CustomResourceDefinition"
65
)

internal/pkg/action/operator_uninstall.go

Lines changed: 107 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,21 @@ import (
99
"github.com/operator-framework/api/pkg/operators/v1alpha1"
1010
apierrors "k8s.io/apimachinery/pkg/api/errors"
1111
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
12-
"k8s.io/apimachinery/pkg/runtime/schema"
1312
"k8s.io/apimachinery/pkg/types"
1413
"sigs.k8s.io/controller-runtime/pkg/client"
1514

15+
"github.com/operator-framework/kubectl-operator/internal/pkg/operand"
1616
"github.com/operator-framework/kubectl-operator/pkg/action"
1717
)
1818

1919
type OperatorUninstall struct {
2020
config *action.Configuration
2121

2222
Package string
23-
DeleteAll bool
24-
DeleteCRDs bool
23+
OperandStrategy operand.DeletionStrategy
2524
DeleteOperatorGroups bool
2625
DeleteOperatorGroupNames []string
27-
28-
Logf func(string, ...interface{})
26+
Logf func(string, ...interface{})
2927
}
3028

3129
func NewOperatorUninstall(cfg *action.Configuration) *OperatorUninstall {
@@ -44,11 +42,6 @@ func (e ErrPackageNotFound) Error() string {
4442
}
4543

4644
func (u *OperatorUninstall) Run(ctx context.Context) error {
47-
if u.DeleteAll {
48-
u.DeleteCRDs = true
49-
u.DeleteOperatorGroups = true
50-
}
51-
5245
subs := v1alpha1.SubscriptionList{}
5346
if err := u.config.Client.List(ctx, &subs, client.InNamespace(u.config.Namespace)); err != nil {
5447
return fmt.Errorf("list subscriptions: %v", err)
@@ -69,62 +62,56 @@ func (u *OperatorUninstall) Run(ctx context.Context) error {
6962
csv, csvName, err := u.getSubscriptionCSV(ctx, sub)
7063
if err != nil && !apierrors.IsNotFound(err) {
7164
if csvName == "" {
72-
return fmt.Errorf("get subscription CSV: %v", err)
65+
return fmt.Errorf("get subscription csv: %v", err)
7366
}
74-
return fmt.Errorf("get subscription CSV %q: %v", csvName, err)
67+
return fmt.Errorf("get subscription csv %q: %v", csvName, err)
7568
}
7669

77-
// Deletion order:
78-
//
79-
// 1. Subscription to prevent further installs or upgrades of the operator while cleaning up.
80-
// 2. CustomResourceDefinitions so the operator has a chance to handle CRs that have finalizers.
81-
// 3. ClusterServiceVersion. OLM puts an ownerref on every namespaced resource to the CSV,
82-
// and an owner label on every cluster scoped resource so they get gc'd on deletion.
70+
// find operands related to the operator on cluster
71+
lister := action.NewOperatorListOperands(u.config)
72+
operands, err := lister.Run(ctx, u.Package)
73+
if err != nil {
74+
return fmt.Errorf("list operands for operator %q: %v", u.Package, err)
75+
}
76+
// validate the provided deletion strategy before proceeding to deletion
77+
if err := u.validStrategy(operands); err != nil {
78+
return fmt.Errorf("could not proceed with deletion of %q: %s", u.Package, err)
79+
}
80+
81+
/*
82+
Deletion order:
83+
1. Subscription to prevent further installs or upgrades of the operator while cleaning up.
84+
85+
If the CSV exists:
86+
2. Operands so the operator has a chance to handle CRs that have finalizers.
87+
Note: the correct strategy must be chosen in order to process an opertor delete with operand on-cluster.
88+
3. ClusterServiceVersion. OLM puts an ownerref on every namespaced resource to the CSV,
89+
and an owner label on every cluster scoped resource so they get gc'd on deletion.
90+
91+
4. OperatorGroup in the namespace if no other subscriptions are in that namespace and OperatorGroup deletion is specified
92+
*/
8393

8494
// Subscriptions can be deleted asynchronously.
8595
if err := u.deleteObjects(ctx, sub); err != nil {
8696
return err
8797
}
8898

89-
if csv != nil {
90-
// Ensure CustomResourceDefinitions are deleted next, so that the operator
91-
// has a chance to handle CRs that have finalizers.
92-
if u.DeleteCRDs {
93-
crds := getCRDs(csv)
94-
if err := u.deleteObjects(ctx, crds...); err != nil {
95-
return err
96-
}
97-
}
98-
99-
// OLM puts an ownerref on every namespaced resource to the CSV,
100-
// and an owner label on every cluster scoped resource. When CSV is deleted
101-
// kube and olm gc will remove all the referenced resources.
102-
if err := u.deleteObjects(ctx, csv); err != nil {
99+
// If we could not find a csv associated with the subscription, that likely
100+
// means there is no CSV associated with it yet. Delete non-CSV related items only like the operatorgroup.
101+
if csv == nil {
102+
u.Logf("csv for package %q not found", u.Package)
103+
} else {
104+
if err := u.deleteCSVRelatedResources(ctx, csv, operands); err != nil {
103105
return err
104106
}
105107
}
106108

107109
if u.DeleteOperatorGroups {
108-
subs := v1alpha1.SubscriptionList{}
109-
if err := u.config.Client.List(ctx, &subs, client.InNamespace(u.config.Namespace)); err != nil {
110-
return fmt.Errorf("list subscriptions: %v", err)
111-
}
112-
// If there are no subscriptions left, delete the operator group(s).
113-
if len(subs.Items) == 0 {
114-
ogs := v1.OperatorGroupList{}
115-
if err := u.config.Client.List(ctx, &ogs, client.InNamespace(u.config.Namespace)); err != nil {
116-
return fmt.Errorf("list operatorgroups: %v", err)
117-
}
118-
for _, og := range ogs.Items {
119-
og := og
120-
if len(u.DeleteOperatorGroupNames) == 0 || contains(u.DeleteOperatorGroupNames, og.GetName()) {
121-
if err := u.deleteObjects(ctx, &og); err != nil {
122-
return err
123-
}
124-
}
125-
}
110+
if err := u.deleteOperatorGroup(ctx); err != nil {
111+
return fmt.Errorf("delete operatorgroup: %v", err)
126112
}
127113
}
114+
128115
return nil
129116
}
130117

@@ -165,28 +152,73 @@ func (u *OperatorUninstall) getSubscriptionCSV(ctx context.Context, subscription
165152
return csv, name, nil
166153
}
167154

168-
func csvNameFromSubscription(subscription *v1alpha1.Subscription) string {
169-
if subscription.Status.InstalledCSV != "" {
170-
return subscription.Status.InstalledCSV
155+
func (u *OperatorUninstall) deleteOperatorGroup(ctx context.Context) error {
156+
subs := v1alpha1.SubscriptionList{}
157+
if err := u.config.Client.List(ctx, &subs, client.InNamespace(u.config.Namespace)); err != nil {
158+
return fmt.Errorf("list subscriptions: %v", err)
171159
}
172-
return subscription.Status.CurrentCSV
160+
161+
// If there are no subscriptions left, delete the operator group(s).
162+
if len(subs.Items) == 0 {
163+
ogs := v1.OperatorGroupList{}
164+
if err := u.config.Client.List(ctx, &ogs, client.InNamespace(u.config.Namespace)); err != nil {
165+
return fmt.Errorf("list operatorgroups: %v", err)
166+
}
167+
for _, og := range ogs.Items {
168+
og := og
169+
if len(u.DeleteOperatorGroupNames) == 0 || contains(u.DeleteOperatorGroupNames, og.GetName()) {
170+
if err := u.deleteObjects(ctx, &og); err != nil {
171+
return err
172+
}
173+
}
174+
}
175+
}
176+
return nil
173177
}
174178

175-
// getCRDs returns the list of CRDs required by a CSV.
176-
func getCRDs(csv *v1alpha1.ClusterServiceVersion) (crds []client.Object) {
177-
for _, resource := range csv.Status.RequirementStatus {
178-
if resource.Kind == crdKind {
179-
obj := &unstructured.Unstructured{}
180-
obj.SetGroupVersionKind(schema.GroupVersionKind{
181-
Group: resource.Group,
182-
Version: resource.Version,
183-
Kind: resource.Kind,
184-
})
185-
obj.SetName(resource.Name)
186-
crds = append(crds, obj)
179+
// validStrategy validates the deletion strategy against the operands on-cluster
180+
// TODO define and use an OperandStrategyError that the cmd can use errors.As() on to provide external callers a more generic error
181+
func (u *OperatorUninstall) validStrategy(operands *unstructured.UnstructuredList) error {
182+
if len(operands.Items) > 0 && u.OperandStrategy.Kind == operand.Cancel {
183+
return fmt.Errorf("%d operands exist and operand strategy %q is in use: "+
184+
"delete operands manually or re-run uninstall with a different operand deletion strategy."+
185+
"\n\nSee kubectl operator uninstall --help for more information on operand deletion strategies.", len(operands.Items), operand.Cancel)
186+
}
187+
return nil
188+
}
189+
190+
func (u *OperatorUninstall) deleteCSVRelatedResources(ctx context.Context, csv *v1alpha1.ClusterServiceVersion, operands *unstructured.UnstructuredList) error {
191+
switch u.OperandStrategy.Kind {
192+
case operand.Ignore:
193+
for _, op := range operands.Items {
194+
u.Logf("%s %q orphaned", strings.ToLower(op.GetKind()), prettyPrint(op))
195+
}
196+
case operand.Delete:
197+
for _, op := range operands.Items {
198+
op := op
199+
if err := u.deleteObjects(ctx, &op); err != nil {
200+
return err
201+
}
187202
}
203+
default:
204+
return fmt.Errorf("unknown operand deletion strategy %q", u.OperandStrategy)
205+
}
206+
207+
// OLM puts an ownerref on every namespaced resource to the CSV,
208+
// and an owner label on every cluster scoped resource. When CSV is deleted
209+
// kube and olm gc will remove all the referenced resources.
210+
if err := u.deleteObjects(ctx, csv); err != nil {
211+
return err
188212
}
189-
return
213+
214+
return nil
215+
}
216+
217+
func csvNameFromSubscription(subscription *v1alpha1.Subscription) string {
218+
if subscription.Status.InstalledCSV != "" {
219+
return subscription.Status.InstalledCSV
220+
}
221+
return subscription.Status.CurrentCSV
190222
}
191223

192224
func contains(haystack []string, needle string) bool {
@@ -197,3 +229,11 @@ func contains(haystack []string, needle string) bool {
197229
}
198230
return false
199231
}
232+
233+
func prettyPrint(op unstructured.Unstructured) string {
234+
namespaced := op.GetNamespace() != ""
235+
if namespaced {
236+
return fmt.Sprint(op.GetName() + "/" + op.GetNamespace())
237+
}
238+
return op.GetName()
239+
}

0 commit comments

Comments
 (0)