Skip to content

Commit 0144f8b

Browse files
committed
Specially handle some OperatorPolicy fields
The optional `config` field in a Subscription, and the optional `targetNamespaces` and `selector` fields in an OperatorGroup often need to exactly match what the user has specified, rather than being merged with what might already be there. That is, when they are specified, it is usually better to treat them with `mustonlyhave` semantics. Previously, if a user adjusted the `targetNamespaces` to change it from one namespace to another (maybe they had a typo originally), the existing list and the new list would be merged, and the operator would likely not deploy as intended. Similarly, if a user had a nodeSelector in their `config`, adjustments would often result in a selector that did not match any nodes. Now, when one of those specific fields is set, it is handled better. When the field is not set, the existing configuration is still used. Refs: - https://issues.redhat.com/browse/ACM-22555 Signed-off-by: Justin Kulikauskas <[email protected]>
1 parent bef584c commit 0144f8b

File tree

3 files changed

+290
-22
lines changed

3 files changed

+290
-22
lines changed

controllers/operatorpolicy_controller.go

Lines changed: 109 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,13 +1138,7 @@ func (r *OperatorPolicyReconciler) musthaveOpGroup(
11381138
return false, nil, updateStatus(policy, mismatchCond("OperatorGroup"), missing, badExisting), nil
11391139
}
11401140

1141-
// check whether the specs match
1142-
desiredUnstruct, err := runtime.DefaultUnstructuredConverter.ToUnstructured(desiredOpGroup)
1143-
if err != nil {
1144-
return false, nil, false, fmt.Errorf("error converting desired OperatorGroup to an Unstructured: %w", err)
1145-
}
1146-
1147-
updateNeeded, skipUpdate, err := r.mergeObjects(ctx, desiredUnstruct, &opGroup, policy.Spec.ComplianceType)
1141+
updateNeeded, skipUpdate, err := r.mergeOpGroups(ctx, desiredOpGroup, &opGroup)
11481142
if err != nil {
11491143
return false, nil, false, fmt.Errorf("error checking if the OperatorGroup needs an update: %w", err)
11501144
}
@@ -1420,18 +1414,7 @@ func (r *OperatorPolicyReconciler) musthaveSubscription(
14201414
}
14211415

14221416
// Subscription found; check if specs match
1423-
desiredUnstruct, err := runtime.DefaultUnstructuredConverter.ToUnstructured(desiredSub)
1424-
if err != nil {
1425-
return nil, nil, false, fmt.Errorf("error converting desired Subscription to an Unstructured: %w", err)
1426-
}
1427-
1428-
// Clear `installPlanApproval` from the desired subscription when in inform mode - since that field can not
1429-
// be set in the policy, we should not check it on the object in the cluster.
1430-
if policy.Spec.RemediationAction.IsInform() {
1431-
unstructured.RemoveNestedField(desiredUnstruct, "spec", "installPlanApproval")
1432-
}
1433-
1434-
updateNeeded, skipUpdate, err := r.mergeObjects(ctx, desiredUnstruct, foundSub, policy.Spec.ComplianceType)
1417+
updateNeeded, skipUpdate, err := r.mergeSubscriptions(ctx, desiredSub, foundSub, policy.Spec.RemediationAction)
14351418
if err != nil {
14361419
return nil, nil, false, fmt.Errorf("error checking if the Subscription needs an update: %w", err)
14371420
}
@@ -2857,14 +2840,118 @@ func opPolIdentifier(namespace, name string) depclient.ObjectIdentifier {
28572840
}
28582841
}
28592842

