Browse Source

update rego expressions and add crd support for certificates

Alexander Belanger 3 years ago
parent
commit
fdc3eb794b

+ 53 - 1
internal/opa/config.yaml

@@ -45,4 +45,56 @@ nginx_pod:
       app.kubernetes.io/name: "ingress-nginx"
   policies:
   - path: "./policies/pod/running.rego"
-    name: "pod.running"
+    name: "pod.running"
+prometheus_server_pod:
+  kind: "pod"
+  match:
+    namespace: monitoring
+    labels:
+      app: "prometheus"
+      component: "server"
+      release: "prometheus"
+  policies:
+  - path: "./policies/pod/running.rego"
+    name: "pod.running"
+prometheus_alertmanager_pod:
+  kind: "pod"
+  match:
+    namespace: monitoring
+    labels:
+      app: "prometheus"
+      component: "alertmanager"
+      release: "prometheus"
+  policies:
+  - path: "./policies/pod/running.rego"
+    name: "pod.running"
+porter_agent_pod:
+  kind: "pod"
+  match:
+    namespace: porter-agent-system
+    labels:
+      control-plane: "controller-manager"
+  policies:
+  - path: "./policies/pod/running.rego"
+    name: "pod.running"
+porter_agent_redis_pod:
+  kind: "pod"
+  match:
+    namespace: porter-agent-system
+    labels:
+      app.kubernetes.io/component: "master"
+      app.kubernetes.io/instance: "porter-agent"
+      app.kubernetes.io/managed-by: "Helm"
+      app.kubernetes.io/name: "redis"
+  policies:
+  - path: "./policies/pod/running.rego"
+    name: "pod.running"
+certificates:
+  kind: "crd_list"
+  match:
+    group: cert-manager.io
+    version: v1
+    resource: certificates
+  policies:
+  - path: "./policies/certificates/expiry_two_weeks.rego"
+    name: "certificates.expiry_two_weeks"

+ 110 - 16
internal/opa/opa.go

