Skip to content

Commit 67a5302

Browse files
authored
Uninstall improvements (#55)
* add options for deleting all objects, operator object, simplify operand.Strategy type Signed-off-by: Joe Lanford <[email protected]> * add note that dependencies are not uninstalled Signed-off-by: Joe Lanford <[email protected]> * uninstall: rename 'cancel' operand strategy to 'abort' Signed-off-by: Joe Lanford <[email protected]>
1 parent 7e2e2de commit 67a5302

File tree

4 files changed

+185
-76
lines changed

4 files changed

+185
-76
lines changed

internal/cmd/operator_uninstall.go

+57-23
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package cmd
22

33
import (
4+
"errors"
5+
46
"github.com/spf13/cobra"
57
"github.com/spf13/pflag"
68

79
"github.com/operator-framework/kubectl-operator/internal/cmd/internal/log"
810
internalaction "github.com/operator-framework/kubectl-operator/internal/pkg/action"
11+
"github.com/operator-framework/kubectl-operator/internal/pkg/operand"
912
"github.com/operator-framework/kubectl-operator/pkg/action"
1013
)
1114

@@ -16,42 +19,73 @@ func newOperatorUninstallCmd(cfg *action.Configuration) *cobra.Command {
1619
cmd := &cobra.Command{
1720
Use: "uninstall <operator>",
1821
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.`,
22+
Long: `Uninstall removes the subscription, operator and optionally operands managed
23+
by the operator as well as the relevant operatorgroup.
24+
25+
Warning: this command permanently deletes objects from the cluster. Running
26+
uninstall concurrently with other operations could result in undefined behavior.
27+
28+
The uninstall command first checks to find the subscription associated with the
29+
operator. It then lists all operands found throughout the cluster for the
30+
operator specified if one is found. Since the scope of an operator is restricted
31+
by its operator group, this search will include namespace-scoped operands from
32+
the operator group's target namespaces and all cluster-scoped operands.
33+
34+
The operand-deletion strategy is then considered if any operands are found
35+
on-cluster. One of abort|ignore|delete. By default, the strategy is "abort",
36+
which means that if any operands are found when deleting the operator abort the
37+
uninstall without deleting anything. The "ignore" strategy keeps the operands on
38+
cluster and deletes the subscription and the operator. The "delete" strategy
39+
deletes the subscription, operands, and after they have finished finalizing, the
40+
operator itself.
41+
42+
Setting --delete-operator-groups to true will delete the operatorgroup in the
43+
provided namespace if no other active subscriptions are currently in that
44+
namespace, after removing the operator. The subscription and operatorgroup will
45+
be removed even if the operator is not found.
46+
47+
There are other deletion flags for removing additional objects, such as custom
48+
resource definitions, operator objects, and any other objects deployed alongside
49+
the operator (e.g. RBAC objects for the operator). These flags are:
50+
51+
--delete-operator
52+
53+
Deletes all objects associated with the operator by looking up the
54+
operator object for the operator and deleting every referenced object
55+
and then deleting the operator object itself. This implies the flag
56+
'--operand-strategy=delete' because it is impossible to delete CRDs
57+
without also deleting instances of those CRDs.
58+
59+
-X, --delete-all
60+
61+
This is a convenience flag that is effectively equivalent to the flags
62+
'--delete-operator=true --delete-operator-groups=true'.
63+
64+
NOTE: This command does not recursively uninstall unused dependencies. To return
65+
a cluster to its state prior to a 'kubectl operator install' call, each
66+
dependency of the operator that was installed automatically by OLM must be
67+
individually uninstalled.
68+
`,
4069
Args: cobra.ExactArgs(1),
4170
Run: func(cmd *cobra.Command, args []string) {
4271
u.Package = args[0]
4372
if err := u.Run(cmd.Context()); err != nil {
73+
if errors.Is(err, operand.ErrAbortStrategy) {
74+
log.Fatalf("uninstall operator: %v"+"\n\n%s", err,
75+
"See kubectl operator uninstall --help for more information on operand deletion strategies.")
76+
}
4477
log.Fatalf("uninstall operator: %v", err)
4578
}
46-
log.Printf("operator %q uninstalled", u.Package)
4779
},
4880
}
4981
bindOperatorUninstallFlags(cmd.Flags(), u)
5082
return cmd
5183
}
5284

5385
func bindOperatorUninstallFlags(fs *pflag.FlagSet, u *internalaction.OperatorUninstall) {
86+
fs.BoolVarP(&u.DeleteAll, "delete-all", "X", false, "delete all objects associated with the operator, implies --delete-operator, --operand-strategy=delete, --delete-operator-groups")
87+
fs.BoolVar(&u.DeleteOperator, "delete-operator", false, "delete operator object associated with the operator, --operand-strategy=delete")
5488
fs.BoolVar(&u.DeleteOperatorGroups, "delete-operator-groups", false, "delete operator groups if no other operators remain")
5589
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")
90+
fs.VarP(&u.OperandStrategy, "operand-strategy", "s", "determines how to handle operands when deleting the operator, one of abort|ignore|delete (default: abort)")
5791
}

internal/pkg/action/operator_uninstall.go

+105-22
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import (
44
"context"
55
"fmt"
66
"strings"
7+
"time"
78

89
v1 "github.com/operator-framework/api/pkg/operators/v1"
910
"github.com/operator-framework/api/pkg/operators/v1alpha1"
1011
apierrors "k8s.io/apimachinery/pkg/api/errors"
1112
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1213
"k8s.io/apimachinery/pkg/types"
14+
"k8s.io/apimachinery/pkg/util/wait"
1315
"sigs.k8s.io/controller-runtime/pkg/client"
1416

1517
"github.com/operator-framework/kubectl-operator/internal/pkg/operand"
@@ -21,15 +23,18 @@ type OperatorUninstall struct {
2123

2224
Package string
2325
OperandStrategy operand.DeletionStrategy
26+
DeleteAll bool
27+
DeleteOperator bool
2428
DeleteOperatorGroups bool
2529
DeleteOperatorGroupNames []string
2630
Logf func(string, ...interface{})
2731
}
2832

2933
func NewOperatorUninstall(cfg *action.Configuration) *OperatorUninstall {
3034
return &OperatorUninstall{
31-
config: cfg,
32-
Logf: func(string, ...interface{}) {},
35+
config: cfg,
36+
OperandStrategy: operand.Abort,
37+
Logf: func(string, ...interface{}) {},
3338
}
3439
}
3540

@@ -42,6 +47,18 @@ func (e ErrPackageNotFound) Error() string {
4247
}
4348

4449
func (u *OperatorUninstall) Run(ctx context.Context) error {
50+
if u.DeleteAll {
51+
u.DeleteOperator = true
52+
u.DeleteOperatorGroups = true
53+
}
54+
if u.DeleteOperator {
55+
u.OperandStrategy = operand.Delete
56+
}
57+
58+
if err := u.OperandStrategy.Valid(); err != nil {
59+
return err
60+
}
61+
4562
subs := v1alpha1.SubscriptionList{}
4663
if err := u.config.Client.List(ctx, &subs, client.InNamespace(u.config.Namespace)); err != nil {
4764
return fmt.Errorf("list subscriptions: %v", err)
@@ -75,20 +92,23 @@ func (u *OperatorUninstall) Run(ctx context.Context) error {
7592
}
7693
// validate the provided deletion strategy before proceeding to deletion
7794
if err := u.validStrategy(operands); err != nil {
78-
return fmt.Errorf("could not proceed with deletion of %q: %s", u.Package, err)
95+
return fmt.Errorf("could not proceed with deletion of %q: %w", u.Package, err)
7996
}
8097

8198
/*
8299
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
100+
1. Subscription to prevent further installs or upgrades of the operator while cleaning up.
101+
102+
If the CSV exists:
103+
2. Operands so the operator has a chance to handle CRs that have finalizers.
104+
Note: the correct strategy must be chosen in order to process an opertor delete with operand
105+
on-cluster.
106+
3. ClusterServiceVersion. OLM puts an ownerref on every namespaced resource to the CSV,
107+
and an owner label on every cluster scoped resource so they get gc'd on deletion.
108+
109+
4. The Operator and all objects referenced by it if Operator deletion is specified
110+
5. OperatorGroup in the namespace if no other subscriptions are in that namespace and OperatorGroup deletion
111+
is specified
92112
*/
93113

94114
// Subscriptions can be deleted asynchronously.
@@ -106,6 +126,12 @@ func (u *OperatorUninstall) Run(ctx context.Context) error {
106126
}
107127
}
108128

129+
if u.DeleteOperator {
130+
if err := u.deleteOperator(ctx); err != nil {
131+
return fmt.Errorf("delete operator: %v", err)
132+
}
133+
}
134+
109135
if u.DeleteOperatorGroups {
110136
if err := u.deleteOperatorGroup(ctx); err != nil {
111137
return fmt.Errorf("delete operatorgroup: %v", err)
@@ -115,6 +141,10 @@ func (u *OperatorUninstall) Run(ctx context.Context) error {
115141
return nil
116142
}
117143

144+
func (u *OperatorUninstall) operatorName() string {
145+
return fmt.Sprintf("%s.%s", u.Package, u.config.Namespace)
146+
}
147+
118148
func (u *OperatorUninstall) deleteObjects(ctx context.Context, objs ...client.Object) error {
119149
for _, obj := range objs {
120150
obj := obj
@@ -152,6 +182,64 @@ func (u *OperatorUninstall) getSubscriptionCSV(ctx context.Context, subscription
152182
return csv, name, nil
153183
}
154184

185+
// deleteOperator deletes the operator and everything it references. It:
186+
// - gets the operator object so that we can look up its references
187+
// - deletes the references
188+
// - waits until the operator object references are all deleted (this step is
189+
// necessary because OLM recreates the operator object until no other
190+
// referenced objects exist)
191+
// - deletes the operator
192+
func (u *OperatorUninstall) deleteOperator(ctx context.Context) error {
193+
// get the operator
194+
var op v1.Operator
195+
key := types.NamespacedName{Name: u.operatorName()}
196+
if err := u.config.Client.Get(ctx, key, &op); err != nil {
197+
if apierrors.IsNotFound(err) {
198+
return nil
199+
}
200+
return fmt.Errorf("get operator: %w", err)
201+
}
202+
203+
// build objects for each of the references and then delete them
204+
objs := []client.Object{}
205+
for _, ref := range op.Status.Components.Refs {
206+
obj := unstructured.Unstructured{}
207+
obj.SetName(ref.Name)
208+
obj.SetNamespace(ref.Namespace)
209+
obj.SetGroupVersionKind(ref.GroupVersionKind())
210+
objs = append(objs, &obj)
211+
}
212+
if err := u.deleteObjects(ctx, objs...); err != nil {
213+
return fmt.Errorf("delete operator references: %v", err)
214+
}
215+
216+
// wait until all of the objects we just deleted disappear from the
217+
// operator's references.
218+
if err := wait.PollImmediateUntil(time.Millisecond*100, func() (bool, error) {
219+
var check v1.Operator
220+
if err := u.config.Client.Get(ctx, key, &check); err != nil {
221+
if apierrors.IsNotFound(err) {
222+
return true, nil
223+
}
224+
return false, fmt.Errorf("get operator: %w", err)
225+
}
226+
if check.Status.Components == nil || len(check.Status.Components.Refs) == 0 {
227+
return true, nil
228+
}
229+
return false, nil
230+
}, ctx.Done()); err != nil {
231+
return err
232+
}
233+
234+
// delete the operator
235+
op.SetGroupVersionKind(v1.GroupVersion.WithKind("Operator"))
236+
if err := u.deleteObjects(ctx, &op); err != nil {
237+
return fmt.Errorf("delete operator: %v", err)
238+
}
239+
240+
return nil
241+
}
242+
155243
func (u *OperatorUninstall) deleteOperatorGroup(ctx context.Context) error {
156244
subs := v1alpha1.SubscriptionList{}
157245
if err := u.config.Client.List(ctx, &subs, client.InNamespace(u.config.Namespace)); err != nil {
@@ -177,18 +265,15 @@ func (u *OperatorUninstall) deleteOperatorGroup(ctx context.Context) error {
177265
}
178266

179267
// 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
181268
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)
269+
if len(operands.Items) > 0 && u.OperandStrategy == operand.Abort {
270+
return operand.ErrAbortStrategy
186271
}
187272
return nil
188273
}
189274

190275
func (u *OperatorUninstall) deleteCSVRelatedResources(ctx context.Context, csv *v1alpha1.ClusterServiceVersion, operands *unstructured.UnstructuredList) error {
191-
switch u.OperandStrategy.Kind {
276+
switch u.OperandStrategy {
192277
case operand.Ignore:
193278
for _, op := range operands.Items {
194279
u.Logf("%s %q orphaned", strings.ToLower(op.GetKind()), prettyPrint(op))
@@ -197,18 +282,16 @@ func (u *OperatorUninstall) deleteCSVRelatedResources(ctx context.Context, csv *
197282
for _, op := range operands.Items {
198283
op := op
199284
if err := u.deleteObjects(ctx, &op); err != nil {
200-
return err
285+
return fmt.Errorf("delete operand: %v", err)
201286
}
202287
}
203-
default:
204-
return fmt.Errorf("unknown operand deletion strategy %q", u.OperandStrategy)
205288
}
206289

207290
// OLM puts an ownerref on every namespaced resource to the CSV,
208291
// and an owner label on every cluster scoped resource. When CSV is deleted
209292
// kube and olm gc will remove all the referenced resources.
210293
if err := u.deleteObjects(ctx, csv); err != nil {
211-
return err
294+
return fmt.Errorf("delete csv: %v", err)
212295
}
213296

214297
return nil

internal/pkg/action/operator_uninstall_test.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ var _ = Describe("OperatorUninstall", func() {
162162

163163
uninstaller := internalaction.NewOperatorUninstall(&cfg)
164164
uninstaller.Package = etcd
165+
uninstaller.OperandStrategy = operand.Ignore
165166
err := uninstaller.Run(context.TODO())
166167
Expect(err).To(BeNil())
167168

@@ -173,23 +174,23 @@ var _ = Describe("OperatorUninstall", func() {
173174
It("should fail due to invalid operand deletion strategy", func() {
174175
uninstaller := internalaction.NewOperatorUninstall(&cfg)
175176
uninstaller.Package = etcd
176-
uninstaller.OperandStrategy.Kind = "foo"
177+
uninstaller.OperandStrategy = "foo"
177178
err := uninstaller.Run(context.TODO())
178179
Expect(err.Error()).To(ContainSubstring("unknown operand deletion strategy"))
179180
})
180181

181-
It("should error with operands on cluster when default cancel strategy is set", func() {
182+
It("should error with operands on cluster when default abort strategy is set", func() {
182183
uninstaller := internalaction.NewOperatorUninstall(&cfg)
183184
uninstaller.Package = etcd
184-
uninstaller.OperandStrategy.Kind = operand.Cancel
185+
uninstaller.OperandStrategy = operand.Abort
185186
err := uninstaller.Run(context.TODO())
186-
Expect(err.Error()).To(ContainSubstring("delete operands manually or re-run uninstall with a different operand deletion strategy"))
187+
Expect(err).To(MatchError(operand.ErrAbortStrategy))
187188
})
188189

189190
It("should ignore operands and delete sub and csv when ignore strategy is set", func() {
190191
uninstaller := internalaction.NewOperatorUninstall(&cfg)
191192
uninstaller.Package = etcd
192-
uninstaller.OperandStrategy.Kind = operand.Ignore
193+
uninstaller.OperandStrategy = operand.Ignore
193194
err := uninstaller.Run(context.TODO())
194195
Expect(err).To(BeNil())
195196

@@ -215,7 +216,7 @@ var _ = Describe("OperatorUninstall", func() {
215216
It("should delete sub, csv, and operands when delete strategy is set", func() {
216217
uninstaller := internalaction.NewOperatorUninstall(&cfg)
217218
uninstaller.Package = etcd
218-
uninstaller.OperandStrategy.Kind = operand.Delete
219+
uninstaller.OperandStrategy = operand.Delete
219220
err := uninstaller.Run(context.TODO())
220221
Expect(err).To(BeNil())
221222

@@ -239,6 +240,7 @@ var _ = Describe("OperatorUninstall", func() {
239240
It("should delete sub and operatorgroup when no CSV is found", func() {
240241
uninstaller := internalaction.NewOperatorUninstall(&cfg)
241242
uninstaller.Package = etcd
243+
uninstaller.OperandStrategy = operand.Ignore
242244
uninstaller.DeleteOperatorGroups = true
243245

244246
sub.Status.InstalledCSV = "foo" // returns nil CSV

0 commit comments

Comments
 (0)