diff --git a/go.mod b/go.mod index 3a199300..fcac9944 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( k8s.io/apimachinery v0.28.2 k8s.io/client-go v13.0.0+incompatible k8s.io/helm v2.17.0+incompatible - k8s.io/klog/v2 v2.100.1 // indirect + k8s.io/klog/v2 v2.100.1 k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect open-cluster-management.io/multicloud-operators-channel v0.8.0 open-cluster-management.io/multicloud-operators-subscription v0.8.0 //Use 2.0 when available diff --git a/pkg/reconciler/reconciler_test.go b/pkg/reconciler/reconciler_test.go index b775533c..9ba5d4b5 100644 --- a/pkg/reconciler/reconciler_test.go +++ b/pkg/reconciler/reconciler_test.go @@ -48,7 +48,7 @@ func initTestReconciler() *Reconciler { func createNodeEvents() []tr.NodeEvent { events := NodeEdge{} nodeEvents := []tr.NodeEvent{} - //First Node + // First Node unstructuredInput := unstructured.Unstructured{ Object: map[string]interface{}{ "kind": "testowner", @@ -77,7 +77,7 @@ func createNodeEvents() []tr.NodeEvent { events.BuildNode = append(events.BuildNode, podNode) events.BuildEdges = append(events.BuildEdges, podEdges) - //Convert events to node events + // Convert events to node events for i := range events.BuildNode { ne := tr.NodeEvent{ Time: time.Now().Unix(), @@ -237,10 +237,10 @@ func TestReconcilerRedundant(t *testing.T) { func TestReconcilerAddEdges(t *testing.T) { testReconciler := initTestReconciler() - //Add events + // Add events events := createNodeEvents() - //Input node events to reconciler + // Input node events to reconciler go func() { for _, ne := range events { testReconciler.Input <- ne @@ -250,16 +250,16 @@ func TestReconcilerAddEdges(t *testing.T) { for range events { testReconciler.reconcileNode() } - //Build edges + // Build edges edgeMap1 := testReconciler.allEdges() - //Expected edge + // Expected edge edgeMap2 := make(map[string]map[string]tr.Edge, 1) edge := tr.Edge{EdgeType: "ownedBy", SourceUID: "local-cluster/5678", DestUID: "local-cluster/1234", SourceKind: "Pod", DestKind: "testowner"} edgeMap2["local-cluster/5678"] = map[string]tr.Edge{} edgeMap2["local-cluster/5678"]["local-cluster/1234"] = edge - //Check if the actual and expected edges are the same + // Check if the actual and expected edges are the same if !reflect.DeepEqual(edgeMap1, edgeMap2) { t.Fatal("Expected edges not found") } else { @@ -269,17 +269,17 @@ func TestReconcilerAddEdges(t *testing.T) { func TestReconcilerDiff(t *testing.T) { testReconciler := initTestReconciler() - //Add a node to reconciler previous nodes + // Add a node to reconciler previous nodes testReconciler.previousNodes["local-cluster/1234"] = tr.Node{ UID: "local-cluster/1234", Properties: map[string]interface{}{ "very": "important", }, } - //Add events + // Add events events := createNodeEvents() - //Input node events to reconciler + // Input node events to reconciler go func() { for _, ne := range events { testReconciler.Input <- ne @@ -289,9 +289,9 @@ func TestReconcilerDiff(t *testing.T) { for range events { testReconciler.reconcileNode() } - //Compute reconciler diff - this time there should be 1 node and edge to add, 1 node to update + // Compute reconciler diff - this time there should be 1 node and edge to add, 1 node to update diff := testReconciler.Diff() - //Compute reconciler diff again - this time there shouldn't be any new edges or nodes to add/update + // Compute reconciler diff again - this time there shouldn't be any new edges or nodes to add/update nextDiff := testReconciler.Diff() if (len(diff.AddNodes) != 1 || len(diff.UpdateNodes) != 1 || len(diff.AddEdges) != 1) || @@ -306,7 +306,7 @@ func TestReconcilerComplete(t *testing.T) { input := make(chan *tr.Event) output := make(chan tr.NodeEvent) ts := time.Now().Unix() - //Read all files in test-data + // Read all files in test-data dir := "../../test-data" files, err := os.ReadDir(dir) if err != nil { @@ -316,19 +316,19 @@ func TestReconcilerComplete(t *testing.T) { events := make([]tr.Event, 0) var appInput unstructured.Unstructured - //Variables to keep track of helm release object + // Variables to keep track of helm release object var c v1.ConfigMap var rls release.Release rlsFileCount := 0 - var rlsEvnt = &tr.Event{} + rlsEvnt := &tr.Event{} - //Convert to events + // Convert to events for _, file := range files { filePath := dir + "/" + file.Name() if file.Name() != "helmrelease-release.json" { tr.UnmarshalFile(filePath, &appInput, t) appInputLocal := appInput - var in = &tr.Event{ + in := &tr.Event{ Time: ts, Operation: tr.Create, Resource: &appInputLocal, @@ -350,7 +350,7 @@ func TestReconcilerComplete(t *testing.T) { testReconciler := initTestReconciler() go tr.TransformRoutine(input, output) - //Convert events to Node events + // Convert events to Node events go func() { for _, ev := range events { localEv := &ev @@ -378,7 +378,7 @@ func TestReconcilerComplete(t *testing.T) { // Checks the count of nodes and edges based on the JSON files in pkg/test-data // Update counts when the test data is changed // We don't create Nodes for kind = Event - const Nodes = 36 + const Nodes = 39 const Edges = 51 if len(com.Edges) != Edges || com.TotalEdges != Edges || len(com.Nodes) != Nodes || com.TotalNodes != Nodes { ns := tr.NodeStore{ diff --git a/pkg/transforms/policy.go b/pkg/transforms/policy.go index 76052654..5ad15f4b 100644 --- a/pkg/transforms/policy.go +++ b/pkg/transforms/policy.go @@ -11,7 +11,11 @@ Copyright (c) 2020 Red Hat, Inc. package transforms import ( - p "github.com/stolostron/governance-policy-propagator/api/v1" + "strings" + + policy "github.com/stolostron/governance-policy-propagator/api/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) // PolicyResource ... @@ -20,7 +24,7 @@ type PolicyResource struct { } // PolicyResourceBuilder ... -func PolicyResourceBuilder(p *p.Policy) *PolicyResource { +func PolicyResourceBuilder(p *policy.Policy) *PolicyResource { node := transformCommon(p) // Start off with the common properties apiGroupVersion(p.TypeMeta, &node) // add kind, apigroup and version // Extract the properties specific to this type @@ -37,6 +41,93 @@ func PolicyResourceBuilder(p *p.Policy) *PolicyResource { if okns && okpp { node.Properties["_parentPolicy"] = pnamespace + "/" + ppolicy } + + return &PolicyResource{node: node} +} + +// For cert, config, operator policies. +// This function returns `annotations`, `_isExternal` for `source`, +// and `severity`, `compliant`, and `remediationAction`. +func getPolicyCommonProperties(c *unstructured.Unstructured, node Node) Node { + node.Properties["_isExternal"] = false + + for _, m := range c.GetManagedFields() { + if m.Manager == "multicluster-operators-subscription" || + strings.Contains(m.Manager, "argocd") { + node.Properties["_isExternal"] = true + + break + } + } + + typeMeta := metav1.TypeMeta{ + Kind: c.GetKind(), + APIVersion: c.GetAPIVersion(), + } + + apiGroupVersion(typeMeta, &node) // add kind, apigroup and version + + node.Properties["compliant"], _, _ = unstructured.NestedString(c.Object, "status", "compliant") + + node.Properties["severity"], _, _ = unstructured.NestedString(c.Object, "spec", "severity") + + node.Properties["remediationAction"], _, _ = unstructured.NestedString(c.Object, "spec", "remediationAction") + + node.Properties["disabled"], _, _ = unstructured.NestedBool(c.Object, "spec", "disabled") + + return node +} + +// For cert, config policies. +func StandalonePolicyResourceBuilder(c *unstructured.Unstructured) *PolicyResource { + node := transformCommon(c) // Start off with the common properties + + return &PolicyResource{node: getPolicyCommonProperties(c, node)} +} + +func OperatorPolicyResourceBuilder(c *unstructured.Unstructured) *PolicyResource { + node := transformCommon(c) // Start off with the common properties + + node = getPolicyCommonProperties(c, node) + + var deploymentAvailable bool + var upgradeAvailable bool + + conditions, _, _ := unstructured.NestedSlice(c.Object, "status", "conditions") + for _, condition := range conditions { + mapCondition, ok := condition.(map[string]interface{}) + if !ok { + continue + } + + conditionType, found, err := unstructured.NestedString(mapCondition, "type") + if !found || err != nil { + continue + } + + conditionReason, found, err := unstructured.NestedString(mapCondition, "reason") + if !found || err != nil { + continue + } + + if conditionType == "InstallPlanCompliant" { + if conditionReason == "InstallPlanRequiresApproval" || conditionReason == "InstallPlanApproved" { + upgradeAvailable = true + } else { + upgradeAvailable = false + } + } else if conditionType == "DeploymentCompliant" { + if conditionReason == "DeploymentsAvailable" { + deploymentAvailable = true + } else { + deploymentAvailable = false + } + } + } + + node.Properties["deploymentAvailable"] = deploymentAvailable + node.Properties["upgradeAvailable"] = upgradeAvailable + return &PolicyResource{node: node} } @@ -47,6 +138,6 @@ func (p PolicyResource) BuildNode() Node { // BuildEdges construct the edges for Policy Resources func (p PolicyResource) BuildEdges(ns NodeStore) []Edge { - //no op for now to implement interface + // no op for now to implement interface return []Edge{} } diff --git a/pkg/transforms/policy_test.go b/pkg/transforms/policy_test.go index 7f3e4b12..f0365b40 100644 --- a/pkg/transforms/policy_test.go +++ b/pkg/transforms/policy_test.go @@ -14,6 +14,7 @@ import ( "testing" policy "github.com/stolostron/governance-policy-propagator/api/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) func TestTransformPolicy(t *testing.T) { @@ -26,3 +27,64 @@ func TestTransformPolicy(t *testing.T) { AssertEqual("disabled", node.Properties["disabled"], false, t) AssertEqual("numRules", node.Properties["numRules"], 1, t) } + +func TestTransformConfigPolicy(t *testing.T) { + var object map[string]interface{} + UnmarshalFile("configurationpolicy.json", &object, t) + + unstructured := &unstructured.Unstructured{ + Object: object, + } + + configResource := StandalonePolicyResourceBuilder(unstructured) + + node := configResource.BuildNode() + + // Test only the fields that exist in policy - the common test will test the other bits + AssertEqual("compliant", node.Properties["compliant"], "NonCompliant", t) + AssertEqual("remediationAction", node.Properties["remediationAction"], "inform", t) + AssertEqual("severity", node.Properties["severity"], "low", t) + AssertEqual("disabled", node.Properties["disabled"], false, t) + AssertEqual("_isExternal", node.Properties["_isExternal"], true, t) +} + +func TestTransformOperatorPolicy(t *testing.T) { + var object map[string]interface{} + UnmarshalFile("operatorpolicy.json", &object, t) + + unstructured := &unstructured.Unstructured{ + Object: object, + } + + operatorResource := OperatorPolicyResourceBuilder(unstructured) + + node := operatorResource.BuildNode() + + // Test only the fields that exist in policy - the common test will test the other bits + AssertEqual("compliant", node.Properties["compliant"], "NonCompliant", t) + AssertEqual("remediationAction", node.Properties["remediationAction"], "inform", t) + AssertEqual("severity", node.Properties["severity"], "critical", t) + AssertEqual("deploymentAvailable", node.Properties["deploymentAvailable"], false, t) + AssertEqual("upgradeAvailable", node.Properties["upgradeAvailable"], true, t) + AssertEqual("disabled", node.Properties["disabled"], false, t) + AssertEqual("_isExternal", node.Properties["_isExternal"], false, t) +} + +func TestTransformCertPolicy(t *testing.T) { + var object map[string]interface{} + UnmarshalFile("certpolicy.json", &object, t) + + unstructured := &unstructured.Unstructured{ + Object: object, + } + + certResource := StandalonePolicyResourceBuilder(unstructured) + + node := certResource.BuildNode() + + // Test only the fields that exist in policy - the common test will test the other bits + AssertEqual("compliant", node.Properties["compliant"], "Compliant", t) + AssertEqual("severity", node.Properties["severity"], "low", t) + AssertEqual("disabled", node.Properties["disabled"], false, t) + AssertEqual("_isExternal", node.Properties["_isExternal"], true, t) +} diff --git a/pkg/transforms/transformer.go b/pkg/transforms/transformer.go index 310ba272..cc569d7c 100644 --- a/pkg/transforms/transformer.go +++ b/pkg/transforms/transformer.go @@ -36,7 +36,10 @@ import ( // Operation is the event operation type Operation int -var APPS_OPEN_CLUSTER_MANAGEMENT_IO = "apps.open-cluster-management.io" +const ( + APPS_OPEN_CLUSTER_MANAGEMENT_IO = "apps.open-cluster-management.io" + POLICY_OPEN_CLUSTER_MANAGEMENT_IO = "policy.open-cluster-management.io" +) const ( Create Operation = iota // 0 @@ -131,7 +134,7 @@ type Transformer struct { } var ( - NonNSResourceMap map[string]struct{} //store non-namespaced resources in this map + NonNSResourceMap map[string]struct{} // store non-namespaced resources in this map NonNSResMapMutex = sync.RWMutex{} ) @@ -151,7 +154,6 @@ func NewTransformer(inputChan chan *Event, outputChan chan NodeEvent, numRoutine Input: inputChan, Output: outputChan, } - } // This function processes k8s objects into Nodes, then pass them into the output channel. @@ -245,7 +247,7 @@ func TransformRoutine(input chan *Event, output chan NodeEvent) { } trans = DeploymentResourceBuilder(&typedResource) - //This is an ocp specific resource + // This is an ocp specific resource case [2]string{"DeploymentConfig", "apps.openshift.io"}: typedResource := ocpapp.DeploymentConfig{} err := runtime.DefaultUnstructuredConverter. @@ -255,7 +257,7 @@ func TransformRoutine(input chan *Event, output chan NodeEvent) { } trans = DeploymentConfigResourceBuilder(&typedResource) - //This is the application's HelmCR of kind HelmRelease. + // This is the application's HelmCR of kind HelmRelease. case [2]string{"HelmRelease", APPS_OPEN_CLUSTER_MANAGEMENT_IO}: typedResource := appHelmRelease.HelmRelease{} err := runtime.DefaultUnstructuredConverter. @@ -346,7 +348,7 @@ func TransformRoutine(input chan *Event, output chan NodeEvent) { } trans = PodResourceBuilder(&typedResource) - case [2]string{"Policy", "policy.open-cluster-management.io"}, + case [2]string{"Policy", POLICY_OPEN_CLUSTER_MANAGEMENT_IO}, [2]string{"Policy", "policies.open-cluster-management.io"}: typedResource := policy.Policy{} err := runtime.DefaultUnstructuredConverter. @@ -356,6 +358,13 @@ func TransformRoutine(input chan *Event, output chan NodeEvent) { } trans = PolicyResourceBuilder(&typedResource) + case [2]string{"ConfigurationPolicy", POLICY_OPEN_CLUSTER_MANAGEMENT_IO}, + [2]string{"CertificatePolicy", POLICY_OPEN_CLUSTER_MANAGEMENT_IO}: + trans = StandalonePolicyResourceBuilder(event.Resource) + + case [2]string{"OperatorPolicy", POLICY_OPEN_CLUSTER_MANAGEMENT_IO}: + trans = OperatorPolicyResourceBuilder(event.Resource) + case [2]string{"ReplicaSet", "apps"}, [2]string{"ReplicaSet", "extensions"}: typedResource := apps.ReplicaSet{} diff --git a/test-data/certpolicy.json b/test-data/certpolicy.json new file mode 100644 index 00000000..0b5c7a62 --- /dev/null +++ b/test-data/certpolicy.json @@ -0,0 +1,53 @@ +{ + "apiVersion": "policy.open-cluster-management.io/v1", + "kind": "CertificatePolicy", + "metadata": { + "creationTimestamp": "2024-08-15T19:21:44Z", + "generation": 1, + "labels": { + "cluster-name": "local-cluster", + "cluster-namespace": "local-cluster", + "policy.open-cluster-management.io/cluster-name": "local-cluster", + "policy.open-cluster-management.io/cluster-namespace": "local-cluster", + "policy.open-cluster-management.io/policy": "default.cert-policy-dd" + }, + "managedFields": [ + { + "apiVersion": "policy.open-cluster-management.io/v1", + "manager": "multicluster-operators-subscription" + } + ], + "name": "policy-certificate", + "namespace": "local-cluster", + "ownerReferences": [ + { + "apiVersion": "policy.open-cluster-management.io/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "Policy", + "name": "default.cert-policy-dd", + "uid": "3104a97d-c42a-43de-b8fa-b8579d9d06b3" + } + ], + "resourceVersion": "395036", + "uid": "2fc12fad-80d4-4545-b9d5-bc329f12f4d1" + }, + "spec": { + "disabled": false, + "minimumDuration": "300h", + "namespaceSelector": { + "exclude": ["kube-*"], + "include": ["default"] + }, + "remediationAction": "inform", + "severity": "low" + }, + "status": { + "compliancyDetails": { + "default": { + "message": "Found 0 non compliant certificates in the namespace default.\n" + } + }, + "compliant": "Compliant" + } +} diff --git a/test-data/configurationpolicy.json b/test-data/configurationpolicy.json new file mode 100644 index 00000000..b20146d6 --- /dev/null +++ b/test-data/configurationpolicy.json @@ -0,0 +1,83 @@ +{ + "apiVersion": "policy.open-cluster-management.io/v1", + "kind": "ConfigurationPolicy", + "metadata": { + "uid": "config-uid-1", + "creationTimestamp": "2024-07-19T14:47:58Z", + "finalizers": ["policy.open-cluster-management.io/delete-related-objects"], + "generation": 1, + "annotations": { + "apps.open-cluster-management.io/hosting-subscription": "policies/demo-sub", + "kubectl.kubernetes.io/last-applied-configuration": "tiger" + }, + "labels": { + "cluster-name": "local-cluster", + "cluster-namespace": "local-cluster", + "policy.open-cluster-management.io/cluster-name": "local-cluster", + "policy.open-cluster-management.io/cluster-namespace": "local-cluster", + "policy.open-cluster-management.io/policy": "default.dd" + }, + "name": "policy-namespace", + "namespace": "local-cluster", + "managedFields": [ + { + "apiVersion": "policy.open-cluster-management.io/v1beta1", + "fieldsType": "FieldsV1", + "manager": "argocd" + } + ] + }, + "spec": { + "disabled": false, + "object-templates": [ + { + "complianceType": "musthave", + "objectDefinition": { + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "name": "hi" + } + }, + "recreateOption": "None" + } + ], + "pruneObjectBehavior": "DeleteAll", + "remediationAction": "inform", + "severity": "low" + }, + "status": { + "compliancyDetails": [ + { + "Compliant": "NonCompliant", + "Validity": {}, + "conditions": [ + { + "lastTransitionTime": "2024-07-19T14:47:59Z", + "message": "namespaces [hi] not found", + "reason": "K8s does not have a `must have` object", + "status": "True", + "type": "violation" + } + ] + } + ], + "compliant": "NonCompliant", + "lastEvaluated": "2024-07-19T16:55:31Z", + "lastEvaluatedGeneration": 1, + "relatedObjects": [ + { + "compliant": "NonCompliant", + "object": { + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "name": "hi" + } + }, + "reason": "Resource not found but should exist", + "cluster": "local-cluster" + } + ] + } +} diff --git a/test-data/operatorpolicy.json b/test-data/operatorpolicy.json new file mode 100644 index 00000000..47950dec --- /dev/null +++ b/test-data/operatorpolicy.json @@ -0,0 +1,226 @@ +{ + "apiVersion": "policy.open-cluster-management.io/v1beta1", + "kind": "OperatorPolicy", + "metadata": { + "creationTimestamp": "2024-08-15T16:58:38Z", + "generation": 1, + "annotations": { "apps.open-cluster-management.io/hosting-subscription": "policies/demo-sub" }, + "labels": { + "cluster-name": "local-cluster", + "cluster-namespace": "local-cluster", + "policy.open-cluster-management.io/cluster-name": "local-cluster", + "policy.open-cluster-management.io/cluster-namespace": "local-cluster", + "policy.open-cluster-management.io/policy": "default.dd-operator" + }, + "managedFields": [ + { + "fieldsType": "FieldsV1", + "manager": "config-policy-controller" + } + ], + "name": "install-operator", + "namespace": "local-cluster", + "ownerReferences": [ + { + "apiVersion": "policy.open-cluster-management.io/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "Policy", + "name": "default.dd-operator", + "uid": "157a2f9c-8329-4377-99a8-544673e63cc7" + } + ], + "resourceVersion": "273745", + "uid": "2d0732a5-15b1-4631-9b8d-032f067b98d2" + }, + "spec": { + "disabled": false, + "complianceConfig": { + "catalogSourceUnhealthy": "Compliant", + "deploymentsUnavailable": "NonCompliant", + "upgradesAvailable": "Compliant" + }, + "complianceType": "musthave", + "remediationAction": "inform", + "removalBehavior": { + "clusterServiceVersions": "Delete", + "customResourceDefinitions": "Keep", + "operatorGroups": "DeleteIfUnused", + "subscriptions": "Delete" + }, + "severity": "critical", + "subscription": { + "channel": "stable", + "name": "gatekeeper-operator-product", + "namespace": "openshift-operators", + "source": "redhat-operators", + "sourceNamespace": "openshift-marketplace", + "startingCSV": "v3.15.0" + }, + "upgradeApproval": "Automatic" + }, + "status": { + "compliant": "NonCompliant", + "conditions": [ + { + "lastTransitionTime": "2024-08-15T16:58:38Z", + "message": "CatalogSource was found", + "reason": "CatalogSourcesFound", + "status": "False", + "type": "CatalogSourcesUnhealthy" + }, + { + "lastTransitionTime": "2024-08-15T16:58:38Z", + "message": "the ClusterServiceVersion required by the policy was not found", + "reason": "ClusterServiceVersionMissing", + "status": "False", + "type": "ClusterServiceVersionCompliant" + }, + { + "lastTransitionTime": "2024-08-15T16:58:38Z", + "message": "NonCompliant; the policy spec is valid, the policy does not specify an OperatorGroup but one already exists in the namespace - assuming that OperatorGroup is correct, the Subscription required by the policy was not found, there are no relevant InstallPlans in the namespace, the ClusterServiceVersion required by the policy was not found, no CRDs were found for the operator, there are no relevant deployments because the ClusterServiceVersion is missing, CatalogSource was found", + "reason": "NonCompliant", + "status": "False", + "type": "Compliant" + }, + { + "lastTransitionTime": "2024-08-15T16:58:38Z", + "message": "no CRDs were found for the operator", + "reason": "RelevantCRDNotFound", + "status": "True", + "type": "CustomResourceDefinitionCompliant" + }, + { + "lastTransitionTime": "2024-08-15T16:58:38Z", + "message": "there are no relevant deployments because the ClusterServiceVersion is missing", + "reason": "NoRelevantDeployments", + "status": "True", + "type": "DeploymentCompliant" + }, + { + "lastTransitionTime": "2024-08-15T16:58:38Z", + "message": "an InstallPlan to update to [strimzi-cluster-operator.v0.36.0] is available", + "reason": "InstallPlanRequiresApproval", + "status": "True", + "type": "InstallPlanCompliant" + }, + { + "lastTransitionTime": "2024-08-15T16:58:38Z", + "message": "the policy does not specify an OperatorGroup but one already exists in the namespace - assuming that OperatorGroup is correct", + "reason": "PreexistingOperatorGroupFound", + "status": "True", + "type": "OperatorGroupCompliant" + }, + { + "lastTransitionTime": "2024-08-15T16:58:38Z", + "message": "the Subscription required by the policy was not found", + "reason": "SubscriptionMissing", + "status": "False", + "type": "SubscriptionCompliant" + }, + { + "lastTransitionTime": "2024-08-15T16:58:38Z", + "message": "the policy spec is valid", + "reason": "PolicyValidated", + "status": "True", + "type": "ValidPolicySpec" + } + ], + "observedGeneration": 1, + "relatedObjects": [ + { + "compliant": "Compliant", + "object": { + "apiVersion": "operators.coreos.com/v1alpha1", + "kind": "CatalogSource", + "metadata": { + "name": "redhat-operators", + "namespace": "openshift-marketplace" + } + }, + "reason": "Resource found as expected", + "cluster": "local-cluster" + }, + { + "compliant": "NonCompliant", + "object": { + "apiVersion": "operators.coreos.com/v1alpha1", + "kind": "ClusterServiceVersion", + "metadata": { + "name": "gatekeeper-operator-product", + "namespace": "openshift-operators" + } + }, + "reason": "Resource not found but should exist", + "cluster": "local-cluster" + }, + { + "compliant": "UnknownCompliancy", + "object": { + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": { + "name": "-" + } + }, + "reason": "No relevant CustomResourceDefinitions found", + "cluster": "local-cluster" + }, + { + "compliant": "UnknownCompliancy", + "object": { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "-" + } + }, + "reason": "No relevant deployments found", + "cluster": "local-cluster" + }, + { + "compliant": "Compliant", + "object": { + "apiVersion": "operators.coreos.com/v1alpha1", + "kind": "InstallPlan", + "metadata": { + "name": "-", + "namespace": "openshift-operators" + } + }, + "reason": "There are no relevant InstallPlans in this namespace", + "cluster": "local-cluster" + }, + { + "compliant": "Compliant", + "object": { + "apiVersion": "operators.coreos.com/v1", + "kind": "OperatorGroup", + "metadata": { + "name": "global-operators", + "namespace": "openshift-operators" + } + }, + "properties": { + "uid": "7c60436b-6d6b-44a6-a534-2fd01967ade6" + }, + "reason": "Resource found as expected", + "cluster": "local-cluster" + }, + { + "compliant": "NonCompliant", + "object": { + "apiVersion": "operators.coreos.com/v1alpha1", + "kind": "Subscription", + "metadata": { + "name": "gatekeeper-operator-product", + "namespace": "openshift-operators" + } + }, + "reason": "Resource not found but should exist", + "cluster": "local-cluster" + } + ], + "resolvedSubscriptionLabel": "gatekeeper-operator-product.openshift-operators" + } +}