Skip to content

Commit 47b2ce2

Browse files
Fix operatorpolicy confusing status when invalid
When the subscription section of the OperatorPolicy is invalid, the status might incorrectly indicate that other parts of the policy are invalid. This, combined with the many "... because the policy is invalid" clauses in the overall status, makes it very confusing to read and understand what parts might need to be fixed by the user. Ref: https://issues.redhat.com/browse/ACM-16781 Signed-off-by: yiraeChristineKim <[email protected]>
1 parent c20f555 commit 47b2ce2

File tree

3 files changed

+291
-43
lines changed

3 files changed

+291
-43
lines changed

controllers/operatorpolicy_controller.go

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -489,9 +489,10 @@ func (r *OperatorPolicyReconciler) buildResources(ctx context.Context, policy *p
489489
) {
490490
var returnedErr error
491491
var tmplResolver *templates.TemplateResolver
492+
var sub *operatorv1alpha1.Subscription
493+
var opGroup *operatorv1.OperatorGroup
492494

493495
opLog := ctrl.LoggerFrom(ctx)
494-
validationErrors := make([]error, 0)
495496
disableTemplates := false
496497

497498
if disableAnnotation, ok := policy.GetAnnotations()["policy.open-cluster-management.io/disable-templates"]; ok {
@@ -505,12 +506,16 @@ func (r *OperatorPolicyReconciler) buildResources(ctx context.Context, policy *p
505506
r.DynamicWatcher, templates.Config{SkipBatchManagement: true},
506507
)
507508
if err != nil {
508-
validationErrors = append(validationErrors, fmt.Errorf("unable to create template resolver: %w", err))
509-
} else {
510-
err := resolveVersionsTemplates(policy, tmplResolver)
511-
if err != nil {
512-
validationErrors = append(validationErrors, err)
513-
}
509+
newError := fmt.Errorf("unable to create template resolver: %w", err)
510+
511+
return sub, opGroup, updateStatus(policy, validationCond([]error{newError})), returnedErr
512+
}
513+
514+
err = resolveVersionsTemplates(policy, tmplResolver)
515+
if err != nil {
516+
newError := fmt.Errorf("unable to create template resolver: %w", err)
517+
518+
return sub, opGroup, updateStatus(policy, validationCond([]error{newError})), returnedErr
514519
}
515520
} else {
516521
opLog.V(1).Info("Templates disabled by annotation")
@@ -530,18 +535,22 @@ func (r *OperatorPolicyReconciler) buildResources(ctx context.Context, policy *p
530535
returnedErr = err
531536
}
532537

533-
validationErrors = append(validationErrors, err)
538+
updateStatus(policy, validationCond([]error{err}))
539+
540+
return sub, opGroup, true, returnedErr
534541
}
535542

536543
if sub != nil && sub.Namespace == "" {
537544
if r.DefaultNamespace != "" {
538545
sub.Namespace = r.DefaultNamespace
539546
} else {
540-
validationErrors = append(validationErrors, errors.New("namespace is required in spec.subscription"))
547+
newError := errors.New("namespace is required in spec.subscription")
548+
549+
return sub, opGroup, updateStatus(policy, validationCond([]error{newError})), returnedErr
541550
}
542551
}
543552
} else {
544-
validationErrors = append(validationErrors, subErr)
553+
return sub, opGroup, updateStatus(policy, validationCond([]error{subErr})), returnedErr
545554
}
546555