2843+
func (r *OperatorPolicyReconciler) mergeOpGroups(
2844+
ctx context.Context,
2845+
desired *operatorv1.OperatorGroup,
2846+
existing *unstructured.Unstructured,
2847+
) (updateNeeded, updateIsForbidden bool, err error) {
2848+
forceUpdate := false
2849+
2850+
desiredUnstruct, err := runtime.DefaultUnstructuredConverter.ToUnstructured(desired)
2851+
if err != nil {
2852+
return false, false, fmt.Errorf("unable to convert desired OperatorGroup to an Unstructured: %w", err)
2853+
}
2854+
2855+
// when specified, manually handle targetNamespaces as if it's "mustonlyhave"
2856+
if len(desired.Spec.TargetNamespaces) > 0 {
2857+
desiredTarget, _, err := unstructured.NestedStringSlice(desiredUnstruct, "spec", "targetNamespaces")
2858+
if err != nil {
2859+
return false, false, fmt.Errorf("unable to get targetNamespaces from desired OperatorGroup: %w", err)
2860+
}
2861+
2862+
existingTarget, found, err := unstructured.NestedStringSlice(existing.Object, "spec", "targetNamespaces")
2863+
if err != nil {
2864+
return false, false, fmt.Errorf("unable to get targetNamespaces from existing OperatorGroup: %w", err)
2865+
} else if !found {
2866+
forceUpdate = true
2867+
unstructured.SetNestedStringSlice(existing.Object,
2868+
desired.Spec.TargetNamespaces, "spec", "targetNamespaces")
2869+
} else if eq, _ := deeplyEquivalent(desiredTarget, existingTarget, true); !eq {
2870+
forceUpdate = true
2871+
unstructured.SetNestedStringSlice(existing.Object,
2872+
desired.Spec.TargetNamespaces, "spec", "targetNamespaces")
2873+
}
2874+
}
2875+
2876+
usesExpressions := desired.Spec.Selector != nil && len(desired.Spec.Selector.MatchExpressions) > 0
2877+
usesLabels := desired.Spec.Selector != nil && len(desired.Spec.Selector.MatchLabels) > 0
2878+
2879+
// when specified, manually handle selector as if it's "mustonlyhave"
2880+
if usesExpressions || usesLabels {
2881+
desiredSelector, _, err := unstructured.NestedMap(desiredUnstruct, "spec", "selector")
2882+
if err != nil {
2883+
return false, false, fmt.Errorf("unable to get selector from desired OperatorGroup: %w", err)
2884+
}
2885+
2886+
existingSelector, found, err := unstructured.NestedMap(existing.Object, "spec", "selector")
2887+
if err != nil {
2888+
return false, false, fmt.Errorf("unable to get selector from existing OperatorGroup: %w", err)
2889+
} else if !found {
2890+
forceUpdate = true
2891+
unstructured.SetNestedMap(existing.Object, desiredSelector, "spec", "selector")
2892+
} else if eq, _ := deeplyEquivalent(desiredSelector, existingSelector, true); !eq {
2893+
forceUpdate = true
2894+
unstructured.SetNestedMap(existing.Object, desiredSelector, "spec", "selector")
2895+
}
2896+
}
2897+
2898+
updateNeeded, forbidden, err := r.mergeObjects(ctx, desiredUnstruct, existing)
2899+
2900+
return updateNeeded || forceUpdate, forbidden, err
2901+
}
2902+
2903+
func (r *OperatorPolicyReconciler) mergeSubscriptions(
2904+
ctx context.Context,
2905+
desired *operatorv1alpha1.Subscription,
2906+
existing *unstructured.Unstructured,
2907+
action policyv1.RemediationAction,
2908+
) (updateNeeded, updateIsForbidden bool, err error) {
2909+
forceUpdate := false
2910+
2911+
desiredUnstruct, err := runtime.DefaultUnstructuredConverter.ToUnstructured(desired)
2912+
if err != nil {
2913+
return false, false, fmt.Errorf("unable to convert desired Subscription to an Unstructured: %w", err)
2914+
}
2915+
2916+
// when specified, manually handle config as if it's "mustonlyhave"
2917+
if desired.Spec.Config != nil {
2918+
desiredConfig, _, err := unstructured.NestedMap(desiredUnstruct, "spec", "config")
2919+
if err != nil {
2920+
return false, false, fmt.Errorf("unable to get config from desired Subscription")
2921+
}
2922+
2923+
if len(desiredConfig) > 0 {
2924+
existingConfig, found, err := unstructured.NestedMap(existing.Object, "spec", "config")
2925+
if err != nil {
2926+
return false, false, fmt.Errorf("unable to get config from existing Subscription")
2927+
} else if !found {
2928+
forceUpdate = true
2929+
unstructured.SetNestedMap(existing.Object, desiredConfig, "spec", "config")
2930+
} else if eq, _ := deeplyEquivalent(desiredConfig, existingConfig, true); !eq {
2931+
forceUpdate = true
2932+
unstructured.SetNestedMap(existing.Object, desiredConfig, "spec", "config")
2933+
}
2934+
}
2935+
}
2936+
2937+
// Clear `installPlanApproval` from the desired subscription when in inform mode - since that field can not
2938+
// be set in the policy, we should not check it on the object in the cluster.
2939+
if action.IsInform() {
2940+
unstructured.RemoveNestedField(desiredUnstruct, "spec", "installPlanApproval")
2941+
}
2942+
2943+
updateNeeded, forbidden, err := r.mergeObjects(ctx, desiredUnstruct, existing)
2944+
2945+
return updateNeeded || forceUpdate, forbidden, err
2946+
}
2947+
28602948
// mergeObjects takes fields from the desired object and sets/merges them on the
28612949
// existing object. It checks and returns whether an update is really necessary
28622950
// with a server-side dry-run.
28632951
func (r *OperatorPolicyReconciler) mergeObjects(
28642952
ctx context.Context,
2865-
desired map[string]interface{},
2953+
desired map[string]any,
28662954
existing *unstructured.Unstructured,
2867-
complianceType policyv1.ComplianceType,
28682955
) (updateNeeded, updateIsForbidden bool, err error) {
28692956
desiredObj := &unstructured.Unstructured{Object: desired}
28702957

@@ -2873,7 +2960,7 @@ func (r *OperatorPolicyReconciler) mergeObjects(
28732960
removeFieldsForComparison(existingObjectCopy)
28742961

28752962
//nolint:dogsled
2876-
_, errMsg, updateNeeded, _, _ := handleKeys(desiredObj, existing, existingObjectCopy, complianceType, "")
2963+
_, errMsg, updateNeeded, _, _ := handleKeys(desiredObj, existing, existingObjectCopy, policyv1.MustHave, "")
28772964
if errMsg != "" {
28782965
return updateNeeded, false, errors.New(errMsg)
28792966
}

test/e2e/case38_install_operator_test.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3827,4 +3827,154 @@ var _ = Describe("Testing OperatorPolicy", Ordered, Label("supports-hosted"), fu
38273827
checkCompliance(bundlePolName, testNamespace, olmWaitTimeout*2, policyv1.Compliant)
38283828
})
38293829
})
3830+
3831+
Describe("Test changes to subscription config and operator group selector", Ordered, func() {
3832+
const (
3833+
policyYAML = "../resources/case38_operator_install/operator-policy-with-group-and-config.yaml"
3834+
policyName = "oppol-with-group-and-config"
3835+
)
3836+
3837+
BeforeEach(func() {
3838+
preFunc()
3839+
})
3840+
3841+
It("Should be able to update the subscription when the policy changes", func(ctx SpecContext) {
3842+
createObjWithParent(parentPolicyYAML, parentPolicyName,
3843+
policyYAML, testNamespace, gvrPolicy, gvrOperatorPolicy)
3844+
3845+
By("Verifying the initial state of the policy")
3846+
check(
3847+
policyName,
3848+
false,
3849+
[]policyv1.RelatedObject{{
3850+
Object: policyv1.ObjectResource{
3851+
Kind: "Subscription",
3852+
APIVersion: "operators.coreos.com/v1alpha1",
3853+
},
3854+
Compliant: "Compliant",
3855+
Reason: "Resource found as expected",
3856+
}},
3857+
metav1.Condition{
3858+
Type: "SubscriptionCompliant",
3859+
Status: metav1.ConditionTrue,
3860+
Reason: "SubscriptionMatches",
3861+
Message: "the Subscription matches what is required by the policy",
3862+
},
3863+
"the Subscription required by the policy was created",
3864+
)
3865+
3866+
By("Verifying the initial state of the config in the subscription")
3867+
Eventually(func(g Gomega) {
3868+
sub, err := targetK8sDynamic.Resource(gvrSubscription).Namespace(opPolTestNS).
3869+
Get(ctx, "example-operator", metav1.GetOptions{})
3870+
g.Expect(err).NotTo(HaveOccurred())
3871+
g.Expect(sub).NotTo(BeNil())
3872+
3873+
sel, found, err := unstructured.NestedStringMap(sub.Object, "spec", "config", "nodeSelector")
3874+
g.Expect(err).NotTo(HaveOccurred())
3875+
g.Expect(found).To(BeTrue())
3876+
g.Expect(sel).To(HaveKeyWithValue("node-role.kubernetes.io/infra", ""))
3877+
}, olmWaitTimeout, 5).Should(Succeed())
3878+
3879+
By("Verifying the initial state of the operator group")
3880+
Eventually(func(g Gomega) {
3881+
group, err := targetK8sDynamic.Resource(gvrOperatorGroup).Namespace(opPolTestNS).
3882+
Get(ctx, "scoped-operator-group", metav1.GetOptions{})
3883+
g.Expect(err).NotTo(HaveOccurred())
3884+
g.Expect(group).NotTo(BeNil())
3885+
3886+
target, found, err := unstructured.NestedStringSlice(group.Object, "spec", "targetNamespaces")
3887+
g.Expect(err).NotTo(HaveOccurred())
3888+
g.Expect(found).To(BeTrue())
3889+
g.Expect(target).To(ContainElement("operator-policy-testns"))
3890+
3891+
_, found, err = unstructured.NestedMap(group.Object, "spec", "selector")
3892+
g.Expect(err).NotTo(HaveOccurred())
3893+
g.Expect(found).To(BeFalse())
3894+
}, olmWaitTimeout, 5).Should(Succeed())
3895+
3896+
By("Adjusting the policy to observe how the subscription and operator group change")
3897+
utils.Kubectl("patch", "operatorpolicy", policyName, "-n", testNamespace, "--type=json", "-p", "["+
3898+
`{"op": "replace", "path": "/spec/subscription/config/nodeSelector",`+
3899+
` "value": {"node-role.kubernetes.io/worker": ""} },`+
3900+
`{"op": "replace", "path": "/spec/operatorGroup/targetNamespaces",`+
3901+
` "value": ["another-namespace"] },`+
3902+
`{"op": "add", "path": "/spec/operatorGroup/selector",`+
3903+
` "value": {"matchLabels": {"foo": "bar"}}}`+
3904+
"]")
3905+
3906+
By("Verifying the new state of the config in the subscription")
3907+
Eventually(func(g Gomega) {
3908+
sub, err := targetK8sDynamic.Resource(gvrSubscription).Namespace(opPolTestNS).
3909+
Get(ctx, "example-operator", metav1.GetOptions{})
3910+
g.Expect(err).NotTo(HaveOccurred())
3911+
g.Expect(sub).NotTo(BeNil())
3912+
3913+
sel, found, err := unstructured.NestedStringMap(sub.Object, "spec", "config", "nodeSelector")
3914+
g.Expect(err).NotTo(HaveOccurred())
3915+
g.Expect(found).To(BeTrue())
3916+
g.Expect(sel).NotTo(HaveKeyWithValue("node-role.kubernetes.io/infra", ""))
3917+
g.Expect(sel).To(HaveKeyWithValue("node-role.kubernetes.io/worker", ""))
3918+
}, olmWaitTimeout, 5).Should(Succeed())
3919+
3920+
By("Verifying the new state of the operator group")
3921+
Eventually(func(g Gomega) {
3922+
group, err := targetK8sDynamic.Resource(gvrOperatorGroup).Namespace(opPolTestNS).
3923+
Get(ctx, "scoped-operator-group", metav1.GetOptions{})
3924+
g.Expect(err).NotTo(HaveOccurred())
3925+
g.Expect(group).NotTo(BeNil())
3926+
3927+
target, found, err := unstructured.NestedStringSlice(group.Object, "spec", "targetNamespaces")
3928+
g.Expect(err).NotTo(HaveOccurred())
3929+
g.Expect(found).To(BeTrue())
3930+
g.Expect(target).NotTo(ContainElement("operator-policy-testns"))
3931+
g.Expect(target).To(ContainElement("another-namespace"))
3932+
3933+
sel, found, err := unstructured.NestedMap(group.Object, "spec", "selector")
3934+
g.Expect(err).NotTo(HaveOccurred())
3935+
g.Expect(found).To(BeTrue())
3936+
g.Expect(sel).To(HaveKey("matchLabels"))
3937+
}, olmWaitTimeout, 5).Should(Succeed())
3938+
3939+
By("Removing some fields from the policy to verify previous configurations are preserved")
3940+
utils.Kubectl("patch", "operatorpolicy", policyName, "-n", testNamespace, "--type=json", "-p", "["+
3941+
`{"op": "remove", "path": "/spec/subscription/config/nodeSelector"},`+
3942+
`{"op": "remove", "path": "/spec/operatorGroup/targetNamespaces"},`+
3943+
`{"op": "replace", "path": "/spec/operatorGroup/selector",`+
3944+
` "value": {"matchExpressions": [{"key": "foo", "operator": "In", "values": ["bar"]}]}}`+
3945+
"]")
3946+
3947+
By("Verifying the final state of the config in the subscription")
3948+
Eventually(func(g Gomega) {
3949+
sub, err := targetK8sDynamic.Resource(gvrSubscription).Namespace(opPolTestNS).
3950+
Get(ctx, "example-operator", metav1.GetOptions{})
3951+
g.Expect(err).NotTo(HaveOccurred())
3952+
g.Expect(sub).NotTo(BeNil())
3953+
3954+
sel, found, err := unstructured.NestedStringMap(sub.Object, "spec", "config", "nodeSelector")
3955+
g.Expect(err).NotTo(HaveOccurred())
3956+
g.Expect(found).To(BeTrue())
3957+
g.Expect(sel).To(HaveKeyWithValue("node-role.kubernetes.io/worker", ""))
3958+
}, olmWaitTimeout, 5).Should(Succeed())
3959+
3960+
By("Verifying the final state of the operator group")
3961+
Eventually(func(g Gomega) {
3962+
group, err := targetK8sDynamic.Resource(gvrOperatorGroup).Namespace(opPolTestNS).
3963+
Get(ctx, "scoped-operator-group", metav1.GetOptions{})
3964+
g.Expect(err).NotTo(HaveOccurred())
3965+
g.Expect(group).NotTo(BeNil())
3966+
3967+
target, found, err := unstructured.NestedStringSlice(group.Object, "spec", "targetNamespaces")
3968+
g.Expect(err).NotTo(HaveOccurred())
3969+
g.Expect(found).To(BeTrue())
3970+
g.Expect(target).To(ContainElement("another-namespace"))
3971+
3972+
sel, found, err := unstructured.NestedMap(group.Object, "spec", "selector")
3973+
g.Expect(err).NotTo(HaveOccurred())
3974+
g.Expect(found).To(BeTrue())
3975+
g.Expect(sel).NotTo(HaveKey("matchLabels"))
3976+
g.Expect(sel).To(HaveKey("matchExpressions"))
3977+
}, olmWaitTimeout, 5).Should(Succeed())
3978+
})
3979+
})
38303980
})
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
apiVersion: policy.open-cluster-management.io/v1beta1
2+
kind: OperatorPolicy
3+
metadata:
4+
name: oppol-with-group-and-config
5+
labels:
6+
policy.open-cluster-management.io/cluster-name: "managed"
7+
policy.open-cluster-management.io/cluster-namespace: "managed"
8+
ownerReferences:
9+
- apiVersion: policy.open-cluster-management.io/v1
10+
kind: Policy
11+
name: parent-policy
12+
uid: 12345678-90ab-cdef-1234-567890abcdef # must be replaced before creation
13+
spec:
14+
remediationAction: enforce
15+
severity: medium
16+
complianceType: musthave
17+
operatorGroup:
18+
name: scoped-operator-group
19+
namespace: operator-policy-testns
20+
targetNamespaces: # will be changed
21+
- operator-policy-testns
22+
subscription:
23+
channel: stable
24+
config:
25+
nodeSelector: # will be changed
26+
node-role.kubernetes.io/infra: ''
27+
name: example-operator
28+
namespace: operator-policy-testns
29+
source: grc-mock-source
30+
sourceNamespace: olm
31+
upgradeApproval: Automatic

0 commit comments

Comments
 (0)