diff --git a/api/v1beta1/operatorpolicy_types.go b/api/v1beta1/operatorpolicy_types.go index 9d16f6e9..414422af 100644 --- a/api/v1beta1/operatorpolicy_types.go +++ b/api/v1beta1/operatorpolicy_types.go @@ -100,10 +100,12 @@ func (rb RemovalBehavior) ApplyDefaults() RemovalBehavior { // StatusConfig defines how resource statuses affect the OperatorPolicy status and compliance type StatusConfig struct { + // +kubebuilder:default=NonCompliant CatalogSourceUnhealthy StatusConfigAction `json:"catalogSourceUnhealthy,omitempty"` + // +kubebuilder:default=NonCompliant DeploymentsUnavailable StatusConfigAction `json:"deploymentsUnavailable,omitempty"` - UpgradesAvailable StatusConfigAction `json:"upgradesAvailable,omitempty"` - UpgradesProgressing StatusConfigAction `json:"upgradesProgressing,omitempty"` + // +kubebuilder:default=NonCompliant + UpgradesAvailable StatusConfigAction `json:"upgradesAvailable,omitempty"` } // OperatorPolicySpec defines the desired state of OperatorPolicy @@ -130,7 +132,7 @@ type OperatorPolicySpec struct { // in 'inform' mode, and which installPlans are approved when in 'enforce' mode Versions []policyv1.NonEmptyString `json:"versions,omitempty"` - //+kubebuilder:default={} + // +kubebuilder:default={} // RemovalBehavior defines what resources will be removed by enforced mustnothave policies. // When in inform mode, any resources that would be deleted if the policy was enforced will // be causes for NonCompliance, but resources that would be kept will be considered Compliant. @@ -143,6 +145,12 @@ type OperatorPolicySpec struct { // approval is not affected by this setting. This setting has no effect when the policy is in // 'mustnothave' mode. Allowed values are "None" or "Automatic". UpgradeApproval string `json:"upgradeApproval"` + + // +kubebuilder:default={} + // StatusConfig defines how resource statuses affect the OperatorPolicy status and compliance. + // Options include StatusMessageOnly, which does not affect compliance but reports a Status, + // and NonCompliant, which will update the OperatorPolicy compliance as well. + StatusConfig StatusConfig `json:"statusConfig,omitempty"` } // OperatorPolicyStatus defines the observed state of OperatorPolicy diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index b0faad9e..c11a2a47 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -87,6 +87,7 @@ func (in *OperatorPolicySpec) DeepCopyInto(out *OperatorPolicySpec) { copy(*out, *in) } out.RemovalBehavior = in.RemovalBehavior + out.StatusConfig = in.StatusConfig } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorPolicySpec. diff --git a/controllers/operatorpolicy_status.go b/controllers/operatorpolicy_status.go index 9ef2f0e5..7db006d4 100644 --- a/controllers/operatorpolicy_status.go +++ b/controllers/operatorpolicy_status.go @@ -210,14 +210,19 @@ func calculateComplianceCondition(policy *policyv1beta1.OperatorPolicy) metav1.C } } + modifyCompliance := policy.Spec.StatusConfig.UpgradesAvailable == "NonCompliant" idx, cond = policy.Status.GetCondition(installPlanConditionType) + if idx == -1 { messages = append(messages, "the status of the InstallPlan is unknown") - foundNonCompliant = true + + if modifyCompliance { + foundNonCompliant = true + } } else { messages = append(messages, cond.Message) - if cond.Status != metav1.ConditionTrue { + if cond.Status != metav1.ConditionTrue && modifyCompliance { foundNonCompliant = true } } @@ -246,27 +251,37 @@ func calculateComplianceCondition(policy *policyv1beta1.OperatorPolicy) metav1.C } } + modifyCompliance = policy.Spec.StatusConfig.DeploymentsUnavailable == "NonCompliant" idx, cond = policy.Status.GetCondition(deploymentConditionType) + if idx == -1 { messages = append(messages, "the status of the Deployments are unknown") - foundNonCompliant = true + + if modifyCompliance { + foundNonCompliant = true + } } else { messages = append(messages, cond.Message) - if cond.Status != metav1.ConditionTrue { + if cond.Status != metav1.ConditionTrue && modifyCompliance { foundNonCompliant = true } } + modifyCompliance = policy.Spec.StatusConfig.CatalogSourceUnhealthy == "NonCompliant" idx, cond = policy.Status.GetCondition(catalogSrcConditionType) + if idx == -1 { messages = append(messages, "the status of the CatalogSource is unknown") - foundNonCompliant = true + + if modifyCompliance { + foundNonCompliant = true + } } else { messages = append(messages, cond.Message) // Note: the CatalogSource condition has a different polarity - if cond.Status != metav1.ConditionFalse { + if cond.Status != metav1.ConditionFalse && modifyCompliance { foundNonCompliant = true } } diff --git a/controllers/operatorpolicy_status_test.go b/controllers/operatorpolicy_status_test.go new file mode 100644 index 00000000..2fe4850a --- /dev/null +++ b/controllers/operatorpolicy_status_test.go @@ -0,0 +1,104 @@ +package controllers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + policyv1beta1 "open-cluster-management.io/config-policy-controller/api/v1beta1" +) + +func TestStatusConfigCompliance(t *testing.T) { + t.Parallel() + + testPolicy := &policyv1beta1.OperatorPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + Spec: policyv1beta1.OperatorPolicySpec{ + Severity: "low", + RemediationAction: "enforce", + ComplianceType: "musthave", + Subscription: runtime.RawExtension{ + Raw: []byte(`{ + "source": "my-catalog", + "sourceNamespace": "my-ns", + "name": "my-operator", + "channel": "stable", + "startingCSV": "my-operator-v1" + }`), + }, + StatusConfig: policyv1beta1.StatusConfig{ + CatalogSourceUnhealthy: "NonCompliant", + DeploymentsUnavailable: "NonCompliant", + UpgradesAvailable: "NonCompliant", + }, + }, + Status: policyv1beta1.OperatorPolicyStatus{ + Conditions: []metav1.Condition{ + { + Type: "ValidPolicySpec", + Status: "True", + }, + { + Type: "OperatorGroupCompliant", + Status: "True", + }, + { + Type: "SubscriptionCompliant", + Status: "True", + }, + { + Type: "InstallPlanCompliant", + Status: "True", + }, + { + Type: "ClusterServiceVersionCompliant", + Status: "True", + }, + { + Type: "CustomResourceDefinitionCompliant", + Status: "True", + }, + { + Type: "DeploymentCompliant", + Status: "True", + }, + { + Type: "CatalogSourcesUnhealthy", + Status: "False", + }, + }, + }, + } + + // upgradesAvailable + testPolicy.Status.Conditions[3].Status = "False" + complianceCond := calculateComplianceCondition(testPolicy) + assert.Equal(t, complianceCond.Reason, "NonCompliant") + + testPolicy.Spec.StatusConfig.UpgradesAvailable = "StatusMessageOnly" + complianceCond = calculateComplianceCondition(testPolicy) + assert.Equal(t, complianceCond.Reason, "Compliant") + + // CatalogSourcesUnhealthy + testPolicy.Status.Conditions[7].Status = "True" + complianceCond = calculateComplianceCondition(testPolicy) + assert.Equal(t, complianceCond.Reason, "NonCompliant") + + testPolicy.Spec.StatusConfig.CatalogSourceUnhealthy = "StatusMessageOnly" + complianceCond = calculateComplianceCondition(testPolicy) + assert.Equal(t, complianceCond.Reason, "Compliant") + + // DeploymentsUnavailable + testPolicy.Status.Conditions[6].Status = "False" + complianceCond = calculateComplianceCondition(testPolicy) + assert.Equal(t, complianceCond.Reason, "NonCompliant") + + testPolicy.Spec.StatusConfig.DeploymentsUnavailable = "StatusMessageOnly" + complianceCond = calculateComplianceCondition(testPolicy) + assert.Equal(t, complianceCond.Reason, "Compliant") +} diff --git a/deploy/crds/kustomize_operatorpolicy/policy.open-cluster-management.io_operatorpolicies.yaml b/deploy/crds/kustomize_operatorpolicy/policy.open-cluster-management.io_operatorpolicies.yaml index a3a4fda7..f3144532 100644 --- a/deploy/crds/kustomize_operatorpolicy/policy.open-cluster-management.io_operatorpolicies.yaml +++ b/deploy/crds/kustomize_operatorpolicy/policy.open-cluster-management.io_operatorpolicies.yaml @@ -122,6 +122,35 @@ spec: - critical - Critical type: string + statusConfig: + default: {} + description: |- + StatusConfig defines how resource statuses affect the OperatorPolicy status and compliance. + Options include StatusMessageOnly, which does not affect compliance but reports a Status, + and NonCompliant, which will update the OperatorPolicy compliance as well. + properties: + catalogSourceUnhealthy: + default: NonCompliant + description: 'StatusConfigAction : StatusMessageOnly or NonCompliant' + enum: + - StatusMessageOnly + - NonCompliant + type: string + deploymentsUnavailable: + default: NonCompliant + description: 'StatusConfigAction : StatusMessageOnly or NonCompliant' + enum: + - StatusMessageOnly + - NonCompliant + type: string + upgradesAvailable: + default: NonCompliant + description: 'StatusConfigAction : StatusMessageOnly or NonCompliant' + enum: + - StatusMessageOnly + - NonCompliant + type: string + type: object subscription: description: |- Include the namespace, and any `spec` fields for the Subscription. diff --git a/deploy/crds/policy.open-cluster-management.io_operatorpolicies.yaml b/deploy/crds/policy.open-cluster-management.io_operatorpolicies.yaml index c77382e7..ce419482 100644 --- a/deploy/crds/policy.open-cluster-management.io_operatorpolicies.yaml +++ b/deploy/crds/policy.open-cluster-management.io_operatorpolicies.yaml @@ -117,6 +117,35 @@ spec: - critical - Critical type: string + statusConfig: + default: {} + description: |- + StatusConfig defines how resource statuses affect the OperatorPolicy status and compliance. + Options include StatusMessageOnly, which does not affect compliance but reports a Status, + and NonCompliant, which will update the OperatorPolicy compliance as well. + properties: + catalogSourceUnhealthy: + default: NonCompliant + description: 'StatusConfigAction : StatusMessageOnly or NonCompliant' + enum: + - StatusMessageOnly + - NonCompliant + type: string + deploymentsUnavailable: + default: NonCompliant + description: 'StatusConfigAction : StatusMessageOnly or NonCompliant' + enum: + - StatusMessageOnly + - NonCompliant + type: string + upgradesAvailable: + default: NonCompliant + description: 'StatusConfigAction : StatusMessageOnly or NonCompliant' + enum: + - StatusMessageOnly + - NonCompliant + type: string + type: object subscription: description: |- Include the namespace, and any `spec` fields for the Subscription. diff --git a/test/e2e/case38_install_operator_test.go b/test/e2e/case38_install_operator_test.go index cfdce23e..afdbe90b 100644 --- a/test/e2e/case38_install_operator_test.go +++ b/test/e2e/case38_install_operator_test.go @@ -1163,6 +1163,37 @@ var _ = Describe("Testing OperatorPolicy", Ordered, Label("supports-hosted"), fu "CatalogSource was found but is unhealthy", ) }) + It("Should become Compliant when StatusConfig is modified", func() { + By("Patching the policy StatusConfig to StatusMessageOnly") + utils.Kubectl("patch", "operatorpolicy", OpPlcName, "-n", opPolTestNS, "--type=json", "-p", + `[{"op": "replace", "path": "/spec/statusConfig/catalogSourceUnhealthy", + "value": "StatusMessageOnly"}]`) + + By("Checking the conditions and relatedObj in the policy") + check( + OpPlcName, + false, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "CatalogSource", + APIVersion: "operators.coreos.com/v1alpha1", + Metadata: policyv1.ObjectMetadata{ + Name: catSrcName, + Namespace: opPolTestNS, + }, + }, + Compliant: "NonCompliant", + Reason: "Resource found as expected but is unhealthy", + }}, + metav1.Condition{ + Type: "CatalogSourcesUnhealthy", + Status: metav1.ConditionTrue, + Reason: "CatalogSourcesFoundUnhealthy", + Message: "CatalogSource was found but is unhealthy", + }, + "CatalogSource was found but is unhealthy", + ) + }) }) Describe("Testing InstallPlan approval and status behavior", Ordered, func() { const ( @@ -1295,7 +1326,7 @@ var _ = Describe("Testing OperatorPolicy", Ordered, Label("supports-hosted"), fu "an InstallPlan to update .* is available for approval", ) }) - It("Should do the initial install when enforced, and stop at the next version", func(ctx SpecContext) { + It("Should do the upgrade when enforced, and stop at the next version", func(ctx SpecContext) { ipList, err := targetK8sDynamic.Resource(gvrInstallPlan).Namespace(opPolTestNS). List(ctx, metav1.ListOptions{}) Expect(err).NotTo(HaveOccurred()) @@ -2987,6 +3018,34 @@ var _ = Describe("Testing OperatorPolicy", Ordered, Label("supports-hosted"), fu Expect(remBehavior).To(HaveKeyWithValue("customResourceDefinitions", "Keep")) }) }) + Describe("Testing defaulted values of statusConfig in an OperatorPolicy", func() { + const ( + opPolYAML = "../resources/case38_operator_install/operator-policy-no-group.yaml" + opPolName = "oppol-no-group" + ) + + BeforeEach(func() { + preFunc() + + createObjWithParent(parentPolicyYAML, parentPolicyName, + opPolYAML, opPolTestNS, gvrPolicy, gvrOperatorPolicy) + }) + + It("Should have applied defaults to the statusConfig field", func(ctx SpecContext) { + policy, err := clientManagedDynamic.Resource(gvrOperatorPolicy).Namespace(opPolTestNS). + Get(ctx, opPolName, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(policy).NotTo(BeNil()) + + statusConfig, found, err := unstructured.NestedStringMap(policy.Object, "spec", "statusConfig") + Expect(found).To(BeTrue()) + Expect(err).NotTo(HaveOccurred()) + + Expect(statusConfig).To(HaveKeyWithValue("catalogSourceUnhealthy", "NonCompliant")) + Expect(statusConfig).To(HaveKeyWithValue("deploymentsUnavailable", "NonCompliant")) + Expect(statusConfig).To(HaveKeyWithValue("upgradesAvailable", "NonCompliant")) + }) + }) Describe("Testing operator policies that specify the same subscription", Ordered, func() { const ( musthaveYAML = "../resources/case38_operator_install/operator-policy-no-group.yaml"