From 59a18f829a0658cadd0c88dc754d4511d5a31636 Mon Sep 17 00:00:00 2001 From: Justin Kulikauskas Date: Fri, 29 Mar 2024 13:49:19 -0400 Subject: [PATCH 1/4] Add/Update some comment strings Signed-off-by: Justin Kulikauskas --- controllers/operatorpolicy_status.go | 35 ++++++++++++++++++++++-- test/e2e/case38_install_operator_test.go | 5 ++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/controllers/operatorpolicy_status.go b/controllers/operatorpolicy_status.go index 20ed84f3..0ae9c650 100644 --- a/controllers/operatorpolicy_status.go +++ b/controllers/operatorpolicy_status.go @@ -252,6 +252,7 @@ func calculateComplianceCondition(policy *policyv1beta1.OperatorPolicy) metav1.C } else { messages = append(messages, cond.Message) + // Note: the CatalogSource condition has a different polarity if cond.Status != metav1.ConditionFalse { foundNonCompliant = true } @@ -276,6 +277,9 @@ func calculateComplianceCondition(policy *policyv1beta1.OperatorPolicy) metav1.C } } +// emitComplianceEvent creates a compliance event on the parent policy (if there is +// one) based on the given compliance condition. It returns an error if creating the +// event fails. func (r *OperatorPolicyReconciler) emitComplianceEvent( ctx context.Context, policy *policyv1beta1.OperatorPolicy, @@ -374,6 +378,8 @@ func condType(kind string) string { } } +// invalidCausingUnknownCond returns a NonCompliant condition, with Reason 'InvalidPolicySpec' +// and a Message like 'the status of the ____ could not be determined because the policy is invalid' func invalidCausingUnknownCond(kind string) metav1.Condition { return metav1.Condition{ Type: condType(kind), @@ -438,7 +444,7 @@ func mismatchCondUnfixable(kind string) metav1.Condition { } } -// updatedCond returns a Compliant condition, with a Reason like'____Updated', +// updatedCond returns a Compliant condition, with a Reason like '____Updated', // and a Message like 'the ____ was updated to match the policy' func updatedCond(kind string) metav1.Condition { return metav1.Condition{ @@ -449,6 +455,10 @@ func updatedCond(kind string) metav1.Condition { } } +// validationCond returns a condition based on the errors passed in... +// If no errors are passed, it will be Compliant, with Reason 'PolicyValidated'. +// If errors are passed in, it is NonCompliant, with Reason 'InvalidPolicySpec', +// and a Message combining all of the errors. func validationCond(validationErrors []error) metav1.Condition { if len(validationErrors) == 0 { return metav1.Condition{ @@ -600,13 +610,17 @@ func buildCSVCond(csv *operatorv1alpha1.ClusterServiceVersion) metav1.Condition } } +// noCSVCond is a NonCompliant condition with Reason 'RelevantCSVNotFound' var noCSVCond = metav1.Condition{ Type: csvConditionType, Status: metav1.ConditionFalse, - Reason: "RelevantCSVFound", + Reason: "RelevantCSVNotFound", Message: "A relevant installed ClusterServiceVersion could not be found", } +// buildDeploymentCond creates a Condition for deployments. If any are not at their +// minimum availability, the condition will be NonCompliant, and the message will +// list the unavailable deployments. func buildDeploymentCond( depsExist bool, unavailableDeps []appsv1.Deployment, @@ -641,6 +655,8 @@ func buildDeploymentCond( } } +// noDeploymentsCond is a Compliant condition with Reason 'NoRelevantDeployments', +// and a message saying that the CSV is missing. var noDeploymentsCond = metav1.Condition{ Type: deploymentConditionType, Status: metav1.ConditionTrue, @@ -743,6 +759,8 @@ func updatedObj(obj client.Object) policyv1.RelatedObject { } } +// nonCompObj returns a NonCompliant RelatedObject with the given reason. +// It includes the UID of the given object. func nonCompObj(obj client.Object, reason string) policyv1.RelatedObject { return policyv1.RelatedObject{ Object: policyv1.ObjectResourceFromObj(obj), @@ -791,6 +809,10 @@ func noInstallPlansObj(namespace string) policyv1.RelatedObject { } } +// existingInstallPlanObj returns a RelatedObject for the InstallPlan, with a reason +// like 'The InstallPlan is ____' based on the phase. Usually the object will not +// have a compliance associated with it, but if it requires approval or is actively +// installing, then it will be NonCompliant. func existingInstallPlanObj(ip client.Object, phase string) policyv1.RelatedObject { relObj := policyv1.RelatedObject{ Object: policyv1.ObjectResourceFromObj(ip), @@ -818,6 +840,8 @@ func existingInstallPlanObj(ip client.Object, phase string) policyv1.RelatedObje return relObj } +// missingCSVObj returns a NonCompliant RelatedObject for the ClusterServiceVersion, +// with Reason 'Resource not found but should exist' func missingCSVObj(name string, namespace string) policyv1.RelatedObject { return policyv1.RelatedObject{ Object: policyv1.ObjectResource{ @@ -833,6 +857,9 @@ func missingCSVObj(name string, namespace string) policyv1.RelatedObject { } } +// existingCSVObj returns a RelatedObject for the ClusterServiceVersion, with a +// Reason that reflects the CSV's status, and will only be Compliant if the CSV +// is in the Succeeded phase. func existingCSVObj(csv *operatorv1alpha1.ClusterServiceVersion) policyv1.RelatedObject { compliance := policyv1.NonCompliant if csv.Status.Phase == operatorv1alpha1.CSVPhaseSucceeded { @@ -862,6 +889,8 @@ var noExistingCSVObj = policyv1.RelatedObject{ Reason: "No relevant ClusterServiceVersion found", } +// missingDeploymentObj returns a NonCompliant RelatedObject for a Deployment, +// with Reason 'Resource not found but should exist' func missingDeploymentObj(name string, namespace string) policyv1.RelatedObject { return policyv1.RelatedObject{ Object: policyv1.ObjectResource{ @@ -877,6 +906,8 @@ func missingDeploymentObj(name string, namespace string) policyv1.RelatedObject } } +// existingDeploymentObj returns a RelatedObject for a Deployment, which will +// be Compliant if there are no unavailable replicas on the deployment. func existingDeploymentObj(dep *appsv1.Deployment) policyv1.RelatedObject { compliance := policyv1.NonCompliant reason := "Deployment Unavailable" diff --git a/test/e2e/case38_install_operator_test.go b/test/e2e/case38_install_operator_test.go index 835823ff..0f1cab87 100644 --- a/test/e2e/case38_install_operator_test.go +++ b/test/e2e/case38_install_operator_test.go @@ -29,6 +29,11 @@ var _ = Describe("Test installing an operator from OperatorPolicy", Ordered, fun olmWaitTimeout = 45 ) + // checks that the policy has the proper compliance, that the relatedObjects of a given + // type exactly match the list given (no extras or omissions), that the condition is present, + // and that an event was emitted that matches the given snippet. + // It initially checks these in an Eventually, so they don't have to be true yet, + // but it follows up with a Consistently, so they do need to be the "final" state. check := func( polName string, wantNonCompliant bool, From e4f27263ebfe4946857a5861795278c927656bce Mon Sep 17 00:00:00 2001 From: Justin Kulikauskas Date: Fri, 29 Mar 2024 14:44:21 -0400 Subject: [PATCH 2/4] Add CRD reporting to OperatorPolicy Also adjusts CSV reporting to handle when the CSV exists but the subscription does not, by using a label that OLM adds to these objects. The authorino operator was chosen because it has more than one CRD, but not too many to make a concise test. Signed-off-by: Justin Kulikauskas --- controllers/operatorpolicy_controller.go | 101 ++++++++++++++---- controllers/operatorpolicy_status.go | 46 ++++++++ test/e2e/case38_install_operator_test.go | 101 +++++++++++++++++- .../operator-policy-authorino.yaml | 23 ++++ 4 files changed, 250 insertions(+), 21 deletions(-) create mode 100644 test/resources/case38_operator_install/operator-policy-authorino.yaml diff --git a/controllers/operatorpolicy_controller.go b/controllers/operatorpolicy_controller.go index ae8c7eac..ca5657da 100644 --- a/controllers/operatorpolicy_controller.go +++ b/controllers/operatorpolicy_controller.go @@ -65,6 +65,11 @@ var ( Version: "v1alpha1", Kind: "ClusterServiceVersion", } + customResourceDefinitionGVK = schema.GroupVersionKind{ + Group: "apiextensions.k8s.io", + Version: "v1", + Kind: "CustomResourceDefinition", + } deploymentGVK = schema.GroupVersionKind{ Group: "apps", Version: "v1", @@ -251,6 +256,16 @@ func (r *OperatorPolicyReconciler) handleResources(ctx context.Context, policy * return earlyComplianceEvents, condChanged, err } + earlyConds, changed, err = r.handleCRDs(ctx, policy, subscription) + earlyComplianceEvents = append(earlyComplianceEvents, earlyConds...) + condChanged = condChanged || changed + + if err != nil { + OpLog.Error(err, "Error handling CustomResourceDefinitions") + + return earlyComplianceEvents, condChanged, err + } + changed, err = r.handleDeployment(ctx, policy, csv) condChanged = condChanged || changed @@ -940,37 +955,38 @@ func (r *OperatorPolicyReconciler) handleCSV( } watcher := opPolIdentifier(policy.Namespace, policy.Name) + selector := subLabelSelector(sub) - // case where subscription status has not been populated yet - if sub.Status.InstalledCSV == "" { - return nil, updateStatus(policy, noCSVCond, noExistingCSVObj), nil + csvList, err := r.DynamicWatcher.List(watcher, clusterServiceVersionGVK, sub.Namespace, selector) + if err != nil { + return nil, false, fmt.Errorf("error listing CSVs: %w", err) } - // Get the CSV related to the object - foundCSV, err := r.DynamicWatcher.Get(watcher, clusterServiceVersionGVK, sub.Namespace, - sub.Status.InstalledCSV) - if err != nil { - return nil, false, err + var foundCSV *operatorv1alpha1.ClusterServiceVersion + + for _, csv := range csvList { + if csv.GetName() == sub.Status.InstalledCSV { + matchedCSV := operatorv1alpha1.ClusterServiceVersion{} + + err = runtime.DefaultUnstructuredConverter.FromUnstructured(csv.UnstructuredContent(), &matchedCSV) + if err != nil { + return nil, false, err + } + + foundCSV = &matchedCSV + } } + // CSV has not yet been created by OLM if foundCSV == nil { changed := updateStatus(policy, missingWantedCond("ClusterServiceVersion"), missingCSVObj(sub.Name, sub.Namespace)) - return nil, changed, nil - } - - // Check CSV most recent condition - unstructured := foundCSV.UnstructuredContent() - var csv operatorv1alpha1.ClusterServiceVersion - - err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructured, &csv) - if err != nil { - return nil, false, err + return foundCSV, changed, nil } - return &csv, updateStatus(policy, buildCSVCond(&csv), existingCSVObj(&csv)), nil + return foundCSV, updateStatus(policy, buildCSVCond(foundCSV), existingCSVObj(foundCSV)), nil } func (r *OperatorPolicyReconciler) handleDeployment( @@ -1029,6 +1045,36 @@ func (r *OperatorPolicyReconciler) handleDeployment( return updateStatus(policy, buildDeploymentCond(depNum > 0, unavailableDeployments), relatedObjects...), nil } +func (r *OperatorPolicyReconciler) handleCRDs( + _ context.Context, + policy *policyv1beta1.OperatorPolicy, + sub *operatorv1alpha1.Subscription, +) ([]metav1.Condition, bool, error) { + if sub == nil { + return nil, updateStatus(policy, noCRDCond, noExistingCRDObj), nil + } + + watcher := opPolIdentifier(policy.Namespace, policy.Name) + selector := subLabelSelector(sub) + + crdList, err := r.DynamicWatcher.List(watcher, customResourceDefinitionGVK, sub.Namespace, selector) + if err != nil { + return nil, false, fmt.Errorf("error listing CRDs: %w", err) + } + + if len(crdList) == 0 { + return nil, updateStatus(policy, noCRDCond, noExistingCRDObj), nil + } + + relatedCRDs := make([]policyv1.RelatedObject, len(crdList)) + + for i := range crdList { + relatedCRDs[i] = matchedObj(&crdList[i]) + } + + return nil, updateStatus(policy, crdFoundCond, relatedCRDs...), nil +} + func (r *OperatorPolicyReconciler) handleCatalogSource( policy *policyv1beta1.OperatorPolicy, subscription *operatorv1alpha1.Subscription, @@ -1136,3 +1182,20 @@ func (r *OperatorPolicyReconciler) mergeObjects( return updateNeeded, false, nil } + +// subLabelSelector returns a selector that matches a label that OLM adds to resources +// that are related to a Subscription. It can be used to find those resources even +// after the Subscription or CSV is deleted. +func subLabelSelector(sub *operatorv1alpha1.Subscription) labels.Selector { + sel, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{{ + Key: fmt.Sprintf("operators.coreos.com/%v.%v", sub.Name, sub.Namespace), + Operator: metav1.LabelSelectorOpExists, + }}, + }) + if err != nil { + panic(err) + } + + return sel +} diff --git a/controllers/operatorpolicy_status.go b/controllers/operatorpolicy_status.go index 0ae9c650..67974868 100644 --- a/controllers/operatorpolicy_status.go +++ b/controllers/operatorpolicy_status.go @@ -233,6 +233,18 @@ func calculateComplianceCondition(policy *policyv1beta1.OperatorPolicy) metav1.C } } + idx, cond = policy.Status.GetCondition(crdConditionType) + if idx == -1 { + messages = append(messages, "the status of the CustomResourceDefinitions is unknown") + foundNonCompliant = true + } else { + messages = append(messages, cond.Message) + + if cond.Status != metav1.ConditionTrue { + foundNonCompliant = true + } + } + idx, cond = policy.Status.GetCondition(deploymentConditionType) if idx == -1 { messages = append(messages, "the status of the Deployments are unknown") @@ -354,6 +366,7 @@ const ( opGroupConditionType = "OperatorGroupCompliant" subConditionType = "SubscriptionCompliant" csvConditionType = "ClusterServiceVersionCompliant" + crdConditionType = "CustomResourceDefinitionCompliant" deploymentConditionType = "DeploymentCompliant" catalogSrcConditionType = "CatalogSourcesUnhealthy" installPlanConditionType = "InstallPlanCompliant" @@ -369,6 +382,8 @@ func condType(kind string) string { return installPlanConditionType case "ClusterServiceVersion": return csvConditionType + case "CustomResourceDefinition": + return crdConditionType case "Deployment": return deploymentConditionType case "CatalogSource": @@ -618,6 +633,22 @@ var noCSVCond = metav1.Condition{ Message: "A relevant installed ClusterServiceVersion could not be found", } +// noCRDCond is a Compliant condition for when no CRDs are found +var noCRDCond = metav1.Condition{ + Type: crdConditionType, + Status: metav1.ConditionTrue, + Reason: "RelevantCRDNotFound", + Message: "No CRDs were found for the operator", +} + +// crdFoundCond is a Compliant condition for when CRDs are found +var crdFoundCond = metav1.Condition{ + Type: crdConditionType, + Status: metav1.ConditionTrue, + Reason: "RelevantCRDFound", + Message: "There are CRDs present for the operator", +} + // buildDeploymentCond creates a Condition for deployments. If any are not at their // minimum availability, the condition will be NonCompliant, and the message will // list the unavailable deployments. @@ -889,6 +920,21 @@ var noExistingCSVObj = policyv1.RelatedObject{ Reason: "No relevant ClusterServiceVersion found", } +// noExistingCRDObj is a Compliant RelatedObject for CustomResourceDefinitions, +// with Reason 'No relevant CustomResourceDefinitions found'. It is considered +// compliant because not all operators will have CRDs. +var noExistingCRDObj = policyv1.RelatedObject{ + Object: policyv1.ObjectResource{ + Kind: customResourceDefinitionGVK.Kind, + APIVersion: customResourceDefinitionGVK.GroupVersion().String(), + Metadata: policyv1.ObjectMetadata{ + Name: "-", + }, + }, + Compliant: string(policyv1.Compliant), + Reason: "No relevant CustomResourceDefinitions found", +} + // missingDeploymentObj returns a NonCompliant RelatedObject for a Deployment, // with Reason 'Resource not found but should exist' func missingDeploymentObj(name string, namespace string) policyv1.RelatedObject { diff --git a/test/e2e/case38_install_operator_test.go b/test/e2e/case38_install_operator_test.go index 0f1cab87..396c6005 100644 --- a/test/e2e/case38_install_operator_test.go +++ b/test/e2e/case38_install_operator_test.go @@ -43,11 +43,11 @@ var _ = Describe("Test installing an operator from OperatorPolicy", Ordered, fun ) { var debugMessage string - defer func() { + DeferCleanup(func() { if CurrentSpecReport().Failed() { GinkgoWriter.Println(debugMessage) } - }() + }) checkFunc := func(g Gomega) { GinkgoHelper() @@ -1200,6 +1200,103 @@ var _ = Describe("Test installing an operator from OperatorPolicy", Ordered, fun ) }) }) + Describe("Testing CustomResourceDefinition reporting", Ordered, func() { + const ( + opPolYAML = "../resources/case38_operator_install/operator-policy-authorino.yaml" + opPolName = "oppol-authorino" + ) + BeforeAll(func() { + utils.Kubectl("delete", "crd", "--selector=olm.managed=true") + utils.Kubectl("create", "ns", opPolTestNS) + DeferCleanup(func() { + utils.Kubectl("delete", "ns", opPolTestNS) + }) + + createObjWithParent(parentPolicyYAML, parentPolicyName, + opPolYAML, opPolTestNS, gvrPolicy, gvrOperatorPolicy) + }) + + It("Should initially not report on CRDs because they won't exist yet", func(ctx SpecContext) { + check( + opPolName, + false, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "CustomResourceDefinition", + APIVersion: "apiextensions.k8s.io/v1", + Metadata: policyv1.ObjectMetadata{ + Name: "-", + }, + }, + Compliant: "Compliant", + Reason: "No relevant CustomResourceDefinitions found", + }}, + metav1.Condition{ + Type: "CustomResourceDefinitionCompliant", + Status: metav1.ConditionTrue, + Reason: "RelevantCRDNotFound", + Message: "No CRDs were found for the operator", + }, + "No CRDs were found for the operator", + ) + }) + + It("Should generate conditions and relatedobjects of CRD", func(ctx SpecContext) { + utils.Kubectl("patch", "operatorpolicy", opPolName, "-n", opPolTestNS, "--type=json", "-p", + `[{"op": "replace", "path": "/spec/remediationAction", "value": "enforce"}]`) + 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{}) + + 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")) + + check( + opPolName, + false, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "CustomResourceDefinition", + APIVersion: "apiextensions.k8s.io/v1", + Metadata: policyv1.ObjectMetadata{ + Name: "authconfigs.authorino.kuadrant.io", + }, + }, + Compliant: "Compliant", + Reason: "Resource found as expected", + }, { + Object: policyv1.ObjectResource{ + Kind: "CustomResourceDefinition", + APIVersion: "apiextensions.k8s.io/v1", + Metadata: policyv1.ObjectMetadata{ + Name: "authorinos.operator.authorino.kuadrant.io", + }, + }, + Compliant: "Compliant", + Reason: "Resource found as expected", + }}, + metav1.Condition{ + Type: "CustomResourceDefinitionCompliant", + Status: metav1.ConditionTrue, + Reason: "RelevantCRDFound", + Message: "There are CRDs present for the operator", + }, + "There are CRDs present for the operator", + ) + }) + }) Describe("Testing OperatorPolicy validation messages", Ordered, func() { const ( opPolYAML = "../resources/case38_operator_install/operator-policy-validity-test.yaml" diff --git a/test/resources/case38_operator_install/operator-policy-authorino.yaml b/test/resources/case38_operator_install/operator-policy-authorino.yaml new file mode 100644 index 00000000..e075b3e4 --- /dev/null +++ b/test/resources/case38_operator_install/operator-policy-authorino.yaml @@ -0,0 +1,23 @@ +apiVersion: policy.open-cluster-management.io/v1beta1 +kind: OperatorPolicy +metadata: + name: oppol-authorino + annotations: + policy.open-cluster-management.io/parent-policy-compliance-db-id: "124" + policy.open-cluster-management.io/policy-compliance-db-id: "64" + ownerReferences: + - apiVersion: policy.open-cluster-management.io/v1 + kind: Policy + name: parent-policy + uid: 12345678-90ab-cdef-1234-567890abcdef # must be replaced before creation +spec: + remediationAction: inform + severity: medium + complianceType: musthave + subscription: + channel: stable + name: authorino-operator + namespace: operator-policy-testns + installPlanApproval: Automatic + source: operatorhubio-catalog + sourceNamespace: olm From fa5b2358a9c01bc3b09f3e189ef8b2b9dde030d5 Mon Sep 17 00:00:00 2001 From: Justin Kulikauskas Date: Thu, 4 Apr 2024 11:37:06 -0400 Subject: [PATCH 3/4] Relax event requirement for a flaky test When the subscription has a ConstraintsNotSatisfiable condition, the exact content of the message is not always consistent. We have added other things to sort the message better, but it looks like we missed this test which was still assuming a certain part was first. Signed-off-by: Justin Kulikauskas --- test/e2e/case38_install_operator_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/case38_install_operator_test.go b/test/e2e/case38_install_operator_test.go index 396c6005..b9e0cbf7 100644 --- a/test/e2e/case38_install_operator_test.go +++ b/test/e2e/case38_install_operator_test.go @@ -704,7 +704,7 @@ var _ = Describe("Test installing an operator from OperatorPolicy", Ordered, fun " in the catalog referenced by subscription project-quay-does-not-exist, subscription " + "project-quay-does-not-exist exists", }, - "constraints not satisfiable: no operators found in package project-quay-does-not-exist", + "constraints not satisfiable", ) // Check if the subscription is still compliant on the operator policy trying to install a valid operator. From 34f38bc99967e58508926b76cb3612e271b768ea Mon Sep 17 00:00:00 2001 From: Justin Kulikauskas Date: Mon, 8 Apr 2024 13:53:22 -0400 Subject: [PATCH 4/4] Implement mustnothave mode for OperatorPolicy Enforced mustnohave policies will delete things based on the new RemovalBehavior field. In inform mode, resources that would be removed by an enforced policy will be causes for NonCompliance. This gives control to users in order to prevent unintended side effects. Refs: - https://issues.redhat.com/browse/ACM-9287 Signed-off-by: Justin Kulikauskas --- api/v1beta1/operatorpolicy_types.go | 86 +- api/v1beta1/zz_generated.deepcopy.go | 1 + controllers/operatorpolicy_controller.go | 412 ++++++++- controllers/operatorpolicy_status.go | 125 ++- .../allowed-compliance-types.json | 2 +- ...luster-management.io_operatorpolicies.yaml | 52 ++ ...luster-management.io_operatorpolicies.yaml | 53 ++ test/e2e/case38_install_operator_test.go | 834 +++++++++++++++++- .../operator-policy-mustnothave.yaml | 32 + 9 files changed, 1549 insertions(+), 48 deletions(-) create mode 100644 test/resources/case38_operator_install/operator-policy-mustnothave.yaml diff --git a/api/v1beta1/operatorpolicy_types.go b/api/v1beta1/operatorpolicy_types.go index a77ef632..89fd69ca 100644 --- a/api/v1beta1/operatorpolicy_types.go +++ b/api/v1beta1/operatorpolicy_types.go @@ -4,6 +4,8 @@ package v1beta1 import ( + "strings" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -15,7 +17,6 @@ import ( type StatusConfigAction string // RemovalAction : Keep, Delete, or DeleteIfUnused -// +kubebuilder:validation:Enum=Keep;Delete;DeleteIfUnused type RemovalAction string const ( @@ -36,20 +37,75 @@ const ( DeleteIfUnused RemovalAction = "DeleteIfUnused" ) -// RemovalBehavior defines resource behavior when policy is removed +func (ra RemovalAction) IsKeep() bool { + return strings.EqualFold(string(ra), string(Keep)) +} + +func (ra RemovalAction) IsDelete() bool { + return strings.EqualFold(string(ra), string(Delete)) +} + +func (ra RemovalAction) IsDeleteIfUnused() bool { + return strings.EqualFold(string(ra), string(DeleteIfUnused)) +} + type RemovalBehavior struct { - // Kind OperatorGroup + //+kubebuilder:default=DeleteIfUnused + //+kubebuilder:validation:Enum=Keep;Delete;DeleteIfUnused + // Specifies whether to delete the OperatorGroup; defaults to 'DeleteIfUnused' which + // will only delete the OperatorGroup if there is not another Subscription using it. OperatorGroups RemovalAction `json:"operatorGroups,omitempty"` - // Kind Subscription + + //+kubebuilder:default=Delete + //+kubebuilder:validation:Enum=Keep;Delete + // Specifies whether to delete the Subscription; defaults to 'Delete' Subscriptions RemovalAction `json:"subscriptions,omitempty"` - // Kind ClusterServiceVersion + + //+kubebuilder:default=Delete + //+kubebuilder:validation:Enum=Keep;Delete + // Specifies whether to delete the ClusterServiceVersion; defaults to 'Delete' CSVs RemovalAction `json:"clusterServiceVersions,omitempty"` - // Kind InstallPlan + + //+kubebuilder:default=Keep + //+kubebuilder:validation:Enum=Keep;Delete + // Specifies whether to delete any InstallPlans associated with the operator; defaults + // to 'Keep' because those objects are only for history InstallPlan RemovalAction `json:"installPlans,omitempty"` - // Kind CustomResourceDefinitions + + //+kubebuilder:default=Keep + //+kubebuilder:validation:Enum=Keep;Delete + // Specifies whether to delete any CustomResourceDefinitions associated with the operator; + // defaults to 'Keep' because deleting them should be done deliberately CRDs RemovalAction `json:"customResourceDefinitions,omitempty"` - // Kind APIServiceDefinitions - APIServiceDefinitions RemovalAction `json:"apiServiceDefinitions,omitempty"` +} + +// ApplyDefaults ensures that unset fields in a RemovalBehavior behave as if they were +// set to the default values. In a cluster, kubernetes API validation should ensure that +// there are no unset values, and should apply the default values itself. +func (rb RemovalBehavior) ApplyDefaults() RemovalBehavior { + withDefaults := *rb.DeepCopy() + + if withDefaults.OperatorGroups == "" { + withDefaults.OperatorGroups = DeleteIfUnused + } + + if withDefaults.Subscriptions == "" { + withDefaults.Subscriptions = Delete + } + + if withDefaults.CSVs == "" { + withDefaults.CSVs = Delete + } + + if withDefaults.InstallPlan == "" { + withDefaults.InstallPlan = Keep + } + + if withDefaults.CRDs == "" { + withDefaults.CRDs = Keep + } + + return withDefaults } // StatusConfig defines how resource statuses affect the OperatorPolicy status and compliance @@ -64,7 +120,7 @@ type StatusConfig struct { type OperatorPolicySpec struct { Severity policyv1.Severity `json:"severity,omitempty"` // low, medium, high RemediationAction policyv1.RemediationAction `json:"remediationAction,omitempty"` // inform, enforce - ComplianceType policyv1.ComplianceType `json:"complianceType"` // musthave + ComplianceType policyv1.ComplianceType `json:"complianceType"` // musthave, mustnothave // Include the name, namespace, and any `spec` fields for the OperatorGroup. // For more info, see `kubectl explain operatorgroup.spec` or @@ -84,11 +140,11 @@ type OperatorPolicySpec struct { // in 'inform' mode, and which installPlans are approved when in 'enforce' mode Versions []policyv1.NonEmptyString `json:"versions,omitempty"` - // FUTURE - //nolint:dupword - // RemovalBehavior RemovalBehavior `json:"removalBehavior,omitempty"` - //nolint:dupword - // StatusConfig StatusConfig `json:"statusConfig,omitempty"` + //+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. + RemovalBehavior RemovalBehavior `json:"removalBehavior,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 0f8b5366..7a901a9a 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -86,6 +86,7 @@ func (in *OperatorPolicySpec) DeepCopyInto(out *OperatorPolicySpec) { *out = make([]v1.NonEmptyString, len(*in)) copy(*out, *in) } + out.RemovalBehavior = in.RemovalBehavior } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorPolicySpec. diff --git a/controllers/operatorpolicy_controller.go b/controllers/operatorpolicy_controller.go index ca5657da..e8098969 100644 --- a/controllers/operatorpolicy_controller.go +++ b/controllers/operatorpolicy_controller.go @@ -238,7 +238,8 @@ func (r *OperatorPolicyReconciler) handleResources(ctx context.Context, policy * return earlyComplianceEvents, condChanged, err } - changed, err = r.handleInstallPlan(ctx, policy, subscription) + earlyConds, changed, err = r.handleInstallPlan(ctx, policy, subscription) + earlyComplianceEvents = append(earlyComplianceEvents, earlyConds...) condChanged = condChanged || changed if err != nil { @@ -247,11 +248,12 @@ func (r *OperatorPolicyReconciler) handleResources(ctx context.Context, policy * return earlyComplianceEvents, condChanged, err } - csv, changed, err := r.handleCSV(policy, subscription) + csv, earlyConds, changed, err := r.handleCSV(ctx, policy, subscription) + earlyComplianceEvents = append(earlyComplianceEvents, earlyConds...) condChanged = condChanged || changed if err != nil { - OpLog.Error(err, "Error handling CSVs") + OpLog.Error(err, "Error handling ClusterServiceVersions") return earlyComplianceEvents, condChanged, err } @@ -287,12 +289,14 @@ func (r *OperatorPolicyReconciler) handleResources(ctx context.Context, policy * return earlyComplianceEvents, condChanged, nil } -// buildResources builds desired states for the Subscription and OperatorGroup, and +// buildResources builds 'musthave' desired states for the Subscription and OperatorGroup, and // checks if the policy's spec is valid. It returns: // - the built Subscription // - the built OperatorGroup // - whether the status has changed because of the validity condition // - an error if an API call failed +// +// The built objects can be used to find relevant objects for a 'mustnothave' policy. func (r *OperatorPolicyReconciler) buildResources(policy *policyv1beta1.OperatorPolicy) ( *operatorv1alpha1.Subscription, *operatorv1.OperatorGroup, bool, error, ) { @@ -474,6 +478,19 @@ func (r *OperatorPolicyReconciler) handleOpGroup( return nil, false, fmt.Errorf("error listing OperatorGroups: %w", err) } + if policy.Spec.ComplianceType.IsMustHave() { + return r.musthaveOpGroup(ctx, policy, desiredOpGroup, foundOpGroups) + } + + return r.mustnothaveOpGroup(ctx, policy, desiredOpGroup, foundOpGroups) +} + +func (r *OperatorPolicyReconciler) musthaveOpGroup( + ctx context.Context, + policy *policyv1beta1.OperatorPolicy, + desiredOpGroup *operatorv1.OperatorGroup, + foundOpGroups []unstructured.Unstructured, +) ([]metav1.Condition, bool, error) { switch len(foundOpGroups) { case 0: // Missing OperatorGroup: report NonCompliance @@ -489,7 +506,7 @@ func (r *OperatorPolicyReconciler) handleOpGroup( earlyConds = append(earlyConds, calculateComplianceCondition(policy)) } - err = r.Create(ctx, desiredOpGroup) + err := r.Create(ctx, desiredOpGroup) if err != nil { return nil, changed, fmt.Errorf("error creating the OperatorGroup: %w", err) } @@ -591,6 +608,89 @@ func (r *OperatorPolicyReconciler) handleOpGroup( } } +func (r *OperatorPolicyReconciler) mustnothaveOpGroup( + ctx context.Context, + policy *policyv1beta1.OperatorPolicy, + desiredOpGroup *operatorv1.OperatorGroup, + foundOpGroups []unstructured.Unstructured, +) ([]metav1.Condition, bool, error) { + if len(foundOpGroups) == 0 { + // Missing OperatorGroup: report Compliance + changed := updateStatus(policy, missingNotWantedCond("OperatorGroup"), missingNotWantedObj(desiredOpGroup)) + + return nil, changed, nil + } + + foundOpGroupName := "" + + for _, opGroup := range foundOpGroups { + emptyNameMatch := desiredOpGroup.Name == "" && opGroup.GetGenerateName() == desiredOpGroup.GenerateName + + if opGroup.GetName() == desiredOpGroup.Name || emptyNameMatch { + foundOpGroupName = opGroup.GetName() + + break + } + } + + if foundOpGroupName == "" { + // 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)) + + return nil, changed, nil + } + + // The found OperatorGroup matches what is *not* wanted by the policy. Report NonCompliance. + changed := updateStatus(policy, foundNotWantedCond("OperatorGroup"), foundNotWantedObj(desiredOpGroup)) + + if policy.Spec.RemediationAction.IsInform() { + 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 + } + } + + earlyConds := []metav1.Condition{} + + if changed { + earlyConds = append(earlyConds, calculateComplianceCondition(policy)) + } + + err := r.Delete(ctx, desiredOpGroup) + if err != nil { + return earlyConds, changed, fmt.Errorf("error deleting the OperatorGroup: %w", err) + } + + desiredOpGroup.SetGroupVersionKind(operatorGroupGVK) // Delete stripped this information + + updateStatus(policy, deletedCond("OperatorGroup"), deletedObj(desiredOpGroup)) + + return earlyConds, true, nil +} + func (r *OperatorPolicyReconciler) handleSubscription( ctx context.Context, policy *policyv1beta1.OperatorPolicy, desiredSub *operatorv1alpha1.Subscription, ) (*operatorv1alpha1.Subscription, []metav1.Condition, bool, error) { @@ -606,6 +706,19 @@ func (r *OperatorPolicyReconciler) handleSubscription( return nil, nil, false, fmt.Errorf("error getting the Subscription: %w", err) } + if policy.Spec.ComplianceType.IsMustHave() { + return r.musthaveSubscription(ctx, policy, desiredSub, foundSub) + } + + return r.mustnothaveSubscription(ctx, policy, desiredSub, foundSub) +} + +func (r *OperatorPolicyReconciler) musthaveSubscription( + ctx context.Context, + policy *policyv1beta1.OperatorPolicy, + desiredSub *operatorv1alpha1.Subscription, + foundSub *unstructured.Unstructured, +) (*operatorv1alpha1.Subscription, []metav1.Condition, bool, error) { if foundSub == nil { // Missing Subscription: report NonCompliance changed := updateStatus(policy, missingWantedCond("Subscription"), missingWantedObj(desiredSub)) @@ -717,6 +830,53 @@ func (r *OperatorPolicyReconciler) handleSubscription( return mergedSub, earlyConds, true, nil } +func (r *OperatorPolicyReconciler) mustnothaveSubscription( + ctx context.Context, + policy *policyv1beta1.OperatorPolicy, + desiredSub *operatorv1alpha1.Subscription, + foundUnstructSub *unstructured.Unstructured, +) (*operatorv1alpha1.Subscription, []metav1.Condition, bool, error) { + if foundUnstructSub == nil { + // Missing Subscription: report Compliance + changed := updateStatus(policy, missingNotWantedCond("Subscription"), missingNotWantedObj(desiredSub)) + + return desiredSub, nil, changed, nil + } + + foundSub := new(operatorv1alpha1.Subscription) + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(foundUnstructSub.Object, foundSub); err != nil { + return nil, nil, false, fmt.Errorf("error converting the retrieved Subscription to the go type: %w", err) + } + + if policy.Spec.RemovalBehavior.ApplyDefaults().Subscriptions.IsKeep() { + changed := updateStatus(policy, keptCond("Subscription"), leftoverObj(foundSub)) + + return foundSub, nil, changed, nil + } + + // Subscription found, not wanted: report NonCompliance. + changed := updateStatus(policy, foundNotWantedCond("Subscription"), foundNotWantedObj(foundSub)) + + if policy.Spec.RemediationAction.IsInform() { + return foundSub, nil, changed, nil + } + + earlyConds := []metav1.Condition{} + + if changed { + earlyConds = append(earlyConds, calculateComplianceCondition(policy)) + } + + err := r.Delete(ctx, foundUnstructSub) + if err != nil { + return foundSub, earlyConds, changed, fmt.Errorf("error deleting the Subscription: %w", err) + } + + updateStatus(policy, deletedCond("Subscription"), deletedObj(desiredSub)) + + return foundSub, earlyConds, true, nil +} + // messageIncludesSubscription checks if the ConstraintsNotSatisfiable message includes the input // subscription or package. Some examples that it catches: // https://github.com/operator-framework/operator-lifecycle-manager/blob/dc0c564f62d526bae0467d53f439e1c91a17ed8a/pkg/controller/registry/resolver/resolver.go#L257-L267 @@ -775,10 +935,10 @@ func constraintMessageMatch(policy *policyv1beta1.OperatorPolicy, cond *operator func (r *OperatorPolicyReconciler) handleInstallPlan( ctx context.Context, policy *policyv1beta1.OperatorPolicy, sub *operatorv1alpha1.Subscription, -) (bool, error) { +) ([]metav1.Condition, bool, error) { if sub == nil { // Note: existing related objects will not be removed by this status update - return updateStatus(policy, invalidCausingUnknownCond("InstallPlan")), nil + return nil, updateStatus(policy, invalidCausingUnknownCond("InstallPlan")), nil } watcher := opPolIdentifier(policy.Namespace, policy.Name) @@ -786,12 +946,20 @@ func (r *OperatorPolicyReconciler) handleInstallPlan( foundInstallPlans, err := r.DynamicWatcher.List( watcher, installPlanGVK, sub.Namespace, labels.Everything()) if err != nil { - return false, fmt.Errorf("error listing InstallPlans: %w", err) + return nil, false, fmt.Errorf("error listing InstallPlans: %w", err) } ownedInstallPlans := make([]unstructured.Unstructured, 0, len(foundInstallPlans)) + selector := subLabelSelector(sub) for _, installPlan := range foundInstallPlans { + // sometimes the OwnerReferences aren't correct, but the label should be + if selector.Matches(labels.Set(installPlan.GetLabels())) { + ownedInstallPlans = append(ownedInstallPlans, installPlan) + + break + } + for _, owner := range installPlan.GetOwnerReferences() { match := owner.Name == sub.Name && owner.Kind == subscriptionGVK.Kind && @@ -806,16 +974,32 @@ func (r *OperatorPolicyReconciler) handleInstallPlan( // InstallPlans are generally kept in order to provide a history of actions on the cluster, but // they can be deleted without impacting the installed operator. So, not finding any should not - // be considered a reason for NonCompliance. + // be considered a reason for NonCompliance, regardless of musthave or mustnothave. if len(ownedInstallPlans) == 0 { - return updateStatus(policy, noInstallPlansCond, noInstallPlansObj(sub.Namespace)), nil + return nil, updateStatus(policy, noInstallPlansCond, noInstallPlansObj(sub.Namespace)), nil } + if policy.Spec.ComplianceType.IsMustHave() { + changed, err := r.musthaveInstallPlan(ctx, policy, sub, ownedInstallPlans) + + return nil, changed, err + } + + return r.mustnothaveInstallPlan(ctx, policy, ownedInstallPlans) +} + +func (r *OperatorPolicyReconciler) musthaveInstallPlan( + ctx context.Context, + policy *policyv1beta1.OperatorPolicy, + sub *operatorv1alpha1.Subscription, + ownedInstallPlans []unstructured.Unstructured, +) (bool, error) { OpLog := ctrl.LoggerFrom(ctx) - relatedInstallPlans := make([]policyv1.RelatedObject, len(ownedInstallPlans)) + relatedInstallPlans := make([]policyv1.RelatedObject, 0, len(ownedInstallPlans)) ipsRequiringApproval := make([]unstructured.Unstructured, 0) anyInstalling := false currentPlanFailed := false + selector := subLabelSelector(sub) // Construct the relevant relatedObjects, and collect any that might be considered for approval for i, installPlan := range ownedInstallPlans { @@ -835,7 +1019,11 @@ func (r *OperatorPolicyReconciler) handleInstallPlan( // consider some special phases switch phase { case string(operatorv1alpha1.InstallPlanPhaseRequiresApproval): - ipsRequiringApproval = append(ipsRequiringApproval, installPlan) + // only consider InstallPlans with this label for approval - this label is supposed to + // indicate the "current" InstallPlan for this subscription. + if selector.Matches(labels.Set(installPlan.GetLabels())) { + ipsRequiringApproval = append(ipsRequiringApproval, installPlan) + } case string(operatorv1alpha1.InstallPlanPhaseInstalling): anyInstalling = true case string(operatorv1alpha1.InstallPlanFailed): @@ -846,7 +1034,7 @@ func (r *OperatorPolicyReconciler) handleInstallPlan( } } - relatedInstallPlans[i] = existingInstallPlanObj(&ownedInstallPlans[i], phase) + relatedInstallPlans = append(relatedInstallPlans, existingInstallPlanObj(&ownedInstallPlans[i], phase)) } if currentPlanFailed { @@ -861,9 +1049,9 @@ func (r *OperatorPolicyReconciler) handleInstallPlan( return updateStatus(policy, installPlansNoApprovals, relatedInstallPlans...), nil } - allUpgradeVersions := make([]string, len(ipsRequiringApproval)) + allUpgradeVersions := make([]string, 0, len(ipsRequiringApproval)) - for i, installPlan := range ipsRequiringApproval { + for _, installPlan := range ipsRequiringApproval { csvNames, ok, err := unstructured.NestedStringSlice(installPlan.Object, "spec", "clusterServiceVersionNames") if !ok && err == nil { @@ -877,7 +1065,7 @@ func (r *OperatorPolicyReconciler) handleInstallPlan( csvNames = []string{"unknown"} } - allUpgradeVersions[i] = fmt.Sprintf("%v", csvNames) + allUpgradeVersions = append(allUpgradeVersions, fmt.Sprintf("%v", csvNames)) } // Only report this status in `inform` mode, because otherwise it could easily oscillate between this and @@ -944,14 +1132,65 @@ func (r *OperatorPolicyReconciler) handleInstallPlan( return updateStatus(policy, installPlanApprovedCond(approvedVersion), relatedInstallPlans...), nil } +func (r *OperatorPolicyReconciler) mustnothaveInstallPlan( + ctx context.Context, + policy *policyv1beta1.OperatorPolicy, + ownedInstallPlans []unstructured.Unstructured, +) ([]metav1.Condition, bool, error) { + relatedInstallPlans := make([]policyv1.RelatedObject, 0, len(ownedInstallPlans)) + + if policy.Spec.RemovalBehavior.ApplyDefaults().InstallPlan.IsKeep() { + for i := range ownedInstallPlans { + relatedInstallPlans = append(relatedInstallPlans, leftoverObj(&ownedInstallPlans[i])) + } + + return nil, updateStatus(policy, keptCond("InstallPlan"), relatedInstallPlans...), nil + } + + for i := range ownedInstallPlans { + relatedInstallPlans = append(relatedInstallPlans, foundNotWantedObj(&ownedInstallPlans[i])) + } + + changed := updateStatus(policy, foundNotWantedCond("InstallPlan"), relatedInstallPlans...) + + if policy.Spec.RemediationAction.IsInform() { + return nil, changed, nil + } + + earlyConds := []metav1.Condition{} + + if changed { + earlyConds = append(earlyConds, calculateComplianceCondition(policy)) + } + + deletedInstallPlans := make([]policyv1.RelatedObject, 0, len(ownedInstallPlans)) + + for i := range ownedInstallPlans { + err := r.Delete(ctx, &ownedInstallPlans[i]) + if err != nil { + changed := updateStatus(policy, foundNotWantedCond("InstallPlan"), deletedInstallPlans...) + + return earlyConds, changed, fmt.Errorf("error deleting the InstallPlan: %w", err) + } + + ownedInstallPlans[i].SetGroupVersionKind(installPlanGVK) // Delete stripped this information + deletedInstallPlans = append(deletedInstallPlans, deletedObj(&ownedInstallPlans[i])) + } + + updateStatus(policy, deletedCond("InstallPlan"), deletedInstallPlans...) + + return earlyConds, true, nil +} + func (r *OperatorPolicyReconciler) handleCSV( + ctx context.Context, policy *policyv1beta1.OperatorPolicy, sub *operatorv1alpha1.Subscription, -) (*operatorv1alpha1.ClusterServiceVersion, bool, error) { +) (*operatorv1alpha1.ClusterServiceVersion, []metav1.Condition, bool, error) { // case where subscription is nil if sub == nil { // need to report lack of existing CSV - return nil, updateStatus(policy, noCSVCond, noExistingCSVObj), nil + return nil, nil, updateStatus(policy, noCSVCond, noExistingCSVObj), nil } watcher := opPolIdentifier(policy.Namespace, policy.Name) @@ -959,7 +1198,7 @@ func (r *OperatorPolicyReconciler) handleCSV( csvList, err := r.DynamicWatcher.List(watcher, clusterServiceVersionGVK, sub.Namespace, selector) if err != nil { - return nil, false, fmt.Errorf("error listing CSVs: %w", err) + return nil, nil, false, fmt.Errorf("error listing CSVs: %w", err) } var foundCSV *operatorv1alpha1.ClusterServiceVersion @@ -970,23 +1209,88 @@ func (r *OperatorPolicyReconciler) handleCSV( err = runtime.DefaultUnstructuredConverter.FromUnstructured(csv.UnstructuredContent(), &matchedCSV) if err != nil { - return nil, false, err + return nil, nil, false, err } foundCSV = &matchedCSV + + break } } + if policy.Spec.ComplianceType.IsMustNotHave() { + earlyConds, changed, err := r.mustnothaveCSV(ctx, policy, csvList, sub.Namespace) + + return foundCSV, earlyConds, changed, err + } // CSV has not yet been created by OLM if foundCSV == nil { changed := updateStatus(policy, missingWantedCond("ClusterServiceVersion"), missingCSVObj(sub.Name, sub.Namespace)) - return foundCSV, changed, nil + return foundCSV, nil, changed, nil + } + + return foundCSV, nil, updateStatus(policy, buildCSVCond(foundCSV), existingCSVObj(foundCSV)), nil +} + +func (r *OperatorPolicyReconciler) mustnothaveCSV( + ctx context.Context, + policy *policyv1beta1.OperatorPolicy, + csvList []unstructured.Unstructured, + namespace string, +) ([]metav1.Condition, bool, error) { + if len(csvList) == 0 { + changed := updateStatus(policy, missingNotWantedCond("ClusterServiceVersion"), + missingNotWantedCSVObj(namespace)) + + return nil, changed, nil + } + + relatedCSVs := make([]policyv1.RelatedObject, 0, len(csvList)) + + if policy.Spec.RemovalBehavior.ApplyDefaults().CSVs.IsKeep() { + for i := range csvList { + relatedCSVs = append(relatedCSVs, leftoverObj(&csvList[i])) + } + + return nil, updateStatus(policy, keptCond("ClusterServiceVersion"), relatedCSVs...), nil + } + + for i := range csvList { + relatedCSVs = append(relatedCSVs, foundNotWantedObj(&csvList[i])) + } + + changed := updateStatus(policy, foundNotWantedCond("ClusterServiceVersion"), relatedCSVs...) + + if policy.Spec.RemediationAction.IsInform() { + return nil, changed, nil + } + + earlyConds := []metav1.Condition{} + + if changed { + earlyConds = append(earlyConds, calculateComplianceCondition(policy)) + } + + deletedCSVs := make([]policyv1.RelatedObject, 0, len(csvList)) + + for i := range csvList { + err := r.Delete(ctx, &csvList[i]) + if err != nil { + changed := updateStatus(policy, foundNotWantedCond("ClusterServiceVersion"), deletedCSVs...) + + return earlyConds, changed, fmt.Errorf("error deleting ClusterServiceVersion: %w", err) + } + + csvList[i].SetGroupVersionKind(clusterServiceVersionGVK) + deletedCSVs = append(deletedCSVs, deletedObj(&csvList[i])) } - return foundCSV, updateStatus(policy, buildCSVCond(foundCSV), existingCSVObj(foundCSV)), nil + updateStatus(policy, deletedCond("ClusterServiceVersion"), deletedCSVs...) + + return earlyConds, true, nil } func (r *OperatorPolicyReconciler) handleDeployment( @@ -994,6 +1298,10 @@ func (r *OperatorPolicyReconciler) handleDeployment( policy *policyv1beta1.OperatorPolicy, csv *operatorv1alpha1.ClusterServiceVersion, ) (bool, error) { + if policy.Spec.ComplianceType.IsMustNotHave() { + return updateStatus(policy, notApplicableCond("Deployment")), nil + } + // case where csv is nil if csv == nil { // need to report lack of existing Deployments @@ -1046,7 +1354,7 @@ func (r *OperatorPolicyReconciler) handleDeployment( } func (r *OperatorPolicyReconciler) handleCRDs( - _ context.Context, + ctx context.Context, policy *policyv1beta1.OperatorPolicy, sub *operatorv1alpha1.Subscription, ) ([]metav1.Condition, bool, error) { @@ -1062,23 +1370,75 @@ func (r *OperatorPolicyReconciler) handleCRDs( return nil, false, fmt.Errorf("error listing CRDs: %w", err) } + // Same condition for musthave and mustnothave if len(crdList) == 0 { return nil, updateStatus(policy, noCRDCond, noExistingCRDObj), nil } - relatedCRDs := make([]policyv1.RelatedObject, len(crdList)) + relatedCRDs := make([]policyv1.RelatedObject, 0, len(crdList)) + + if policy.Spec.ComplianceType.IsMustHave() { + for i := range crdList { + relatedCRDs = append(relatedCRDs, matchedObj(&crdList[i])) + } + + return nil, updateStatus(policy, crdFoundCond, relatedCRDs...), nil + } + + if policy.Spec.RemovalBehavior.ApplyDefaults().CRDs.IsKeep() { + for i := range crdList { + relatedCRDs = append(relatedCRDs, leftoverObj(&crdList[i])) + } + + return nil, updateStatus(policy, keptCond("CustomResourceDefinition"), relatedCRDs...), nil + } + + for i := range crdList { + relatedCRDs = append(relatedCRDs, foundNotWantedObj(&crdList[i])) + } + + changed := updateStatus(policy, foundNotWantedCond("CustomResourceDefinition"), relatedCRDs...) + + if policy.Spec.RemediationAction.IsInform() { + return nil, changed, nil + } + + earlyConds := []metav1.Condition{} + + if changed { + earlyConds = append(earlyConds, calculateComplianceCondition(policy)) + } + + deletedCRDs := make([]policyv1.RelatedObject, 0, len(crdList)) for i := range crdList { - relatedCRDs[i] = matchedObj(&crdList[i]) + err := r.Delete(ctx, &crdList[i]) + if err != nil { + changed := updateStatus(policy, foundNotWantedCond("CustomResourceDefinition"), deletedCRDs...) + + return earlyConds, changed, fmt.Errorf("error deleting the CRD: %w", err) + } + + crdList[i].SetGroupVersionKind(customResourceDefinitionGVK) + deletedCRDs = append(deletedCRDs, deletedObj(&crdList[i])) } - return nil, updateStatus(policy, crdFoundCond, relatedCRDs...), nil + updateStatus(policy, deletedCond("CustomResourceDefinition"), deletedCRDs...) + + return earlyConds, true, nil } func (r *OperatorPolicyReconciler) handleCatalogSource( policy *policyv1beta1.OperatorPolicy, subscription *operatorv1alpha1.Subscription, ) (bool, error) { + if policy.Spec.ComplianceType.IsMustNotHave() { + cond := notApplicableCond("CatalogSource") + cond.Status = metav1.ConditionFalse // CatalogSource condition has the opposite polarity + + return updateStatus(policy, cond), nil + } + watcher := opPolIdentifier(policy.Namespace, policy.Name) if subscription == nil { diff --git a/controllers/operatorpolicy_status.go b/controllers/operatorpolicy_status.go index 67974868..3b2a6b6e 100644 --- a/controllers/operatorpolicy_status.go +++ b/controllers/operatorpolicy_status.go @@ -415,7 +415,29 @@ func missingWantedCond(kind string) metav1.Condition { } } -// createdCond returns a Compliant condition, with a Reason like'____Created', +// missingNotWantedCond returns a Compliant condition with a Reason like '____NotPresent' +// and a Message like 'the ____ is not present' +func missingNotWantedCond(kind string) metav1.Condition { + return metav1.Condition{ + Type: condType(kind), + Status: metav1.ConditionTrue, + Reason: kind + "NotPresent", + Message: "the " + kind + " is not present", + } +} + +// foundNotWantedCond returns a NonCompliant condition with a Reason like '____Present' +// and a Message like 'the ____ is present' +func foundNotWantedCond(kind string) metav1.Condition { + return metav1.Condition{ + Type: condType(kind), + Status: metav1.ConditionFalse, + Reason: kind + "Present", + Message: "the " + kind + " is present", + } +} + +// createdCond returns a Compliant condition, with a Reason like '____Created', // and a Message like 'the ____ required by the policy was created' func createdCond(kind string) metav1.Condition { return metav1.Condition{ @@ -426,6 +448,28 @@ func createdCond(kind string) metav1.Condition { } } +// deletedCond returns a Compliant condition, with a Reason like '____Deleted', +// and a Message like 'the ____ was deleted' +func deletedCond(kind string) metav1.Condition { + return metav1.Condition{ + Type: condType(kind), + Status: metav1.ConditionTrue, + Reason: kind + "Deleted", + Message: "the " + kind + " was deleted", + } +} + +// keptCond returns a Compliant condition, with a Reason like '____Kept', +// and a Message like 'the policy specifies to keep the ____' +func keptCond(kind string) metav1.Condition { + return metav1.Condition{ + Type: condType(kind), + Status: metav1.ConditionTrue, + Reason: kind + "Kept", + Message: "the policy specifies to keep the " + kind, + } +} + // matchesCond returns a Compliant condition, with a Reason like'____Matches', // and a Message like 'the ____ matches what is required by the policy' func matchesCond(kind string) metav1.Condition { @@ -514,6 +558,17 @@ func subResFailedCond(subFailedCond operatorv1alpha1.SubscriptionCondition) meta return cond } +// notApplicableCond returns a Compliant condition, with a Reason like '____NotApplicable', +// and a Message like 'MustNotHave policies ignore kind ____' +func notApplicableCond(kind string) metav1.Condition { + return metav1.Condition{ + Type: condType(kind), + Status: metav1.ConditionTrue, + Reason: kind + "NotApplicable", + Message: "MustNotHave policies ignore kind " + kind, + } +} + // opGroupPreexistingCond is a Compliant condition with Reason 'PreexistingOperatorGroupFound', // and Message 'the policy does not specify an OperatorGroup but one already exists in the // namespace - assuming that OperatorGroup is correct' @@ -739,6 +794,27 @@ func missingWantedObj(obj client.Object) policyv1.RelatedObject { } } +// missingNotWantedObj returns a Compliant RelatedObject with reason = 'Resource not found as expected' +func missingNotWantedObj(obj client.Object) policyv1.RelatedObject { + return policyv1.RelatedObject{ + Object: policyv1.ObjectResourceFromObj(obj), + Compliant: string(policyv1.Compliant), + Reason: reasonWantNotFoundDNE, + } +} + +// foundNotWantedObj returns a NonCompliant RelatedObject with reason = 'Resource found but should not exist' +func foundNotWantedObj(obj client.Object) policyv1.RelatedObject { + return policyv1.RelatedObject{ + Object: policyv1.ObjectResourceFromObj(obj), + Compliant: string(policyv1.NonCompliant), + Reason: reasonWantNotFoundExists, + Properties: &policyv1.ObjectProperties{ + UID: string(obj.GetUID()), + }, + } +} + // createdObj returns a Compliant RelatedObject with reason = 'K8s creation success' func createdObj(obj client.Object) policyv1.RelatedObject { created := true @@ -754,6 +830,15 @@ func createdObj(obj client.Object) policyv1.RelatedObject { } } +// deletedObj returns a Compliant RelatedObject with reason = 'K8s deletion success' +func deletedObj(obj client.Object) policyv1.RelatedObject { + return policyv1.RelatedObject{ + Object: policyv1.ObjectResourceFromObj(obj), + Compliant: string(policyv1.Compliant), + Reason: reasonDeleteSuccess, + } +} + // matchedObj returns a Compliant RelatedObject with reason = 'Resource found as expected' func matchedObj(obj client.Object) policyv1.RelatedObject { return policyv1.RelatedObject{ @@ -803,21 +888,36 @@ func nonCompObj(obj client.Object, reason string) policyv1.RelatedObject { } } +// leftoverObj returns a RelatedObject for an object related to a +// mustnothave policy which specifies to keep this kind of object. +// The object does not have a compliance associated with it. +func leftoverObj(obj client.Object) policyv1.RelatedObject { + kind := obj.GetObjectKind().GroupVersionKind().Kind + + return policyv1.RelatedObject{ + Object: policyv1.ObjectResourceFromObj(obj), + Properties: &policyv1.ObjectProperties{ + UID: string(obj.GetUID()), + }, + Reason: "The " + kind + " is attached to a mustnothave policy, but does not need to be removed", + } +} + // opGroupTooManyObjs returns a list of NonCompliant RelatedObjects, each with // reason = 'There is more than one OperatorGroup in this namespace' func opGroupTooManyObjs(opGroups []unstructured.Unstructured) []policyv1.RelatedObject { - objs := make([]policyv1.RelatedObject, len(opGroups)) + objs := make([]policyv1.RelatedObject, 0, len(opGroups)) for i, opGroup := range opGroups { opGroup := opGroup - objs[i] = policyv1.RelatedObject{ + objs = append(objs, policyv1.RelatedObject{ Object: policyv1.ObjectResourceFromObj(&opGroups[i]), Compliant: string(policyv1.NonCompliant), Reason: "There is more than one OperatorGroup in this namespace", Properties: &policyv1.ObjectProperties{ UID: string(opGroup.GetUID()), }, - } + }) } return objs @@ -888,6 +988,23 @@ func missingCSVObj(name string, namespace string) policyv1.RelatedObject { } } +// missingNotWantedCSVObj returns a Compliant RelatedObject for the ClusterServiceVersion, +// with Reason 'Resource not found as expected' +func missingNotWantedCSVObj(namespace string) policyv1.RelatedObject { + return policyv1.RelatedObject{ + Object: policyv1.ObjectResource{ + Kind: clusterServiceVersionGVK.Kind, + APIVersion: clusterServiceVersionGVK.GroupVersion().String(), + Metadata: policyv1.ObjectMetadata{ + Name: "-", + Namespace: namespace, + }, + }, + Compliant: string(policyv1.Compliant), + Reason: reasonWantNotFoundDNE, + } +} + // existingCSVObj returns a RelatedObject for the ClusterServiceVersion, with a // Reason that reflects the CSV's status, and will only be Compliant if the CSV // is in the Succeeded phase. diff --git a/deploy/crds/kustomize_operatorpolicy/allowed-compliance-types.json b/deploy/crds/kustomize_operatorpolicy/allowed-compliance-types.json index 55ffc812..547d3ef9 100644 --- a/deploy/crds/kustomize_operatorpolicy/allowed-compliance-types.json +++ b/deploy/crds/kustomize_operatorpolicy/allowed-compliance-types.json @@ -2,6 +2,6 @@ { "op":"replace", "path":"/spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/complianceType/enum", - "value": ["musthave"] + "value": ["musthave", "mustnothave"] } ] 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 be7e48bc..b293df9f 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 @@ -68,6 +68,58 @@ spec: - Enforce - enforce type: string + removalBehavior: + default: {} + description: |- + 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. + properties: + clusterServiceVersions: + default: Delete + description: Specifies whether to delete the ClusterServiceVersion; + defaults to 'Delete' + enum: + - Keep + - Delete + type: string + customResourceDefinitions: + default: Keep + description: |- + Specifies whether to delete any CustomResourceDefinitions associated with the operator; + defaults to 'Keep' because deleting them should be done deliberately + enum: + - Keep + - Delete + type: string + installPlans: + default: Keep + description: |- + Specifies whether to delete any InstallPlans associated with the operator; defaults + to 'Keep' because those objects are only for history + enum: + - Keep + - Delete + type: string + operatorGroups: + 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. + enum: + - Keep + - Delete + - DeleteIfUnused + type: string + subscriptions: + default: Delete + description: Specifies whether to delete the Subscription; defaults + to 'Delete' + enum: + - Keep + - Delete + type: string + type: object severity: description: 'Severity : low, medium, high, or critical' enum: diff --git a/deploy/crds/policy.open-cluster-management.io_operatorpolicies.yaml b/deploy/crds/policy.open-cluster-management.io_operatorpolicies.yaml index 05f74803..30a22aba 100644 --- a/deploy/crds/policy.open-cluster-management.io_operatorpolicies.yaml +++ b/deploy/crds/policy.open-cluster-management.io_operatorpolicies.yaml @@ -46,6 +46,7 @@ spec: have a given resource enum: - musthave + - mustnothave type: string operatorGroup: description: |- @@ -62,6 +63,58 @@ spec: - Enforce - enforce type: string + removalBehavior: + default: {} + description: |- + 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. + properties: + clusterServiceVersions: + default: Delete + description: Specifies whether to delete the ClusterServiceVersion; + defaults to 'Delete' + enum: + - Keep + - Delete + type: string + customResourceDefinitions: + default: Keep + description: |- + Specifies whether to delete any CustomResourceDefinitions associated with the operator; + defaults to 'Keep' because deleting them should be done deliberately + enum: + - Keep + - Delete + type: string + installPlans: + default: Keep + description: |- + Specifies whether to delete any InstallPlans associated with the operator; defaults + to 'Keep' because those objects are only for history + enum: + - Keep + - Delete + type: string + operatorGroups: + 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. + enum: + - Keep + - Delete + - DeleteIfUnused + type: string + subscriptions: + default: Delete + description: Specifies whether to delete the Subscription; defaults + to 'Delete' + enum: + - Keep + - Delete + type: string + type: object severity: description: 'Severity : low, medium, high, or critical' enum: diff --git a/test/e2e/case38_install_operator_test.go b/test/e2e/case38_install_operator_test.go index b9e0cbf7..08c5c58b 100644 --- a/test/e2e/case38_install_operator_test.go +++ b/test/e2e/case38_install_operator_test.go @@ -19,14 +19,14 @@ import ( "open-cluster-management.io/config-policy-controller/test/utils" ) -var _ = Describe("Test installing an operator from OperatorPolicy", Ordered, func() { +var _ = Describe("Testing OperatorPolicy", Ordered, func() { const ( opPolTestNS = "operator-policy-testns" parentPolicyYAML = "../resources/case38_operator_install/parent-policy.yaml" parentPolicyName = "parent-policy" eventuallyTimeout = 10 consistentlyDuration = 5 - olmWaitTimeout = 45 + olmWaitTimeout = 60 ) // checks that the policy has the proper compliance, that the relatedObjects of a given @@ -1433,4 +1433,834 @@ var _ = Describe("Test installing an operator from OperatorPolicy", Ordered, fun ) }) }) + Describe("Testing general OperatorPolicy mustnothave behavior", Ordered, func() { + const ( + opPolYAML = "../resources/case38_operator_install/operator-policy-mustnothave.yaml" + opPolName = "oppol-mustnothave" + subName = "project-quay" + ) + + BeforeAll(func() { + utils.Kubectl("create", "ns", opPolTestNS) + utils.Kubectl("delete", "crd", "--selector=olm.managed=true") + DeferCleanup(func() { + utils.Kubectl("delete", "ns", opPolTestNS) + }) + + createObjWithParent(parentPolicyYAML, parentPolicyName, + opPolYAML, opPolTestNS, gvrPolicy, gvrOperatorPolicy) + }) + + It("Should be Compliant and report all the things are correctly missing", func() { + check( + opPolName, + false, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "OperatorGroup", + APIVersion: "operators.coreos.com/v1", + Metadata: policyv1.ObjectMetadata{ + Namespace: opPolTestNS, + Name: "-", + }, + }, + Compliant: "Compliant", + Reason: "Resource not found as expected", + }}, + metav1.Condition{ + Type: "OperatorGroupCompliant", + Status: metav1.ConditionTrue, + Reason: "OperatorGroupNotPresent", + Message: "the OperatorGroup is not present", + }, + `the OperatorGroup is not present`, + ) + check( + opPolName, + false, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "Subscription", + APIVersion: "operators.coreos.com/v1alpha1", + Metadata: policyv1.ObjectMetadata{ + Namespace: opPolTestNS, + Name: "project-quay", + }, + }, + Compliant: "Compliant", + Reason: "Resource not found as expected", + }}, + metav1.Condition{ + Type: "SubscriptionCompliant", + Status: metav1.ConditionTrue, + Reason: "SubscriptionNotPresent", + Message: "the Subscription is not present", + }, + `the Subscription is not present`, + ) + check( + opPolName, + false, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "InstallPlan", + APIVersion: "operators.coreos.com/v1alpha1", + Metadata: policyv1.ObjectMetadata{ + Namespace: opPolTestNS, + Name: "-", + }, + }, + Compliant: "Compliant", + Reason: "There are no relevant InstallPlans in this namespace", + }}, + metav1.Condition{ + Type: "InstallPlanCompliant", + Status: metav1.ConditionTrue, + Reason: "NoInstallPlansFound", + Message: "there are no relevant InstallPlans in the namespace", + }, + `there are no relevant InstallPlans in the namespace`, + ) + check( + opPolName, + false, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "ClusterServiceVersion", + APIVersion: "operators.coreos.com/v1alpha1", + Metadata: policyv1.ObjectMetadata{ + Namespace: opPolTestNS, + Name: "-", + }, + }, + Compliant: "Compliant", + Reason: "Resource not found as expected", + }}, + metav1.Condition{ + Type: "ClusterServiceVersionCompliant", + Status: metav1.ConditionTrue, + Reason: "ClusterServiceVersionNotPresent", + Message: "the ClusterServiceVersion is not present", + }, + `the ClusterServiceVersion is not present`, + ) + check( + opPolName, + false, + []policyv1.RelatedObject{}, + metav1.Condition{ + Type: "DeploymentCompliant", + Status: metav1.ConditionTrue, + Reason: "DeploymentNotApplicable", + Message: "MustNotHave policies ignore kind Deployment", + }, + `MustNotHave policies ignore kind Deployment`, + ) + check( + opPolName, + false, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "CustomResourceDefinition", + APIVersion: "apiextensions.k8s.io/v1", + Metadata: policyv1.ObjectMetadata{ + Name: "-", + }, + }, + Compliant: "Compliant", + Reason: "No relevant CustomResourceDefinitions found", + }}, + metav1.Condition{ + Type: "CustomResourceDefinitionCompliant", + Status: metav1.ConditionTrue, + Reason: "RelevantCRDNotFound", + Message: "No CRDs were found for the operator", + }, + `No CRDs were found for the operator`, + ) + check( + opPolName, + false, + []policyv1.RelatedObject{}, + metav1.Condition{ + Type: "CatalogSourcesUnhealthy", + Status: metav1.ConditionFalse, + Reason: "CatalogSourceNotApplicable", + Message: "MustNotHave policies ignore kind CatalogSource", + }, + `MustNotHave policies ignore kind CatalogSource`, + ) + + // The `check` function doesn't check that it is compliant, only that each piece is compliant + pol := utils.GetWithTimeout(clientManagedDynamic, gvrOperatorPolicy, opPolName, + opPolTestNS, true, eventuallyTimeout) + compliance, found, err := unstructured.NestedString(pol.Object, "status", "compliant") + Expect(err).NotTo(HaveOccurred()) + Expect(found).To(BeTrue()) + Expect(compliance).To(Equal("Compliant")) + }) + It("Should be NonCompliant and report resources when the operator is installed", func(ctx SpecContext) { + // Make it musthave and enforced, 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"}]`) + + 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")) + + // Revert to the original mustnothave policy + utils.Kubectl("patch", "operatorpolicy", opPolName, "-n", opPolTestNS, "--type=json", "-p", + `[{"op": "replace", "path": "/spec/complianceType", "value": "mustnothave"},`+ + `{"op": "replace", "path": "/spec/remediationAction", "value": "inform"}]`) + + By("Checking the OperatorPolicy status") + check( + opPolName, + true, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "OperatorGroup", + APIVersion: "operators.coreos.com/v1", + Metadata: policyv1.ObjectMetadata{ + Namespace: opPolTestNS, + }, + }, + Compliant: "NonCompliant", + Reason: "Resource found but should not exist", + }}, + metav1.Condition{ + Type: "OperatorGroupCompliant", + Status: metav1.ConditionFalse, + Reason: "OperatorGroupPresent", + Message: "the OperatorGroup is present", + }, + `the OperatorGroup is present`, + ) + check( + opPolName, + true, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "Subscription", + APIVersion: "operators.coreos.com/v1alpha1", + Metadata: policyv1.ObjectMetadata{ + Namespace: opPolTestNS, + Name: "project-quay", + }, + }, + Compliant: "NonCompliant", + Reason: "Resource found but should not exist", + }}, + metav1.Condition{ + Type: "SubscriptionCompliant", + Status: metav1.ConditionFalse, + Reason: "SubscriptionPresent", + Message: "the Subscription is present", + }, + `the Subscription is present`, + ) + check( + opPolName, + true, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "InstallPlan", + APIVersion: "operators.coreos.com/v1alpha1", + Metadata: policyv1.ObjectMetadata{ + Namespace: opPolTestNS, + }, + }, + Compliant: "NonCompliant", + Reason: "Resource found but should not exist", + }}, + metav1.Condition{ + Type: "InstallPlanCompliant", + Status: metav1.ConditionFalse, + Reason: "InstallPlanPresent", + Message: "the InstallPlan is present", + }, + `the InstallPlan is present`, + ) + check( + opPolName, + true, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "ClusterServiceVersion", + APIVersion: "operators.coreos.com/v1alpha1", + Metadata: policyv1.ObjectMetadata{ + Namespace: opPolTestNS, + }, + }, + Compliant: "NonCompliant", + Reason: "Resource found but should not exist", + }}, + metav1.Condition{ + Type: "ClusterServiceVersionCompliant", + Status: metav1.ConditionFalse, + Reason: "ClusterServiceVersionPresent", + Message: "the ClusterServiceVersion is present", + }, + `the ClusterServiceVersion is present`, + ) + check( + opPolName, + true, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "CustomResourceDefinition", + APIVersion: "apiextensions.k8s.io/v1", + Metadata: policyv1.ObjectMetadata{ + Name: "quayecosystems.redhatcop.redhat.io", + }, + }, + Compliant: "NonCompliant", + Reason: "Resource found but should not exist", + }, { + Object: policyv1.ObjectResource{ + Kind: "CustomResourceDefinition", + APIVersion: "apiextensions.k8s.io/v1", + Metadata: policyv1.ObjectMetadata{ + Name: "quayregistries.quay.redhat.com", + }, + }, + Compliant: "NonCompliant", + Reason: "Resource found but should not exist", + }}, + metav1.Condition{ + Type: "CustomResourceDefinitionCompliant", + Status: metav1.ConditionFalse, + Reason: "CustomResourceDefinitionPresent", + Message: "the CustomResourceDefinition is present", + }, + `the CustomResourceDefinition is present`, + ) + }) + + // These are the same for inform and enforce, so just write them once + keptChecks := func() { + check( + opPolName, + false, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "OperatorGroup", + APIVersion: "operators.coreos.com/v1", + Metadata: policyv1.ObjectMetadata{ + Namespace: opPolTestNS, + }, + }, + Reason: "The OperatorGroup is attached to a mustnothave policy, but does not need to be removed", + }}, + metav1.Condition{ + Type: "OperatorGroupCompliant", + Status: metav1.ConditionTrue, + Reason: "OperatorGroupKept", + Message: "the policy specifies to keep the OperatorGroup", + }, + `the policy specifies to keep the OperatorGroup`, + ) + check( + opPolName, + false, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "Subscription", + APIVersion: "operators.coreos.com/v1alpha1", + Metadata: policyv1.ObjectMetadata{ + Namespace: opPolTestNS, + Name: "project-quay", + }, + }, + Reason: "The Subscription is attached to a mustnothave policy, but does not need to be removed", + }}, + metav1.Condition{ + Type: "SubscriptionCompliant", + Status: metav1.ConditionTrue, + Reason: "SubscriptionKept", + Message: "the policy specifies to keep the Subscription", + }, + `the policy specifies to keep the Subscription`, + ) + check( + opPolName, + false, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "InstallPlan", + APIVersion: "operators.coreos.com/v1alpha1", + Metadata: policyv1.ObjectMetadata{ + Namespace: opPolTestNS, + }, + }, + Reason: "The InstallPlan is attached to a mustnothave policy, but does not need to be removed", + }}, + metav1.Condition{ + Type: "InstallPlanCompliant", + Status: metav1.ConditionTrue, + Reason: "InstallPlanKept", + Message: "the policy specifies to keep the InstallPlan", + }, + `the policy specifies to keep the InstallPlan`, + ) + check( + opPolName, + false, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "ClusterServiceVersion", + APIVersion: "operators.coreos.com/v1alpha1", + Metadata: policyv1.ObjectMetadata{ + Namespace: opPolTestNS, + }, + }, + Reason: "The ClusterServiceVersion is attached to a mustnothave policy, " + + "but does not need to be removed", + }}, + metav1.Condition{ + Type: "ClusterServiceVersionCompliant", + Status: metav1.ConditionTrue, + Reason: "ClusterServiceVersionKept", + Message: "the policy specifies to keep the ClusterServiceVersion", + }, + `the policy specifies to keep the ClusterServiceVersion`, + ) + check( + opPolName, + false, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "CustomResourceDefinition", + APIVersion: "apiextensions.k8s.io/v1", + Metadata: policyv1.ObjectMetadata{ + Name: "quayecosystems.redhatcop.redhat.io", + }, + }, + Reason: "The CustomResourceDefinition is attached to a mustnothave policy, but " + + "does not need to be removed", + }, { + Object: policyv1.ObjectResource{ + Kind: "CustomResourceDefinition", + APIVersion: "apiextensions.k8s.io/v1", + Metadata: policyv1.ObjectMetadata{ + Name: "quayregistries.quay.redhat.com", + }, + }, + Reason: "The CustomResourceDefinition is attached to a mustnothave policy, but " + + "does not need to be removed", + }}, + metav1.Condition{ + Type: "CustomResourceDefinitionCompliant", + Status: metav1.ConditionTrue, + Reason: "CustomResourceDefinitionKept", + Message: "the policy specifies to keep the CustomResourceDefinition", + }, + `the policy specifies to keep the CustomResourceDefinition`, + ) + } + It("Should report resources differently when told to keep them", func(ctx SpecContext) { + // Change the removal behaviors from Delete to Keep + utils.Kubectl("patch", "operatorpolicy", opPolName, "-n", opPolTestNS, "--type=json", "-p", + `[{"op": "replace", "path": "/spec/removalBehavior/operatorGroups", "value": "Keep"},`+ + `{"op": "replace", "path": "/spec/removalBehavior/subscriptions", "value": "Keep"},`+ + `{"op": "replace", "path": "/spec/removalBehavior/clusterServiceVersions", "value": "Keep"},`+ + `{"op": "replace", "path": "/spec/removalBehavior/installPlans", "value": "Keep"},`+ + `{"op": "replace", "path": "/spec/removalBehavior/customResourceDefinitions", "value": "Keep"}]`) + By("Checking the OperatorPolicy status") + keptChecks() + }) + It("Should not remove anything when enforced while set to Keep everything", func(ctx SpecContext) { + // Enforce the policy + utils.Kubectl("patch", "operatorpolicy", opPolName, "-n", opPolTestNS, "--type=json", "-p", + `[{"op": "replace", "path": "/spec/remediationAction", "value": "enforce"}]`) + By("Checking the OperatorPolicy status") + keptChecks() + + By("Checking that certain (named) resources are still there") + utils.GetWithTimeout(clientManagedDynamic, gvrClusterServiceVersion, "quay-operator.v3.8.13", + opPolTestNS, true, eventuallyTimeout) + utils.GetWithTimeout(clientManagedDynamic, gvrSubscription, subName, + opPolTestNS, true, eventuallyTimeout) + utils.GetWithTimeout(clientManagedDynamic, gvrCRD, "quayecosystems.redhatcop.redhat.io", + "", true, eventuallyTimeout) + utils.GetWithTimeout(clientManagedDynamic, gvrCRD, "quayregistries.quay.redhat.com", + "", true, eventuallyTimeout) + }) + It("Should remove things when enforced while set to Delete everything", func(ctx SpecContext) { + // Change the removal behaviors from Keep 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/subscriptions", "value": "Delete"},`+ + `{"op": "replace", "path": "/spec/removalBehavior/clusterServiceVersions", "value": "Delete"},`+ + `{"op": "replace", "path": "/spec/removalBehavior/installPlans", "value": "Delete"},`+ + `{"op": "replace", "path": "/spec/removalBehavior/customResourceDefinitions", "value": "Delete"}]`) + + By("Checking that certain (named) resources are not there, indicating the removal was completed") + utils.GetWithTimeout(clientManagedDynamic, gvrClusterServiceVersion, "quay-operator.v3.8.13", + opPolTestNS, false, eventuallyTimeout) + utils.GetWithTimeout(clientManagedDynamic, gvrSubscription, subName, + opPolTestNS, false, eventuallyTimeout) + utils.GetWithTimeout(clientManagedDynamic, gvrCRD, "quayecosystems.redhatcop.redhat.io", + "", false, eventuallyTimeout) + utils.GetWithTimeout(clientManagedDynamic, gvrCRD, "quayregistries.quay.redhat.com", + "", false, eventuallyTimeout) + + By("Checking the OperatorPolicy status") + check( + opPolName, + false, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "OperatorGroup", + APIVersion: "operators.coreos.com/v1", + Metadata: policyv1.ObjectMetadata{ + Namespace: opPolTestNS, + Name: "-", + }, + }, + Compliant: "Compliant", + Reason: "Resource not found as expected", + }}, + metav1.Condition{ + Type: "OperatorGroupCompliant", + Status: metav1.ConditionTrue, + Reason: "OperatorGroupNotPresent", + Message: "the OperatorGroup is not present", + }, + `the OperatorGroup was deleted`, + ) + check( + opPolName, + false, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "Subscription", + APIVersion: "operators.coreos.com/v1alpha1", + Metadata: policyv1.ObjectMetadata{ + Namespace: opPolTestNS, + Name: "project-quay", + }, + }, + Compliant: "Compliant", + Reason: "Resource not found as expected", + }}, + metav1.Condition{ + Type: "SubscriptionCompliant", + Status: metav1.ConditionTrue, + Reason: "SubscriptionNotPresent", + Message: "the Subscription is not present", + }, + `the Subscription was deleted`, + ) + check( + opPolName, + false, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "InstallPlan", + APIVersion: "operators.coreos.com/v1alpha1", + Metadata: policyv1.ObjectMetadata{ + Namespace: opPolTestNS, + Name: "-", + }, + }, + Compliant: "Compliant", + Reason: "There are no relevant InstallPlans in this namespace", + }}, + metav1.Condition{ + Type: "InstallPlanCompliant", + Status: metav1.ConditionTrue, + Reason: "NoInstallPlansFound", + Message: "there are no relevant InstallPlans in the namespace", + }, + `the InstallPlan was deleted`, + ) + check( + opPolName, + false, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "ClusterServiceVersion", + APIVersion: "operators.coreos.com/v1alpha1", + Metadata: policyv1.ObjectMetadata{ + Namespace: opPolTestNS, + Name: "-", + }, + }, + Compliant: "Compliant", + Reason: "Resource not found as expected", + }}, + metav1.Condition{ + Type: "ClusterServiceVersionCompliant", + Status: metav1.ConditionTrue, + Reason: "ClusterServiceVersionNotPresent", + Message: "the ClusterServiceVersion is not present", + }, + `the ClusterServiceVersion was deleted`, + ) + check( + opPolName, + false, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "CustomResourceDefinition", + APIVersion: "apiextensions.k8s.io/v1", + Metadata: policyv1.ObjectMetadata{ + Name: "-", + }, + }, + Compliant: "Compliant", + Reason: "No relevant CustomResourceDefinitions found", + }}, + metav1.Condition{ + Type: "CustomResourceDefinitionCompliant", + Status: metav1.ConditionTrue, + Reason: "RelevantCRDNotFound", + Message: "No CRDs were found for the operator", + }, + `the CustomResourceDefinition was deleted`, + ) + + // the checks don't verify that the policy is compliant, do that now: + pol := utils.GetWithTimeout(clientManagedDynamic, gvrOperatorPolicy, opPolName, + opPolTestNS, true, eventuallyTimeout) + compliance, found, err := unstructured.NestedString(pol.Object, "status", "compliant") + Expect(err).NotTo(HaveOccurred()) + Expect(found).To(BeTrue()) + Expect(compliance).To(Equal("Compliant")) + }) + }) + Describe("Testing mustnothave behavior for an operator group that is different than the specified one", func() { + const ( + opPolYAML = "../resources/case38_operator_install/operator-policy-with-group.yaml" + opPolName = "oppol-with-group" + subName = "project-quay" + ) + + BeforeEach(func() { + utils.Kubectl("create", "ns", opPolTestNS) + DeferCleanup(func() { + utils.Kubectl("delete", "ns", opPolTestNS) + }) + + createObjWithParent(parentPolicyYAML, parentPolicyName, + opPolYAML, opPolTestNS, gvrPolicy, gvrOperatorPolicy) + }) + + It("should not report an operator group that does not match the spec", func() { + // create the extra operator group + utils.Kubectl("apply", "-f", "../resources/case38_operator_install/incorrect-operator-group.yaml", + "-n", opPolTestNS) + // change the operator policy to mustnothave + utils.Kubectl("patch", "operatorpolicy", opPolName, "-n", opPolTestNS, "--type=json", "-p", + `[{"op": "replace", "path": "/spec/complianceType", "value": "mustnothave"}]`) + + check( + opPolName, + false, + []policyv1.RelatedObject{{ + Object: policyv1.ObjectResource{ + Kind: "OperatorGroup", + APIVersion: "operators.coreos.com/v1", + Metadata: policyv1.ObjectMetadata{ + Namespace: opPolTestNS, + Name: "scoped-operator-group", + }, + }, + Compliant: "Compliant", + Reason: "Resource not found as expected", + }}, + metav1.Condition{ + Type: "OperatorGroupCompliant", + Status: metav1.ConditionTrue, + Reason: "OperatorGroupNotPresent", + Message: "the OperatorGroup is not present", + }, + "the OperatorGroup is not present", + ) + }) + }) + Describe("Testing mustnothave behavior of operator groups in DeleteIfUnused mode", Ordered, func() { + const ( + opPolYAML = "../resources/case38_operator_install/operator-policy-mustnothave.yaml" + otherYAML = "../resources/case38_operator_install/operator-policy-authorino.yaml" + opPolName = "oppol-mustnothave" + subName = "project-quay" + ) + + BeforeEach(func() { + utils.Kubectl("create", "ns", opPolTestNS) + utils.Kubectl("delete", "crd", "--selector=olm.managed=true") + DeferCleanup(func() { + utils.Kubectl("delete", "ns", opPolTestNS) + }) + + createObjWithParent(parentPolicyYAML, parentPolicyName, + opPolYAML, opPolTestNS, gvrPolicy, gvrOperatorPolicy) + }) + + It("should delete the 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"}]`) + + 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 not delete the 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"},`+ + `{"op": "replace", "path": "/spec/remediationAction", "value": "enforce"},`+ + `{"op": "replace", "path": "/spec/removalBehavior/operatorGroups", "value": "DeleteIfUnused"}]`) + + 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 another operator policy in the namespace") + createObjWithParent(parentPolicyYAML, parentPolicyName, + otherYAML, opPolTestNS, gvrPolicy, gvrOperatorPolicy) + + // enforce the other policy + utils.Kubectl("patch", "operatorpolicy", "oppol-authorino", "-n", opPolTestNS, "--type=json", "-p", + `[{"op": "replace", "path": "/spec/remediationAction", "value": "enforce"}]`) + + By("Waiting for a CRD to appear, which should indicate the other operator was successfully installed.") + Eventually(func(ctx SpecContext) *unstructured.Unstructured { + crd, _ := clientManagedDynamic.Resource(gvrCRD).Get(ctx, + "authconfigs.authorino.kuadrant.io", metav1.GetOptions{}) + + return crd + }, olmWaitTimeout, 5, ctx).ShouldNot(BeNil()) + + // revert main policy 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()) + }) + }) + Describe("Testing defaulted values in an OperatorPolicy", func() { + const ( + opPolYAML = "../resources/case38_operator_install/operator-policy-authorino.yaml" + opPolName = "oppol-authorino" + subName = "authorino-operator" + ) + + BeforeEach(func() { + utils.Kubectl("create", "ns", opPolTestNS) + DeferCleanup(func() { + utils.Kubectl("delete", "ns", opPolTestNS) + }) + + createObjWithParent(parentPolicyYAML, parentPolicyName, + opPolYAML, opPolTestNS, gvrPolicy, gvrOperatorPolicy) + }) + + It("Should have applied defaults to the removalBehavior field", func(ctx SpecContext) { + policy, err := clientManagedDynamic.Resource(gvrOperatorPolicy).Namespace(opPolTestNS). + Get(ctx, opPolName, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(policy).NotTo(BeNil()) + + remBehavior, found, err := unstructured.NestedStringMap(policy.Object, "spec", "removalBehavior") + Expect(found).To(BeTrue()) + Expect(err).NotTo(HaveOccurred()) + + Expect(remBehavior).To(HaveKeyWithValue("operatorGroups", "DeleteIfUnused")) + Expect(remBehavior).To(HaveKeyWithValue("subscriptions", "Delete")) + Expect(remBehavior).To(HaveKeyWithValue("clusterServiceVersions", "Delete")) + Expect(remBehavior).To(HaveKeyWithValue("installPlans", "Keep")) + Expect(remBehavior).To(HaveKeyWithValue("customResourceDefinitions", "Keep")) + }) + }) }) diff --git a/test/resources/case38_operator_install/operator-policy-mustnothave.yaml b/test/resources/case38_operator_install/operator-policy-mustnothave.yaml new file mode 100644 index 00000000..4b994677 --- /dev/null +++ b/test/resources/case38_operator_install/operator-policy-mustnothave.yaml @@ -0,0 +1,32 @@ +apiVersion: policy.open-cluster-management.io/v1beta1 +kind: OperatorPolicy +metadata: + name: oppol-mustnothave + annotations: + policy.open-cluster-management.io/parent-policy-compliance-db-id: "124" + policy.open-cluster-management.io/policy-compliance-db-id: "64" + ownerReferences: + - apiVersion: policy.open-cluster-management.io/v1 + kind: Policy + name: parent-policy + uid: 12345678-90ab-cdef-1234-567890abcdef # must be replaced before creation +spec: + remediationAction: inform + severity: medium + complianceType: mustnothave + subscription: + channel: stable-3.8 + name: project-quay + namespace: operator-policy-testns + installPlanApproval: Manual + source: operatorhubio-catalog + sourceNamespace: olm + startingCSV: quay-operator.v3.8.13 + versions: + - quay-operator.v3.8.13 + removalBehavior: + operatorGroups: Delete + subscriptions: Delete + clusterServiceVersions: Delete + installPlans: Delete + customResourceDefinitions: Delete