Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve OperatorGroup removal logic #230

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions api/v1beta1/operatorpolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ func (ra RemovalAction) IsDeleteIfUnused() bool {

type RemovalBehavior struct {
//+kubebuilder:default=DeleteIfUnused
//+kubebuilder:validation:Enum=Keep;Delete;DeleteIfUnused
//+kubebuilder:validation:Enum=Keep;DeleteIfUnused
// Specifies whether to delete the OperatorGroup; defaults to 'DeleteIfUnused' which
// will only delete the OperatorGroup if there is not another Subscription using it.
// will only delete the OperatorGroup if there is not another resource using it.
OperatorGroups RemovalAction `json:"operatorGroups,omitempty"`

//+kubebuilder:default=Delete
Expand Down
94 changes: 57 additions & 37 deletions controllers/operatorpolicy_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,12 @@ func (r *OperatorPolicyReconciler) handleResources(ctx context.Context, policy *
return earlyComplianceEvents, condChanged, err
}

earlyConds, changed, err := r.handleOpGroup(ctx, policy, desiredOG)
desiredSubName := ""
if desiredSub != nil {
desiredSubName = desiredSub.Name
}

earlyConds, changed, err := r.handleOpGroup(ctx, policy, desiredOG, desiredSubName)
earlyComplianceEvents = append(earlyComplianceEvents, earlyConds...)
condChanged = condChanged || changed

Expand Down Expand Up @@ -567,7 +572,10 @@ func buildOperatorGroup(
}

func (r *OperatorPolicyReconciler) handleOpGroup(
ctx context.Context, policy *policyv1beta1.OperatorPolicy, desiredOpGroup *operatorv1.OperatorGroup,
ctx context.Context,
policy *policyv1beta1.OperatorPolicy,
desiredOpGroup *operatorv1.OperatorGroup,
desiredSubName string,
) ([]metav1.Condition, bool, error) {
watcher := opPolIdentifier(policy.Namespace, policy.Name)

Expand All @@ -586,7 +594,7 @@ func (r *OperatorPolicyReconciler) handleOpGroup(
return r.musthaveOpGroup(ctx, policy, desiredOpGroup, foundOpGroups)
}

return r.mustnothaveOpGroup(ctx, policy, desiredOpGroup, foundOpGroups)
return r.mustnothaveOpGroup(ctx, policy, desiredOpGroup, foundOpGroups, desiredSubName)
}

func (r *OperatorPolicyReconciler) musthaveOpGroup(
Expand Down Expand Up @@ -716,44 +724,72 @@ func (r *OperatorPolicyReconciler) mustnothaveOpGroup(
ctx context.Context,
policy *policyv1beta1.OperatorPolicy,
desiredOpGroup *operatorv1.OperatorGroup,
foundOpGroups []unstructured.Unstructured,
allFoundOpGroups []unstructured.Unstructured,
desiredSubName string,
) ([]metav1.Condition, bool, error) {
if len(foundOpGroups) == 0 {
if len(allFoundOpGroups) == 0 {
// Missing OperatorGroup: report Compliance
changed := updateStatus(policy, missingNotWantedCond("OperatorGroup"), missingNotWantedObj(desiredOpGroup))

return nil, changed, nil
}

var foundOpGroupName string
var foundOpGroup *unstructured.Unstructured
if len(allFoundOpGroups) > 1 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to confirm, OperatorPolicy would never add an additional operator group if one already existed so there is never a time where this would prevent undoing an OperatorPolicy mistake by the user right? This would only happen if there was already multiple operator groups when the OperatorPolicy was defined or the must have OperatorPolicy was defined and then an additional operator group was added afterwards.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the controller sees an OperatorGroup already in the namespace, it will not create another one, even if the policy specifies a different OperatorGroup than the one that was found.

But, there is always the possibility of a race condition where something else creates an OperatorGroup between when this controller determines one is not yet present and when it submits its request to create one. But, since this controller does not have concurrent reconciles, it at least can not race with itself.

// Don't try to choose one, just report this as NonCompliant.
return nil, updateStatus(policy, opGroupTooManyCond, opGroupTooManyObjs(allFoundOpGroups)...), nil
}

for _, opGroup := range foundOpGroups {
emptyNameMatch := desiredOpGroup.Name == "" && opGroup.GetGenerateName() == desiredOpGroup.GenerateName
foundOpGroup := allFoundOpGroups[0]

if opGroup.GetName() == desiredOpGroup.Name || emptyNameMatch {
foundOpGroupName = opGroup.GetName()
foundOpGroup = opGroup.DeepCopy()
removalBehavior := policy.Spec.RemovalBehavior.ApplyDefaults()
keep := removalBehavior.OperatorGroups.IsKeep()

break
// if DeleteIfUnused and there are other subscriptions, then keep the operator group
if removalBehavior.OperatorGroups.IsDeleteIfUnused() {
// Check the namespace for any subscriptions, including the sub for this mustnothave policy,
// since deleting the OperatorGroup before that could cause problems
watcher := opPolIdentifier(policy.Namespace, policy.Name)

foundSubscriptions, err := r.DynamicWatcher.List(
watcher, subscriptionGVK, desiredOpGroup.Namespace, labels.Everything())
if err != nil {
return nil, false, fmt.Errorf("error listing Subscriptions: %w", err)
}

anotherSubFound := false

for _, sub := range foundSubscriptions {
if sub.GetName() != desiredSubName {
anotherSubFound = true

break
}
}

if anotherSubFound {
keep = true
}
}

if keep {
return nil, updateStatus(policy, keptCond("OperatorGroup"), leftoverObj(&foundOpGroup)), nil
}

if foundOpGroupName == "" {
emptyNameMatch := desiredOpGroup.Name == "" && foundOpGroup.GetGenerateName() == desiredOpGroup.GenerateName

if !(foundOpGroup.GetName() == desiredOpGroup.Name || emptyNameMatch) {
// no found OperatorGroup matches what the policy is looking for, report Compliance.
changed := updateStatus(policy, missingNotWantedCond("OperatorGroup"), missingNotWantedObj(desiredOpGroup))

return nil, changed, nil
}

desiredOpGroup.SetName(foundOpGroupName)

removalBehavior := policy.Spec.RemovalBehavior.ApplyDefaults()

if removalBehavior.OperatorGroups.IsKeep() {
changed := updateStatus(policy, keptCond("OperatorGroup"), leftoverObj(desiredOpGroup))
desiredOpGroup.SetName(foundOpGroup.GetName()) // set it for the generateName case

return nil, changed, nil
if len(foundOpGroup.GetOwnerReferences()) != 0 {
// the OperatorGroup specified in the policy might be used or managed by something else
// so we will keep it.
return nil, updateStatus(policy, keptCond("OperatorGroup"), leftoverObj(desiredOpGroup)), nil
}

// The found OperatorGroup matches what is *not* wanted by the policy. Report NonCompliance.
Expand All @@ -763,22 +799,6 @@ func (r *OperatorPolicyReconciler) mustnothaveOpGroup(
return nil, changed, nil
}

if removalBehavior.OperatorGroups.IsDeleteIfUnused() {
// Check the namespace for any subscriptions, including the sub for this mustnothave policy,
// since deleting the OperatorGroup before that could cause problems
watcher := opPolIdentifier(policy.Namespace, policy.Name)

foundSubscriptions, err := r.DynamicWatcher.List(
watcher, subscriptionGVK, desiredOpGroup.Namespace, labels.Everything())
if err != nil {
return nil, false, fmt.Errorf("error listing Subscriptions: %w", err)
}

if len(foundSubscriptions) != 0 {
return nil, changed, nil
}
}

if foundOpGroup.GetDeletionTimestamp() != nil {
// No "early" condition because that would cause the status to flap
return nil, updateStatus(policy, deletingCond("OperatorGroup"), deletingObj(desiredOpGroup)), nil
Expand Down
2 changes: 2 additions & 0 deletions controllers/operatorpolicy_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,8 @@ func deletedObj(obj client.Object) policyv1.RelatedObject {
}
}

// deletingObj returns a NonCompliant RelatedObject with
// reason = 'The object is being deleted but has not been removed yet'
func deletingObj(obj client.Object) policyv1.RelatedObject {
return policyv1.RelatedObject{
Object: policyv1.ObjectResourceFromObj(obj),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,9 @@ spec:
default: DeleteIfUnused
description: |-
Specifies whether to delete the OperatorGroup; defaults to 'DeleteIfUnused' which
will only delete the OperatorGroup if there is not another Subscription using it.
will only delete the OperatorGroup if there is not another resource using it.
enum:
- Keep
- Delete
- DeleteIfUnused
type: string
subscriptions:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,9 @@ spec:
default: DeleteIfUnused
description: |-
Specifies whether to delete the OperatorGroup; defaults to 'DeleteIfUnused' which
will only delete the OperatorGroup if there is not another Subscription using it.
will only delete the OperatorGroup if there is not another resource using it.
enum:
- Keep
- Delete
- DeleteIfUnused
type: string
subscriptions:
Expand Down
135 changes: 124 additions & 11 deletions test/e2e/case38_install_operator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1295,8 +1295,8 @@ var _ = Describe("Testing OperatorPolicy", Ordered, func() {
})
Describe("Testing CustomResourceDefinition reporting", Ordered, func() {
const (
opPolYAML = "../resources/case38_operator_install/operator-policy-authorino.yaml"
opPolName = "oppol-authorino"
opPolYAML = "../resources/case38_operator_install/operator-policy-no-group.yaml"
opPolName = "oppol-no-group"
)
BeforeAll(func() {
utils.Kubectl("delete", "crd", "--selector=olm.managed=true")
Expand Down Expand Up @@ -1340,7 +1340,7 @@ var _ = Describe("Testing OperatorPolicy", Ordered, func() {
By("Waiting for a CRD to appear, which should indicate the operator is installing")
Eventually(func(ctx SpecContext) *unstructured.Unstructured {
crd, _ := clientManagedDynamic.Resource(gvrCRD).Get(ctx,
"authconfigs.authorino.kuadrant.io", metav1.GetOptions{})
"quayregistries.quay.redhat.com", metav1.GetOptions{})

return crd
}, olmWaitTimeout, 5, ctx).ShouldNot(BeNil())
Expand All @@ -1364,7 +1364,7 @@ var _ = Describe("Testing OperatorPolicy", Ordered, func() {
Kind: "CustomResourceDefinition",
APIVersion: "apiextensions.k8s.io/v1",
Metadata: policyv1.ObjectMetadata{
Name: "authconfigs.authorino.kuadrant.io",
Name: "quayregistries.quay.redhat.com",
},
},
Compliant: "Compliant",
Expand All @@ -1374,7 +1374,7 @@ var _ = Describe("Testing OperatorPolicy", Ordered, func() {
Kind: "CustomResourceDefinition",
APIVersion: "apiextensions.k8s.io/v1",
Metadata: policyv1.ObjectMetadata{
Name: "authorinos.operator.authorino.kuadrant.io",
Name: "quayecosystems.redhatcop.redhat.io",
},
},
Compliant: "Compliant",
Expand Down Expand Up @@ -2048,7 +2048,7 @@ var _ = Describe("Testing OperatorPolicy", Ordered, func() {

By("Setting the removal behaviors to Delete")
utils.Kubectl("patch", "operatorpolicy", opPolName, "-n", opPolTestNS, "--type=json", "-p",
`[{"op": "replace", "path": "/spec/removalBehavior/operatorGroups", "value": "Delete"},`+
`[{"op": "replace", "path": "/spec/removalBehavior/operatorGroups", "value": "DeleteIfUnused"},`+
`{"op": "replace", "path": "/spec/removalBehavior/subscriptions", "value": "Delete"},`+
`{"op": "replace", "path": "/spec/removalBehavior/clusterServiceVersions", "value": "Delete"},`+
`{"op": "replace", "path": "/spec/removalBehavior/installPlans", "value": "Delete"},`+
Expand Down Expand Up @@ -2378,7 +2378,7 @@ var _ = Describe("Testing OperatorPolicy", Ordered, func() {
opPolYAML, opPolTestNS, gvrPolicy, gvrOperatorPolicy)
})

It("should delete the operator group when there is only one subscription", func(ctx SpecContext) {
It("should delete the inferred operator group when there is only one subscription", func(ctx SpecContext) {
// enforce it as a musthave in order to install the operator
utils.Kubectl("patch", "operatorpolicy", opPolName, "-n", opPolTestNS, "--type=json", "-p",
`[{"op": "replace", "path": "/spec/complianceType", "value": "musthave"},`+
Expand Down Expand Up @@ -2427,7 +2427,121 @@ var _ = Describe("Testing OperatorPolicy", Ordered, func() {
}, eventuallyTimeout, 3, ctx).Should(BeEmpty())
})

It("should not delete the operator group when there is another subscription", func(ctx SpecContext) {
It("should delete the specified operator group when there is only one subscription", func(ctx SpecContext) {
// enforce it as a musthave in order to install the operator
utils.Kubectl("patch", "operatorpolicy", opPolName, "-n", opPolTestNS, "--type=json", "-p",
`[{"op": "replace", "path": "/spec/complianceType", "value": "musthave"},`+
`{"op": "replace", "path": "/spec/remediationAction", "value": "enforce"},`+
`{"op": "replace", "path": "/spec/removalBehavior/operatorGroups", "value": "DeleteIfUnused"},`+
`{"op": "add", "path": "/spec/operatorGroup", "value": {"name": "scoped-operator-group", `+
`"namespace": "operator-policy-testns", "targetNamespaces": ["operator-policy-testns"]}}]`)

By("Waiting for a CRD to appear, which should indicate the operator is installing.")
Eventually(func(ctx SpecContext) *unstructured.Unstructured {
crd, _ := clientManagedDynamic.Resource(gvrCRD).Get(ctx,
"quayregistries.quay.redhat.com", metav1.GetOptions{})

return crd
}, olmWaitTimeout, 5, ctx).ShouldNot(BeNil())

By("Waiting for the policy to become compliant, indicating the operator is installed")
Eventually(func(g Gomega) string {
pol := utils.GetWithTimeout(clientManagedDynamic, gvrOperatorPolicy, opPolName,
opPolTestNS, true, eventuallyTimeout)
compliance, found, err := unstructured.NestedString(pol.Object, "status", "compliant")
g.Expect(err).NotTo(HaveOccurred())
g.Expect(found).To(BeTrue())

return compliance
}, olmWaitTimeout, 5, ctx).Should(Equal("Compliant"))

By("Verifying that an operator group exists")
Eventually(func(g Gomega) []unstructured.Unstructured {
list, err := clientManagedDynamic.Resource(gvrOperatorGroup).Namespace(opPolTestNS).
List(ctx, metav1.ListOptions{})
g.Expect(err).NotTo(HaveOccurred())

return list.Items
}, eventuallyTimeout, 3, ctx).ShouldNot(BeEmpty())

// revert it to mustnothave
utils.Kubectl("patch", "operatorpolicy", opPolName, "-n", opPolTestNS, "--type=json", "-p",
`[{"op": "replace", "path": "/spec/complianceType", "value": "mustnothave"}]`)

By("Verifying that the operator group was removed")
Eventually(func(g Gomega) []unstructured.Unstructured {
list, err := clientManagedDynamic.Resource(gvrOperatorGroup).Namespace(opPolTestNS).
List(ctx, metav1.ListOptions{})
g.Expect(err).NotTo(HaveOccurred())

return list.Items
}, eventuallyTimeout, 3, ctx).Should(BeEmpty())
})

It("should keep the specified operator group when it is owned by something", func(ctx SpecContext) {
// enforce it as a musthave in order to install the operator
utils.Kubectl("patch", "operatorpolicy", opPolName, "-n", opPolTestNS, "--type=json", "-p",
`[{"op": "replace", "path": "/spec/complianceType", "value": "musthave"},`+
`{"op": "replace", "path": "/spec/remediationAction", "value": "enforce"},`+
`{"op": "replace", "path": "/spec/removalBehavior/operatorGroups", "value": "DeleteIfUnused"},`+
`{"op": "add", "path": "/spec/operatorGroup", "value": {"name": "scoped-operator-group", `+
`"namespace": "operator-policy-testns", "targetNamespaces": ["operator-policy-testns"]}}]`)

By("Waiting for a CRD to appear, which should indicate the operator is installing.")
Eventually(func(ctx SpecContext) *unstructured.Unstructured {
crd, _ := clientManagedDynamic.Resource(gvrCRD).Get(ctx,
"quayregistries.quay.redhat.com", metav1.GetOptions{})

return crd
}, olmWaitTimeout, 5, ctx).ShouldNot(BeNil())

By("Waiting for the policy to become compliant, indicating the operator is installed")
Eventually(func(g Gomega) string {
pol := utils.GetWithTimeout(clientManagedDynamic, gvrOperatorPolicy, opPolName,
opPolTestNS, true, eventuallyTimeout)
compliance, found, err := unstructured.NestedString(pol.Object, "status", "compliant")
g.Expect(err).NotTo(HaveOccurred())
g.Expect(found).To(BeTrue())

return compliance
}, olmWaitTimeout, 5, ctx).Should(Equal("Compliant"))

By("Verifying that an operator group exists")
Eventually(func(g Gomega) []unstructured.Unstructured {
list, err := clientManagedDynamic.Resource(gvrOperatorGroup).Namespace(opPolTestNS).
List(ctx, metav1.ListOptions{})
g.Expect(err).NotTo(HaveOccurred())

return list.Items
}, eventuallyTimeout, 3, ctx).ShouldNot(BeEmpty())

By("Creating and setting an owner for the operator group")
utils.Kubectl("create", "configmap", "ownercm", "-n", opPolTestNS, "--from-literal=foo=bar")

ownerCM := utils.GetWithTimeout(clientManagedDynamic, gvrConfigMap, "ownercm",
opPolTestNS, true, eventuallyTimeout)
ownerUID := string(ownerCM.GetUID())
Expect(ownerUID).NotTo(BeEmpty())

utils.Kubectl("patch", "operatorgroup", "scoped-operator-group", "-n", opPolTestNS, "--type=json", "-p",
`[{"op": "add", "path": "/metadata/ownerReferences", "value": [{"apiVersion": "v1",
"kind": "ConfigMap", "name": "ownercm", "uid": "`+ownerUID+`"}]}]`)

// revert it to mustnothave
utils.Kubectl("patch", "operatorpolicy", opPolName, "-n", opPolTestNS, "--type=json", "-p",
`[{"op": "replace", "path": "/spec/complianceType", "value": "mustnothave"}]`)

By("Verifying the operator group was not removed")
Consistently(func(g Gomega) []unstructured.Unstructured {
list, err := clientManagedDynamic.Resource(gvrOperatorGroup).Namespace(opPolTestNS).
List(ctx, metav1.ListOptions{})
g.Expect(err).NotTo(HaveOccurred())

return list.Items
}, consistentlyDuration, 3, ctx).ShouldNot(BeEmpty())
})

It("should not delete the inferred operator group when there is another subscription", func(ctx SpecContext) {
// enforce it as a musthave in order to install the operator
utils.Kubectl("patch", "operatorpolicy", opPolName, "-n", opPolTestNS, "--type=json", "-p",
`[{"op": "replace", "path": "/spec/complianceType", "value": "musthave"},`+
Expand Down Expand Up @@ -2494,9 +2608,8 @@ var _ = Describe("Testing OperatorPolicy", Ordered, func() {
})
Describe("Testing defaulted values in an OperatorPolicy", func() {
const (
opPolYAML = "../resources/case38_operator_install/operator-policy-authorino.yaml"
opPolName = "oppol-authorino"
subName = "authorino-operator"
opPolYAML = "../resources/case38_operator_install/operator-policy-no-group.yaml"
opPolName = "oppol-no-group"
)

BeforeEach(func() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ spec:
versions:
- quay-operator.v3.8.13
removalBehavior:
operatorGroups: Delete
operatorGroups: DeleteIfUnused
subscriptions: Delete
clusterServiceVersions: Delete
installPlans: Delete
Expand Down