@@ -13,7 +13,10 @@ import (
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/pkg/logger"
 	"helm.sh/helm/v3/pkg/release"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
 )
 
 type KubernetesPolicies struct {
@@ -23,7 +26,8 @@ type KubernetesPolicies struct {
 type KubernetesOPARunner struct {
 	*KubernetesPolicies
 
-	k8sAgent *kubernetes.Agent
+	k8sAgent      *kubernetes.Agent
+	dynamicClient dynamic.Interface
 }
 
 type KubernetesBuiltInKind string
@@ -31,6 +35,7 @@ type KubernetesBuiltInKind string
 const (
 	HelmRelease KubernetesBuiltInKind = "helm_release"
 	Pod         KubernetesBuiltInKind = "pod"
+	CRDList     KubernetesBuiltInKind = "crd_list"
 )
 
 type KubernetesOPAQueryCollection struct {
@@ -46,22 +51,38 @@ type MatchParameters struct {
 	ChartName string `json:"chart_name"`
 
 	Labels map[string]string `json:"labels"`
+
+	// parameters for CRDs
+	Group    string `json:"group"`
+	Version  string `json:"version"`
+	Resource string `json:"resource"`
 }
 
 type OPARecommenderQueryResult struct {
-	Allow bool `mapstructure:"Allow"`
+	Allow bool
 
 	CategoryName string
 	ObjectID     string
 
+	PolicyVersion  string
+	PolicySeverity string
+	PolicyTitle    string
+	PolicyMessage  string
+}
+
+type rawQueryResult struct {
+	Allow          bool   `mapstructure:"ALLOW"`
+	PolicyID       string `mapstructure:"POLICY_ID"`
 	PolicyVersion  string `mapstructure:"POLICY_VERSION"`
 	PolicySeverity string `mapstructure:"POLICY_SEVERITY"`
 	PolicyTitle    string `mapstructure:"POLICY_TITLE"`
-	PolicyMessage  string `mapstructure:"POLICY_MESSAGE"`
+	SuccessMessage string `mapstructure:"POLICY_SUCCESS_MESSAGE"`
+
+	FailureMessage []string `mapstructure:"FAILURE_MESSAGE"`
 }
 
-func NewRunner(policies *KubernetesPolicies, k8sAgent *kubernetes.Agent) *KubernetesOPARunner {
-	return &KubernetesOPARunner{policies, k8sAgent}
+func NewRunner(policies *KubernetesPolicies, k8sAgent *kubernetes.Agent, dynamicClient dynamic.Interface) *KubernetesOPARunner {
+	return &KubernetesOPARunner{policies, k8sAgent, dynamicClient}
 }
 
 func (runner *KubernetesOPARunner) GetRecommendationsByName(name string) ([]*OPARecommenderQueryResult, error) {
@@ -77,6 +98,8 @@ func (runner *KubernetesOPARunner) GetRecommendationsByName(name string) ([]*OPA
 		return runner.runHelmReleaseQueries(name, queryCollection)
 	case Pod:
 		return runner.runPodQueries(name, queryCollection)
+	case CRDList:
+		return runner.runCRDListQueries(name, queryCollection)
 	default:
 		return nil, fmt.Errorf("Not a supported query kind")
 	}
@@ -147,18 +170,19 @@ func (runner *KubernetesOPARunner) runHelmReleaseQueries(name string, collection
 			}
 
 			if len(results) == 1 {
-				queryRes := &OPARecommenderQueryResult{
-					ObjectID:     fmt.Sprintf("helm_release/%s/%s", helmRelease.Namespace, helmRelease.Name),
-					CategoryName: name,
-				}
+				rawQueryRes := &rawQueryResult{}
 
-				err = mapstructure.Decode(results[0].Expressions[0].Value, queryRes)
+				err = mapstructure.Decode(results[0].Expressions[0].Value, rawQueryRes)
 
 				if err != nil {
 					return nil, err
 				}
 
-				res = append(res, queryRes)
+				res = append(res, rawQueryResToRecommenderQueryResult(
+					rawQueryRes,
+					fmt.Sprintf("helm_release/%s/%s/%s", helmRelease.Namespace, helmRelease.Name, rawQueryRes.PolicyID),
+					name,
+				))
 			}
 		}
 	}
@@ -201,21 +225,91 @@ func (runner *KubernetesOPARunner) runPodQueries(name string, collection Kuberne
 			}
 
 			if len(results) == 1 {
-				queryRes := &OPARecommenderQueryResult{
-					ObjectID:     fmt.Sprintf("pod/%s/%s", pod.Namespace, pod.Name),
-					CategoryName: name,
+				rawQueryRes := &rawQueryResult{}
+
+				err = mapstructure.Decode(results[0].Expressions[0].Value, rawQueryRes)
+
+				if err != nil {
+					return nil, err
 				}
 
-				err = mapstructure.Decode(results[0].Expressions[0].Value, queryRes)
+				res = append(res, rawQueryResToRecommenderQueryResult(
+					rawQueryRes,
+					fmt.Sprintf("pod/%s/%s", pod.Namespace, pod.Name),
+					name,
+				))
+			}
+		}
+	}
+
+	return res, nil
+}
+
+func (runner *KubernetesOPARunner) runCRDListQueries(name string, collection KubernetesOPAQueryCollection) ([]*OPARecommenderQueryResult, error) {
+	res := make([]*OPARecommenderQueryResult, 0)
+
+	objRes := schema.GroupVersionResource{
+		Group:    collection.Match.Group,
+		Version:  collection.Match.Version,
+		Resource: collection.Match.Resource,
+	}
+
+	crdList, err := runner.dynamicClient.Resource(objRes).Namespace(collection.Match.Namespace).List(context.Background(), v1.ListOptions{})
+
+	if err != nil {
+		return nil, err
+	}
+
+	for _, crd := range crdList.Items {
+		for _, query := range collection.Queries {
+			results, err := query.Eval(
+				context.Background(),
+				rego.EvalInput(crd.Object),
+			)
+
+			if err != nil {
+				return nil, err
+			}
+
+			if len(results) == 1 {
+				rawQueryRes := &rawQueryResult{}
+
+				err = mapstructure.Decode(results[0].Expressions[0].Value, rawQueryRes)
 
 				if err != nil {
 					return nil, err
 				}
 
-				res = append(res, queryRes)
+				res = append(res, rawQueryResToRecommenderQueryResult(
+					rawQueryRes,
+					fmt.Sprintf("%s/%s/%s/%s", collection.Match.Group, collection.Match.Version, collection.Match.Resource, rawQueryRes.PolicyID),
+					name,
+				))
 			}
 		}
 	}
 
 	return res, nil
 }
+
+func rawQueryResToRecommenderQueryResult(rawQueryRes *rawQueryResult, objectID, categoryName string) *OPARecommenderQueryResult {
+	queryRes := &OPARecommenderQueryResult{
+		ObjectID:     objectID,
+		CategoryName: categoryName,
+	}
+
+	message := rawQueryRes.SuccessMessage
+
+	// if failure, compose failure messages into single string
+	if !rawQueryRes.Allow {
+		message = strings.Join(rawQueryRes.FailureMessage, ". ")
+	}
+
+	queryRes.PolicyMessage = message
+	queryRes.Allow = rawQueryRes.Allow
+	queryRes.PolicySeverity = rawQueryRes.PolicySeverity
+	queryRes.PolicyTitle = rawQueryRes.PolicyTitle
+	queryRes.PolicyVersion = rawQueryRes.PolicyVersion
+
+	return queryRes
+}

+ 30 - 0
internal/opa/policies/certificates/expiry_two_weeks.rego

@@ -0,0 +1,30 @@
+package certificates.expiry_two_weeks
+
+import future.keywords
+
+POLICY_ID := sprintf("certificates_expiry_two_weeks_%s_%s", [input.metadata.namespace, input.metadata.name])
+
+POLICY_VERSION := "v0.0.1"
+
+POLICY_SEVERITY := "high"
+
+POLICY_TITLE := sprintf("Certificate %s/%s should have longer than 2 weeks left before expiry", [input.metadata.namespace, input.metadata.name])
+
+POLICY_SUCCESS_MESSAGE := sprintf("Success: certificate %s/%s has longer than 2 weeks before expiry", [input.metadata.namespace, input.metadata.name])
+
+allow if {
+	not rfc3339_expiry_within_2_weeks(input.status.notAfter)
+}
+
+FAILURE_MESSAGE contains msg if {
+	rfc3339_expiry_within_2_weeks(input.status.notAfter)
+	msg := sprintf("Certificate expires at %s, which is less than 2 weeks from now", [input.status.notAfter])
+}
+
+rfc3339_lt(a, b) if {
+	time.parse_rfc3339_ns(a) < time.parse_rfc3339_ns(b)
+}
+
+rfc3339_expiry_within_2_weeks(a) if {
+	time.add_date(time.parse_rfc3339_ns(a), 0, 0, -14) < time.now_ns()
+}

+ 8 - 5
internal/opa/policies/nginx/memory_limits.rego

@@ -1,6 +1,6 @@
 package nginx.memory_limits
 
-import future.keywords.if
+import future.keywords
 
 # Policy expects input structure of form:
 # values: {}
@@ -17,18 +17,21 @@ import future.keywords.if
 #       cpu: 250m
 #       memory: 275Mi
 
+POLICY_ID := "nginx_memory_limits"
+
 POLICY_VERSION := "v0.0.1"
 
 POLICY_SEVERITY := "high"
 
 POLICY_TITLE := sprintf("NGINX ingress controller should have memory limits set", [])
 
+POLICY_SUCCESS_MESSAGE := sprintf("Success: NGINX ingress controller has memory limits set", [])
+
 allow if {
 	input.values.controller.resources.limits.memory
 }
 
-POLICY_MESSAGE := sprintf("Success: NGINX ingress controller has memory limits set", []) if allow
-
-else := sprintf("Failed: NGINX ingress controller does not have memory limits set", []) {
-	true
+FAILURE_MESSAGE contains msg if {
+	not allow
+	msg := "Failed: NGINX ingress controller does not have memory limits set"
 }

+ 8 - 5
internal/opa/policies/nginx/nginx_topology_spread_constraints.rego

@@ -1,6 +1,6 @@
 package nginx.topology_spread_constraints
 
-import future.keywords.if
+import future.keywords
 
 # Policy expects input structure of form:
 # values: {}
@@ -27,18 +27,21 @@ import future.keywords.if
 #       topologyKey: topology.kubernetes.io/zone
 #       whenUnsatisfiable: ScheduleAnyway
 
+POLICY_ID := "nginx_topology_spread_constraints"
+
 POLICY_VERSION := "v0.0.1"
 
 POLICY_SEVERITY := "high"
 
 POLICY_TITLE := sprintf("NGINX ingress controller should have topology spread constraints", [])
 
+POLICY_SUCCESS_MESSAGE := sprintf("Success: NGINX ingress controller has topology spread constraints", [])
+
 allow if {
 	count(input.values.controller.topologySpreadConstraints) >= 1
 }
 
-POLICY_MESSAGE := sprintf("Success: NGINX ingress controller has topology spread constraints", []) if allow
-
-else := sprintf("Failed: NGINX ingress controller does not have topology spread constraints set", []) {
-	true
+FAILURE_MESSAGE contains msg if {
+	not allow
+	msg := "Failed: NGINX ingress controller does not have topology spread constraints set"
 }

+ 8 - 6
internal/opa/policies/nginx/nginx_version.rego

@@ -1,23 +1,25 @@
 package nginx.version
 
-import future.keywords.if
+import future.keywords
+
+POLICY_ID := "nginx_version"
 
 POLICY_VERSION := "v0.0.1"
 
 POLICY_SEVERITY := "high"
 
-# TODO: set the actual latest stable version
 latest_stable_version := "0.4.18"
 
 POLICY_TITLE := sprintf("The NGINX version should be at least v%s", [latest_stable_version])
 
+POLICY_SUCCESS_MESSAGE := sprintf("Success: NGINX version is up-to-date", [])
+
 trimmedVersion := trim_left(input.version, "v")
 
 # semver.compare returns -1 if latest_stable_version < trimmedVersion
 allow if semver.compare(latest_stable_version, trimmedVersion) == -1
 
-POLICY_MESSAGE := sprintf("Success: NGINX version is up-to-date", []) if allow
-
-else := sprintf("Failed: latest stable version is %s, but you are on %s", [latest_stable_version, trimmedVersion]) {
-	true
+FAILURE_MESSAGE contains msg if {
+	not allow
+	msg := sprintf("Failed: latest stable version is %s, but you are on %s", [latest_stable_version, trimmedVersion])
 }

+ 8 - 5
internal/opa/policies/nginx/wait_shutdown.rego

@@ -1,6 +1,6 @@
 package nginx.wait_shutdown
 
-import future.keywords.if
+import future.keywords
 
 # Policy expects input structure of form:
 # values: {}
@@ -17,19 +17,22 @@ import future.keywords.if
 #           - '-c'
 #           - sleep 120 && /wait-shutdown
 
+POLICY_ID := "nginx_wait_shutdown"
+
 POLICY_VERSION := "v0.0.1"
 
 POLICY_SEVERITY := "high"
 
 POLICY_TITLE := sprintf("NGINX ingress controller should have a modified wait-shutdown script", [])
 
+POLICY_SUCCESS_MESSAGE := sprintf("Success: NGINX ingress controller has a properly modified wait-shutdown script set", [])
+
 allow if {
 	input.values.controller.lifecycle.preStop.exec.command
 	count(input.values.controller.lifecycle.preStop.exec.command) != 1
 }
 
-POLICY_MESSAGE := sprintf("Success: NGINX ingress controller has a properly modified wait-shutdown script set", []) if allow
-
-else := sprintf("Failed: NGINX ingress controller does not have a properly modified wait-shutdown script", []) {
-	true
+FAILURE_MESSAGE contains msg if {
+	not allow
+	msg := sprintf("Failed: NGINX ingress controller does not have a properly modified wait-shutdown script", [])
 }

+ 12 - 20
internal/opa/policies/pod/running.rego

@@ -8,39 +8,31 @@ import future.keywords.in
 # TODO: this file needs a lot of work to capture all pod statuses and container statuses. 
 # It currently only checks if a pod is in a "Running" status and if all containers are in
 # running status.
+POLICY_ID := "pod_running"
+
 POLICY_VERSION := "v0.0.1"
 
 POLICY_SEVERITY := "high"
 
 POLICY_TITLE := sprintf("Pod %s in namespace %s should be running", [input.metadata.name, input.metadata.namespace])
 
+POLICY_SUCCESS_MESSAGE := sprintf("Success: pod is running", [])
+
 allow if {
 	input.status.phase == "Running"
-	#   msg := sprintf("Pod '%s' is not running", [input.metadata.name])
-}
 
-allow if {
 	every containerStatus in input.status.containerStatuses {
 		containerStatus.state.running
 	}
-	#     msg := sprintf("Container '%s' in pod '%s' was not running", [containerStatus.state.running, input.metadata.name])
-
 }
 
-POLICY_MESSAGE := sprintf("Success: pod is running", []) if allow
-
-else := sprintf("Failed: the pod is not running", []) {
-	true
+FAILURE_MESSAGE contains msg1 if {
+	input.status.phase != "Running"
+	msg1 := sprintf("Pod %s does not have a Running status", [input.metadata.name])
 }
 
-# TODO: REWORK SO THAT FAILURE MESSAGES WILL LOOK SOMETHING LIKE:
-# POLICY_SUCCESS_MESSAGE := "The pod is running successfully"
-# failure contains msg1 if {
-#   input.status.phase != "Running"
-#   msg1 := sprintf("Pod '%s' is not running", [input.metadata.name])
-# }
-# failure contains msg2 if {
-#   some containerStatus in input.status.containerStatuses 
-#   not containerStatus.state.running
-#   msg2 := sprintf("Container '%s' in pod '%s' is not running", [containerStatus.name, input.metadata.name])
-# }
+FAILURE_MESSAGE contains msg2 if {
+	some containerStatus in input.status.containerStatuses
+	not containerStatus.state.running
+	msg2 := sprintf("Container %s in pod %s is not running", [containerStatus.name, input.metadata.name])
+}

+ 8 - 5
internal/opa/policies/prometheus/alertmanager_memory_limits.rego

@@ -1,6 +1,6 @@
 package prometheus.alertmanager_memory_limits
 
-import future.keywords.if
+import future.keywords
 
 # Policy expects input structure of form:
 # values: {}
@@ -17,18 +17,21 @@ import future.keywords.if
 #       cpu: 10m
 #       memory: 256Mi
 
+POLICY_ID := "alertmanager_memory_limits"
+
 POLICY_VERSION := "v0.0.1"
 
 POLICY_SEVERITY := "high"
 
 POLICY_TITLE := sprintf("Prometheus alert-manager should have memory limits set", [])
 
+POLICY_SUCCESS_MESSAGE := sprintf("Success: Prometheus alert-manager has memory limits set", [])
+
 allow if {
 	input.values.alertmanager.resources.limits.memory
 }
 
-POLICY_MESSAGE := sprintf("Success: Prometheus alert-manager has memory limits set", []) if allow
-
-else := sprintf("Failed: Prometheus alert-manager does not have memory limits set", []) {
-	true
+FAILURE_MESSAGE contains msg if {
+	not allow
+	msg := "Failed: Prometheus alert-manager does not have memory limits set"
 }

+ 8 - 5
internal/opa/policies/prometheus/kubestatemetrics_memory_limits.rego

@@ -1,6 +1,6 @@
 package prometheus.kubestatemetrics_memory_limits
 
-import future.keywords.if
+import future.keywords
 
 # Policy expects input structure of form:
 # values: {}
@@ -17,18 +17,21 @@ import future.keywords.if
 #       cpu: 10m
 #       memory: 256Mi
 
+POLICY_ID := "kubestatemetrics_memory_limits"
+
 POLICY_VERSION := "v0.0.1"
 
 POLICY_SEVERITY := "high"
 
 POLICY_TITLE := sprintf("Prometheus kube-state-metrics should have memory limits set", [])
 
+POLICY_SUCCESS_MESSAGE := sprintf("Success: Prometheus kube-state-metrics has memory limits set", [])
+
 allow if {
 	input.values["kube-state-metrics"].resources.limits.memory
 }
 
-POLICY_MESSAGE := sprintf("Success: Prometheus kube-state-metrics has memory limits set", []) if allow
-
-else := sprintf("Failed: Prometheus kube-state-metrics does not have memory limits set", []) {
-	true
+FAILURE_MESSAGE contains msg if {
+	not allow
+	msg := "Failed: Prometheus kube-state-metrics does not have memory limits set"
 }

+ 9 - 6
internal/opa/policies/prometheus/nodeexporter_memory_limits.rego

@@ -1,6 +1,6 @@
 package prometheus.nodeexporter_memory_limits
 
-import future.keywords.if
+import future.keywords
 
 # Policy expects input structure of form:
 # values: {}
@@ -8,7 +8,7 @@ import future.keywords.if
 # This policy tests for the existence of memory limits as a hard constraint. We look
 # for Helm values of the form:
 # 
-# pushgateway:
+# nodeExporter:
 #   resources:
 #     limits:
 #       cpu: 200m
@@ -17,18 +17,21 @@ import future.keywords.if
 #       cpu: 10m
 #       memory: 256Mi
 
+POLICY_ID := "nodeexporter_memory_limits"
+
 POLICY_VERSION := "v0.0.1"
 
 POLICY_SEVERITY := "high"
 
 POLICY_TITLE := sprintf("Prometheus nodeExporter should have memory limits set", [])
 
+POLICY_SUCCESS_MESSAGE := sprintf("Success: Prometheus nodeExporter has memory limits set", [])
+
 allow if {
 	input.values.nodeExporter.resources.limits.memory
 }
 
-POLICY_MESSAGE := sprintf("Success: Prometheus nodeExporter has memory limits set", []) if allow
-
-else := sprintf("Failed: Prometheus nodeExporter does not have memory limits set", []) {
-	true
+FAILURE_MESSAGE contains msg if {
+	not allow
+	msg := "Failed: Prometheus nodeExporter does not have memory limits set"
 }

+ 8 - 5
internal/opa/policies/prometheus/pushgateway_memory_limits.rego

@@ -1,6 +1,6 @@
 package prometheus.pushgateway_memory_limits
 
-import future.keywords.if
+import future.keywords
 
 # Policy expects input structure of form:
 # values: {}
@@ -17,18 +17,21 @@ import future.keywords.if
 #       cpu: 10m
 #       memory: 256Mi
 
+POLICY_ID := "pushgateway_memory_limits"
+
 POLICY_VERSION := "v0.0.1"
 
 POLICY_SEVERITY := "high"
 
 POLICY_TITLE := sprintf("Prometheus pushgateway should have memory limits set", [])
 
+POLICY_SUCCESS_MESSAGE := sprintf("Success: Prometheus pushgateway has memory limits set", [])
+
 allow if {
 	input.values.pushgateway.resources.limits.memory
 }
 
-POLICY_MESSAGE := sprintf("Success: Prometheus pushgateway has memory limits set", []) if allow
-
-else := sprintf("Failed: Prometheus pushgateway does not have memory limits set", []) {
-	true
+FAILURE_MESSAGE contains msg if {
+	not allow
+	msg := "Failed: Prometheus pushgateway does not have memory limits set"
 }

+ 8 - 5
internal/opa/policies/prometheus/server_memory_limits.rego

@@ -1,6 +1,6 @@
 package prometheus.server_memory_limits
 
-import future.keywords.if
+import future.keywords
 
 # Policy expects input structure of form:
 # values: {}
@@ -17,18 +17,21 @@ import future.keywords.if
 #       cpu: 100m
 #       memory: 400Mi
 
+POLICY_ID := "server_memory_limits"
+
 POLICY_VERSION := "v0.0.1"
 
 POLICY_SEVERITY := "high"
 
 POLICY_TITLE := sprintf("Prometheus server should have memory limits set", [])
 
+POLICY_SUCCESS_MESSAGE := sprintf("Success: Prometheus server has memory limits set", [])
+
 allow if {
 	input.values.server.resources.limits.memory
 }
 
-POLICY_MESSAGE := sprintf("Success: Prometheus server has memory limits set", []) if allow
-
-else := sprintf("Failed: Prometheus server does not have memory limits set", []) {
-	true
+FAILURE_MESSAGE contains msg if {
+	not allow
+	msg := "Failed: Prometheus server does not have memory limits set"
 }

+ 8 - 5
internal/opa/policies/web/web_version.rego

@@ -1,6 +1,8 @@
 package web.version
 
-import future.keywords.if
+import future.keywords
+
+POLICY_ID := "web_version"
 
 POLICY_VERSION := "v0.0.1"
 
@@ -11,13 +13,14 @@ latest_stable_version := "0.115.0"
 
 POLICY_TITLE := sprintf("The web version should be at least v%s", [latest_stable_version])
 
+POLICY_SUCCESS_MESSAGE := sprintf("Success: web version is up-to-date", [])
+
 trimmedVersion := trim_left(input.version, "v")
 
 # semver.compare returns -1 if latest_stable_version < trimmedVersion
 allow if semver.compare(latest_stable_version, trimmedVersion) == -1
 
-POLICY_MESSAGE := sprintf("Success: web version is up-to-date", []) if allow
-
-else := sprintf("Failed: latest stable version is %s, but you are on %s", [latest_stable_version, trimmedVersion]) {
-	true
+FAILURE_MESSAGE contains msg if {
+	not allow
+	msg := sprintf("Failed: latest stable version is %s, but you are on %s", [latest_stable_version, trimmedVersion])
 }

+ 13 - 1
workers/jobs/recommender.go

@@ -153,7 +153,19 @@ func (n *recommender) Run() error {
 		return err
 	}
 
-	runner := opa.NewRunner(n.policies, k8sAgent)
+	dynamicClient, err := kubernetes.GetDynamicClientOutOfClusterConfig(&kubernetes.OutOfClusterConfig{
+		Cluster:                   cluster,
+		Repo:                      n.repo,
+		DigitalOceanOAuth:         n.doConf,
+		AllowInClusterConnections: false,
+	})
+
+	if err != nil {
+		log.Printf("error getting dynamic client for cluster ID %d: %v. skipping cluster ...", n.clusterID, err)
+		return err
+	}
+
+	runner := opa.NewRunner(n.policies, k8sAgent, dynamicClient)
 
 	queryResults, err := runner.GetRecommendationsByName(n.collectionName)