547556
opGroupNS := r.DefaultNamespace
@@ -551,23 +560,24 @@ func (r *OperatorPolicyReconciler) buildResources(ctx context.Context, policy *p
551560

552561
opGroup, ogErr := buildOperatorGroup(policy, opGroupNS, tmplResolver)
553562
if ogErr != nil {
554-
validationErrors = append(validationErrors, ogErr)
555-
} else {
556-
watcher := opPolIdentifier(policy.Namespace, policy.Name)
563+
return sub, opGroup, updateStatus(policy, validationCond([]error{ogErr})), returnedErr
564+
}
557565

558-
gotNamespace, err := r.DynamicWatcher.Get(watcher, namespaceGVK, "", opGroupNS)
559-
if err != nil {
560-
return sub, opGroup, false, fmt.Errorf("error getting operator namespace: %w", err)
561-
}
566+
watcher := opPolIdentifier(policy.Namespace, policy.Name)
562567

563-
if gotNamespace == nil && policy.Spec.ComplianceType.IsMustHave() {
564-
validationErrors = append(validationErrors,
565-
fmt.Errorf("the operator namespace ('%v') does not exist", opGroupNS))
566-
}
568+
gotNamespace, err := r.DynamicWatcher.Get(watcher, namespaceGVK, "", opGroupNS)
569+
if err != nil {
570+
return sub, opGroup, false, fmt.Errorf("error getting operator namespace: %w", err)
571+
}
572+
573+
if gotNamespace == nil && policy.Spec.ComplianceType.IsMustHave() {
574+
newError := fmt.Errorf("the operator namespace ('%v') does not exist", opGroupNS)
575+
576+
return sub, opGroup, updateStatus(policy, validationCond([]error{newError})), returnedErr
567577
}
568578

569579
changed, overlapErr, apiErr := r.checkSubOverlap(ctx, policy, sub)
570-
if apiErr != nil && returnedErr == nil {
580+
if apiErr != nil {
571581
returnedErr = apiErr
572582
}
573583

@@ -577,10 +587,12 @@ func (r *OperatorPolicyReconciler) buildResources(ctx context.Context, policy *p
577587
sub = nil
578588
opGroup = nil
579589

580-
validationErrors = append(validationErrors, overlapErr)
590+
updateStatus(policy, validationCond([]error{overlapErr}))
591+
592+
return sub, opGroup, true, returnedErr
581593
}
582594

583-
changed = updateStatus(policy, validationCond(validationErrors)) || changed
595+
changed = changed || updateStatus(policy, validationCond([]error{}))
584596

585597
return sub, opGroup, changed, returnedErr
586598
}

controllers/operatorpolicy_controller_test.go

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,230 @@ import (
1414
"k8s.io/apimachinery/pkg/runtime/schema"
1515
"k8s.io/apimachinery/pkg/util/sets"
1616
"k8s.io/apimachinery/pkg/util/yaml"
17+
dynamicfake "k8s.io/client-go/dynamic/fake"
1718

1819
policyv1beta1 "open-cluster-management.io/config-policy-controller/api/v1beta1"
1920
)
2021

22+
func TestBuildResources_SubscriptionErrorUpdatesStatus(t *testing.T) {
23+
t.Parallel()
24+
25+
r := &OperatorPolicyReconciler{}
26+
27+
policy := &policyv1beta1.OperatorPolicy{
28+
ObjectMeta: metav1.ObjectMeta{
29+
Name: "my-policy",
30+
Namespace: "default",
31+
Annotations: map[string]string{
32+
"policy.open-cluster-management.io/disable-templates": "true",
33+
},
34+
},
35+
Spec: policyv1beta1.OperatorPolicySpec{
36+
Severity: "low",
37+
RemediationAction: "inform",
38+
ComplianceType: "musthave",
39+
Subscription: runtime.RawExtension{
40+
Raw: []byte(`{
41+
"namespace": "default",
42+
"source": "my-catalog",
43+
"sourceNamespace": "my-ns",
44+
"name": "",
45+
"channel": "stable"
46+
}`),
47+
},
48+
UpgradeApproval: "None",
49+
},
50+
}
51+
52+
_, _, changed, returnedErr := r.buildResources(t.Context(), policy)
53+
assert.True(t, changed, "expected status to be updated")
54+
assert.NoError(t, returnedErr)
55+
56+
_, cond := policy.Status.GetCondition(validPolicyConditionType)
57+
assert.Equal(t, metav1.ConditionFalse, cond.Status)
58+
assert.Equal(t, "InvalidPolicySpec", cond.Reason)
59+
assert.Equal(t, "name is required in spec.subscription", cond.Message)
60+
}
61+
62+
func TestBuildResources_subInstallPlanApprovalErrorUpdatesStatus(t *testing.T) {
63+
t.Parallel()
64+
65+
r := &OperatorPolicyReconciler{}
66+
67+
policy := &policyv1beta1.OperatorPolicy{
68+
ObjectMeta: metav1.ObjectMeta{
69+
Name: "my-policy",
70+
Namespace: "default",
71+
Annotations: map[string]string{
72+
"policy.open-cluster-management.io/disable-templates": "true",
73+
},
74+
},
75+
Spec: policyv1beta1.OperatorPolicySpec{
76+
Severity: "low",
77+
RemediationAction: "inform",
78+
ComplianceType: "musthave",
79+
Subscription: runtime.RawExtension{
80+
Raw: []byte(`{
81+
"namespace": "default",
82+
"source": "my-catalog",
83+
"sourceNamespace": "my-ns",
84+
"name": "my-operator",
85+
"channel": "stable",
86+
"installPlanApproval": "ERR"
87+
}`),
88+
},
89+
UpgradeApproval: "None",
90+
},
91+
}
92+
93+
_, _, changed, returnedErr := r.buildResources(t.Context(), policy)
94+
assert.True(t, changed, "expected status to be updated")
95+
assert.NoError(t, returnedErr)
96+
97+
_, cond := policy.Status.GetCondition(validPolicyConditionType)
98+
assert.Equal(t, metav1.ConditionFalse, cond.Status)
99+
assert.Equal(t, "InvalidPolicySpec", cond.Reason)
100+
assert.Equal(t, "installPlanApproval is prohibited in spec.subscription", cond.Message)
101+
}
102+
103+
func TestBuildResources_SubDefaultsPkgManifestNotFoundUpdatesStatusAndReturnsErr(t *testing.T) {
104+
// To test line 542
105+
t.Parallel()
106+
107+
r := &OperatorPolicyReconciler{
108+
// Use a fake dynamic client without any objects so PackageManifest GET returns NotFound
109+
DynamicClient: dynamicfake.NewSimpleDynamicClient(runtime.NewScheme()),
110+
}
111+
112+
policy := &policyv1beta1.OperatorPolicy{
113+
ObjectMeta: metav1.ObjectMeta{
114+
Name: "my-policy",
115+
Namespace: "default",
116+
Annotations: map[string]string{
117+
"policy.open-cluster-management.io/disable-templates": "true",
118+
},
119+
},
120+
Spec: policyv1beta1.OperatorPolicySpec{
121+
Severity: "low",
122+
RemediationAction: "inform",
123+
ComplianceType: "musthave",
124+
Subscription: runtime.RawExtension{
125+
// Omit namespace key entirely so buildSubscription succeeds with empty namespace,
126+
// and omit source/sourceNamespace so defaultsNeeded = true (triggers PackageManifest lookup).
127+
Raw: []byte(`{
128+
"name": "my-operator",
129+
"channel": "stable",
130+
"startingCSV": "my-operator-v1"
131+
}`),
132+
},
133+
UpgradeApproval: "None",
134+
},
135+
}
136+
137+
sub, opGroup, changed, returnedErr := r.buildResources(t.Context(), policy)
138+
assert.True(t, changed, "expected status to be updated")
139+
assert.ErrorIs(t, returnedErr, ErrPackageManifest, "expected returned error to wrap ErrPackageManifest")
140+
141+
_, cond := policy.Status.GetCondition(validPolicyConditionType)
142+
assert.Equal(t, metav1.ConditionFalse, cond.Status)
143+
assert.Equal(t, "InvalidPolicySpec", cond.Reason)
144+
assert.Equal(t,
145+
"the subscription defaults could not be determined because the PackageManifest was not found",
146+
cond.Message)
147+
assert.Nil(t, sub, "expected subscription to be nil")
148+
assert.Nil(t, opGroup, "expected operator group to be nil")
149+
}
150+
151+
func TestBuildResources_OperatorGroupErrorUpdatesStatus(t *testing.T) {
152+
t.Parallel()
153+
154+
r := &OperatorPolicyReconciler{}
155+
156+
policy := &policyv1beta1.OperatorPolicy{
157+
ObjectMeta: metav1.ObjectMeta{
158+
Name: "my-policy",
159+
Namespace: "default",
160+
Annotations: map[string]string{
161+
"policy.open-cluster-management.io/disable-templates": "true",
162+
},
163+
},
164+
Spec: policyv1beta1.OperatorPolicySpec{
165+
Severity: "low",
166+
RemediationAction: "inform",
167+
ComplianceType: "musthave",
168+
Subscription: runtime.RawExtension{
169+
Raw: []byte(`{
170+
"namespace": "default",
171+
"source": "my-catalog",
172+
"sourceNamespace": "my-ns",
173+
"name": "my-operator",
174+
"channel": "stable"
175+
}`),
176+
},
177+
// Invalid operatorGroup (missing name) should trigger a validation error
178+
OperatorGroup: &runtime.RawExtension{
179+
Raw: []byte(`{}`),
180+
},
181+
UpgradeApproval: "None",
182+
},
183+
}
184+
185+
_, _, changed, returnedErr := r.buildResources(t.Context(), policy)
186+
assert.True(t, changed, "expected status to be updated")
187+
assert.NoError(t, returnedErr)
188+
189+
_, cond := policy.Status.GetCondition(validPolicyConditionType)
190+
assert.Equal(t, metav1.ConditionFalse, cond.Status)
191+
assert.Equal(t, "InvalidPolicySpec", cond.Reason)
192+
assert.Equal(t, "name is required in spec.operatorGroup", cond.Message)
193+
}
194+
195+
func TestBuildResources_TemplateResolverCreationErrorUpdatesStatus(t *testing.T) {
196+
t.Parallel()
197+
198+
r := &OperatorPolicyReconciler{
199+
DynamicWatcher: nil,
200+
}
201+
202+
policy := &policyv1beta1.OperatorPolicy{
203+
ObjectMeta: metav1.ObjectMeta{
204+
Name: "my-policy",
205+
Namespace: "default",
206+
},
207+
Spec: policyv1beta1.OperatorPolicySpec{
208+
Severity: "low",
209+
RemediationAction: "inform",
210+
ComplianceType: "musthave",
211+
// Include an obviously bad template so template resolution would fail if reached.
212+
// In this test, resolver creation itself is expected to fail earlier.
213+
Versions: []string{"{{ .badTemplate "},
214+
Subscription: runtime.RawExtension{
215+
Raw: []byte(`{
216+
"namespace": "default",
217+
"name": "my-operator",
218+
"channel": "stable",
219+
"startingCSV": "my-operator-v1"
220+
}`),
221+
},
222+
UpgradeApproval: "None",
223+
},
224+
}
225+
226+
sub, opGroup, changed, returnedErr := r.buildResources(t.Context(), policy)
227+
assert.True(t, changed, "expected status to be updated")
228+
assert.NoError(t, returnedErr)
229+
assert.Nil(t, sub, "expected subscription to be nil on early return")
230+
assert.Nil(t, opGroup, "expected operator group to be nil on early return")
231+
232+
_, cond := policy.Status.GetCondition(validPolicyConditionType)
233+
assert.Equal(t, metav1.ConditionFalse, cond.Status)
234+
assert.Equal(t, "InvalidPolicySpec", cond.Reason)
235+
assert.Equal(t,
236+
"unable to create template resolver: could not resolve the version template: "+
237+
"failed to parse the template JSON string [\"{{ .badTemplate \"]: template: tmpl:1: "+
238+
"unterminated character constant", cond.Message)
239+
}
240+
21241
func TestBuildSubscription(t *testing.T) {
22242
testPolicy := &policyv1beta1.OperatorPolicy{
23243
ObjectMeta: metav1.ObjectMeta{

0 commit comments

Comments
 (0)