diff --git a/artifacthub/library/general/requiredprobes/1.1.0/artifacthub-pkg.yml b/artifacthub/library/general/requiredprobes/1.1.0/artifacthub-pkg.yml new file mode 100644 index 000000000..4f3f1deba --- /dev/null +++ b/artifacthub/library/general/requiredprobes/1.1.0/artifacthub-pkg.yml @@ -0,0 +1,22 @@ +version: 1.1.0 +name: k8srequiredprobes +displayName: Required Probes +createdAt: "2023-01-04T23:20:17Z" +description: Requires Pods to have readiness and/or liveness probes. +digest: 1f871f3eb3e25749d3d6872736253ae2cd2dc394ed9b402bb1560cdd28613666 +license: Apache-2.0 +homeURL: https://open-policy-agent.github.io/gatekeeper-library/website/requiredprobes +keywords: + - gatekeeper + - open-policy-agent + - policies +readme: |- + # Required Probes + Requires Pods to have readiness and/or liveness probes. +install: |- + ### Usage + ```shell + kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/artifacthub/library/general/requiredprobes/1.1.0/template.yaml + ``` +provider: + name: Gatekeeper Library diff --git a/artifacthub/library/general/requiredprobes/1.1.0/kustomization.yaml b/artifacthub/library/general/requiredprobes/1.1.0/kustomization.yaml new file mode 100644 index 000000000..7d70d11b7 --- /dev/null +++ b/artifacthub/library/general/requiredprobes/1.1.0/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - template.yaml diff --git a/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes-on-service/constraint.yaml b/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes-on-service/constraint.yaml new file mode 100644 index 000000000..d736aa4c0 --- /dev/null +++ b/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes-on-service/constraint.yaml @@ -0,0 +1,15 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sRequiredProbes +metadata: + name: must-have-probes-on-service +spec: + enforcementAction: warn + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + onlyServices: true + probes: ["readinessProbe", "livenessProbe"] + probeTypes: ["tcpSocket", "httpGet", "exec"] + customViolationMessage: "See https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes for more info." diff --git a/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes-on-service/example_allowed_with_service.yaml b/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes-on-service/example_allowed_with_service.yaml new file mode 100644 index 000000000..b333d912f --- /dev/null +++ b/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes-on-service/example_allowed_with_service.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: Pod +metadata: + name: test-pod1 + namespace: default + labels: + app.kubernetes.io/name: tomcat +spec: + containers: + - name: tomcat + image: tomcat + ports: + - containerPort: 8080 + name: tomcat-http + livenessProbe: + tcpSocket: + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 + readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: cache-volume + emptyDir: {} diff --git a/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes-on-service/example_allowed_without_service.yaml b/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes-on-service/example_allowed_without_service.yaml new file mode 100644 index 000000000..2c8f3a84f --- /dev/null +++ b/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes-on-service/example_allowed_without_service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: test-pod1 + namespace: default + labels: + app.kubernetes.io/name: tomcat-no-svc + second-label: "example" +spec: + containers: + - name: tomcat + image: tomcat + ports: + - containerPort: 8080 + volumes: + - name: cache-volume + emptyDir: {} diff --git a/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes-on-service/example_disallowed_with_service.yaml b/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes-on-service/example_disallowed_with_service.yaml new file mode 100644 index 000000000..662741368 --- /dev/null +++ b/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes-on-service/example_disallowed_with_service.yaml @@ -0,0 +1,35 @@ +apiVersion: v1 +kind: Pod +metadata: + name: test-pod1 + namespace: default + labels: + app.kubernetes.io/name: tomcat + second-label: "example" +spec: + containers: + - name: nginx-1 + image: nginx:1.7.9 + ports: + - containerPort: 80 + livenessProbe: + # tcpSocket: + # port: 80 + # initialDelaySeconds: 5 + # periodSeconds: 10 + volumeMounts: + - mountPath: /tmp/cache + name: cache-volume + - name: tomcat + image: tomcat + ports: + - containerPort: 8080 + name: tomcat-http + readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: cache-volume + emptyDir: {} diff --git a/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes-on-service/inventory.yaml b/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes-on-service/inventory.yaml new file mode 100644 index 000000000..b32e5c6a1 --- /dev/null +++ b/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes-on-service/inventory.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: tomcat-service + namespace: default +spec: + selector: + app.kubernetes.io/name: tomcat + ports: + - name: name-of-service-port + protocol: TCP + port: 80 + targetPort: tomcat-http diff --git a/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes/constraint.yaml b/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes/constraint.yaml new file mode 100644 index 000000000..f4d526b16 --- /dev/null +++ b/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes/constraint.yaml @@ -0,0 +1,14 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sRequiredProbes +metadata: + name: must-have-probes +spec: + enforcementAction: warn + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + onlyServices: false + probes: ["readinessProbe", "livenessProbe"] + probeTypes: ["tcpSocket", "httpGet", "exec"] diff --git a/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes/example_allowed.yaml b/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes/example_allowed.yaml new file mode 100644 index 000000000..4248b67dd --- /dev/null +++ b/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes/example_allowed.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Pod +metadata: + name: test-pod1 +spec: + containers: + - name: tomcat + image: tomcat + ports: + - containerPort: 8080 + livenessProbe: + tcpSocket: + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 + readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: cache-volume + emptyDir: {} diff --git a/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes/example_disallowed.yaml b/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes/example_disallowed.yaml new file mode 100644 index 000000000..6db251904 --- /dev/null +++ b/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes/example_disallowed.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: Pod +metadata: + name: test-pod1 +spec: + containers: + - name: nginx-1 + image: nginx:1.7.9 + ports: + - containerPort: 80 + livenessProbe: + # tcpSocket: + # port: 80 + # initialDelaySeconds: 5 + # periodSeconds: 10 + volumeMounts: + - mountPath: /tmp/cache + name: cache-volume + - name: tomcat + image: tomcat + ports: + - containerPort: 8080 + readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: cache-volume + emptyDir: {} diff --git a/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes/example_disallowed2.yaml b/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes/example_disallowed2.yaml new file mode 100644 index 000000000..6e0536487 --- /dev/null +++ b/artifacthub/library/general/requiredprobes/1.1.0/samples/must-have-probes/example_disallowed2.yaml @@ -0,0 +1,41 @@ +apiVersion: v1 +kind: Pod +metadata: + name: test-pod2 +spec: + containers: + - name: nginx-1 + image: nginx:1.7.9 + ports: + - containerPort: 80 + readinessProbe: + # httpGet: + # path: / + # port: 80 + # initialDelaySeconds: 5 + # periodSeconds: 10 + livenessProbe: + tcpSocket: + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 + volumeMounts: + - mountPath: /tmp/cache + name: cache-volume + - name: tomcat + image: tomcat + ports: + - containerPort: 8080 + readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + # livenessProbe: + # tcpSocket: + # port: 8080 + # initialDelaySeconds: 5 + # periodSeconds: 10 + volumes: + - name: cache-volume + emptyDir: {} diff --git a/artifacthub/library/general/requiredprobes/1.1.0/suite.yaml b/artifacthub/library/general/requiredprobes/1.1.0/suite.yaml new file mode 100644 index 000000000..4aab97420 --- /dev/null +++ b/artifacthub/library/general/requiredprobes/1.1.0/suite.yaml @@ -0,0 +1,43 @@ +kind: Suite +apiVersion: test.gatekeeper.sh/v1alpha1 +metadata: + name: containerprobes +tests: +- name: container-probes + template: template.yaml + constraint: samples/must-have-probes/constraint.yaml + cases: + - name: example-allowed + object: samples/must-have-probes/example_allowed.yaml + assertions: + - violations: no + - name: example-disallowed + object: samples/must-have-probes/example_disallowed.yaml + assertions: + - violations: yes + - name: example-disallowed2 + object: samples/must-have-probes/example_disallowed2.yaml + assertions: + - violations: yes +- name: container-probes-only-services + template: template.yaml + constraint: samples/must-have-probes-on-service/constraint.yaml + cases: + - name: example-allowed-without-service + object: samples/must-have-probes-on-service/example_allowed_without_service.yaml + inventory: + - samples/must-have-probes-on-service/inventory.yaml + assertions: + - violations: no + - name: example-allowed-with-service + object: samples/must-have-probes-on-service/example_allowed_with_service.yaml + inventory: + - samples/must-have-probes-on-service/inventory.yaml + assertions: + - violations: no + - name: example-disallowed-with-service + object: samples/must-have-probes-on-service/example_disallowed_with_service.yaml + inventory: + - samples/must-have-probes-on-service/inventory.yaml + assertions: + - violations: yes diff --git a/artifacthub/library/general/requiredprobes/1.1.0/template.yaml b/artifacthub/library/general/requiredprobes/1.1.0/template.yaml new file mode 100644 index 000000000..b61c3bfdc --- /dev/null +++ b/artifacthub/library/general/requiredprobes/1.1.0/template.yaml @@ -0,0 +1,101 @@ +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8srequiredprobes + annotations: + metadata.gatekeeper.sh/title: "Required Probes" + metadata.gatekeeper.sh/version: 1.1.0 + metadata.gatekeeper.sh/requiresSyncData: | + "[ + [ + { + "groups":[""], + "versions": ["v1"], + "kinds": ["Service"] + } + ] + ]" + description: Requires Pods to have readiness and/or liveness probes. +spec: + crd: + spec: + names: + kind: K8sRequiredProbes + validation: + openAPIV3Schema: + type: object + properties: + onlyServices: + description: "Only apply to pods that are selected by a service" + type: boolean + probes: + description: "A list of probes that are required (ex: `readinessProbe`)" + type: array + items: + type: string + probeTypes: + description: "The probe must define a field listed in `probeType` in order to satisfy the constraint (ex. `tcpSocket` satisfies `['tcpSocket', 'exec']`)" + type: array + items: + type: string + customViolationMessage: + type: string + description: >- + Custom error message generated by a violation that is appended to the standard violation message + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8srequiredprobes + + probe_type_set = probe_types { + probe_types := {type | type := input.parameters.probeTypes[_]} + } + + violation[{"msg": msg}] { + not input.parameters.onlyServices + container := input.review.object.spec.containers[_] + probe := input.parameters.probes[_] + probe_is_missing(container, probe) + custom_msg := object.get(input.parameters, "customViolationMessage", "") + msg := trim(sprintf("Container <%v> in this <%v> has no <%v>. %v", [container.name, input.review.kind.kind, probe, custom_msg]), " ") + } + + violation[{"msg": msg}] { + input.parameters.onlyServices + container := input.review.object.spec.containers[_] + probe := input.parameters.probes[_] + probe_is_missing(container, probe) + + obj := input.review.object + svc := data.inventory.namespace[obj.metadata.namespace]["v1"]["Service"][_] + matchLabels := { [label, value] | some label; value := svc.spec.selector[label] } + labels := { [label, value] | some label; value := obj.metadata.labels[label] } + count(matchLabels - labels) == 0 + matching_ports := [p | p := svc.spec.ports[_].targetPort; has_port(p, container)] + count(matching_ports) > 0 + + custom_msg := object.get(input.parameters, "customViolationMessage", "") + msg := trim(sprintf("Container <%v> in this <%v> has no <%v> and is selected by service <%v> with targetPort(s) %v. %v", [container.name, input.review.kind.kind, probe, svc.metadata.name, matching_ports, custom_msg]), " ") + } + + has_port(targetPort, container){ + targetPort == container.ports[_].containerPort + } + + has_port(targetPort, container){ + targetPort == container.ports[_].name + } + + probe_is_missing(ctr, probe) = true { + not ctr[probe] + } + + probe_is_missing(ctr, probe) = true { + probe_field_empty(ctr, probe) + } + + probe_field_empty(ctr, probe) = true { + probe_fields := {field | ctr[probe][field]} + diff_fields := probe_type_set - probe_fields + count(diff_fields) == count(probe_type_set) + } diff --git a/library/general/requiredprobes/samples/must-have-probes-on-service/constraint.yaml b/library/general/requiredprobes/samples/must-have-probes-on-service/constraint.yaml new file mode 100644 index 000000000..d736aa4c0 --- /dev/null +++ b/library/general/requiredprobes/samples/must-have-probes-on-service/constraint.yaml @@ -0,0 +1,15 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sRequiredProbes +metadata: + name: must-have-probes-on-service +spec: + enforcementAction: warn + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + onlyServices: true + probes: ["readinessProbe", "livenessProbe"] + probeTypes: ["tcpSocket", "httpGet", "exec"] + customViolationMessage: "See https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes for more info." diff --git a/library/general/requiredprobes/samples/must-have-probes-on-service/example_allowed_with_service.yaml b/library/general/requiredprobes/samples/must-have-probes-on-service/example_allowed_with_service.yaml new file mode 100644 index 000000000..b333d912f --- /dev/null +++ b/library/general/requiredprobes/samples/must-have-probes-on-service/example_allowed_with_service.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: Pod +metadata: + name: test-pod1 + namespace: default + labels: + app.kubernetes.io/name: tomcat +spec: + containers: + - name: tomcat + image: tomcat + ports: + - containerPort: 8080 + name: tomcat-http + livenessProbe: + tcpSocket: + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 + readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: cache-volume + emptyDir: {} diff --git a/library/general/requiredprobes/samples/must-have-probes-on-service/example_allowed_without_service.yaml b/library/general/requiredprobes/samples/must-have-probes-on-service/example_allowed_without_service.yaml new file mode 100644 index 000000000..2c8f3a84f --- /dev/null +++ b/library/general/requiredprobes/samples/must-have-probes-on-service/example_allowed_without_service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: test-pod1 + namespace: default + labels: + app.kubernetes.io/name: tomcat-no-svc + second-label: "example" +spec: + containers: + - name: tomcat + image: tomcat + ports: + - containerPort: 8080 + volumes: + - name: cache-volume + emptyDir: {} diff --git a/library/general/requiredprobes/samples/must-have-probes-on-service/example_disallowed_with_service.yaml b/library/general/requiredprobes/samples/must-have-probes-on-service/example_disallowed_with_service.yaml new file mode 100644 index 000000000..662741368 --- /dev/null +++ b/library/general/requiredprobes/samples/must-have-probes-on-service/example_disallowed_with_service.yaml @@ -0,0 +1,35 @@ +apiVersion: v1 +kind: Pod +metadata: + name: test-pod1 + namespace: default + labels: + app.kubernetes.io/name: tomcat + second-label: "example" +spec: + containers: + - name: nginx-1 + image: nginx:1.7.9 + ports: + - containerPort: 80 + livenessProbe: + # tcpSocket: + # port: 80 + # initialDelaySeconds: 5 + # periodSeconds: 10 + volumeMounts: + - mountPath: /tmp/cache + name: cache-volume + - name: tomcat + image: tomcat + ports: + - containerPort: 8080 + name: tomcat-http + readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: cache-volume + emptyDir: {} diff --git a/library/general/requiredprobes/samples/must-have-probes-on-service/inventory.yaml b/library/general/requiredprobes/samples/must-have-probes-on-service/inventory.yaml new file mode 100644 index 000000000..b32e5c6a1 --- /dev/null +++ b/library/general/requiredprobes/samples/must-have-probes-on-service/inventory.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: tomcat-service + namespace: default +spec: + selector: + app.kubernetes.io/name: tomcat + ports: + - name: name-of-service-port + protocol: TCP + port: 80 + targetPort: tomcat-http diff --git a/library/general/requiredprobes/samples/must-have-probes/constraint.yaml b/library/general/requiredprobes/samples/must-have-probes/constraint.yaml index 84fde016a..f4d526b16 100644 --- a/library/general/requiredprobes/samples/must-have-probes/constraint.yaml +++ b/library/general/requiredprobes/samples/must-have-probes/constraint.yaml @@ -3,10 +3,12 @@ kind: K8sRequiredProbes metadata: name: must-have-probes spec: + enforcementAction: warn match: kinds: - apiGroups: [""] kinds: ["Pod"] parameters: + onlyServices: false probes: ["readinessProbe", "livenessProbe"] probeTypes: ["tcpSocket", "httpGet", "exec"] diff --git a/library/general/requiredprobes/suite.yaml b/library/general/requiredprobes/suite.yaml index 379e77563..4aab97420 100644 --- a/library/general/requiredprobes/suite.yaml +++ b/library/general/requiredprobes/suite.yaml @@ -1,9 +1,9 @@ kind: Suite apiVersion: test.gatekeeper.sh/v1alpha1 metadata: - name: requiredprobes + name: containerprobes tests: -- name: block-endpoint-default-role +- name: container-probes template: template.yaml constraint: samples/must-have-probes/constraint.yaml cases: @@ -19,3 +19,25 @@ tests: object: samples/must-have-probes/example_disallowed2.yaml assertions: - violations: yes +- name: container-probes-only-services + template: template.yaml + constraint: samples/must-have-probes-on-service/constraint.yaml + cases: + - name: example-allowed-without-service + object: samples/must-have-probes-on-service/example_allowed_without_service.yaml + inventory: + - samples/must-have-probes-on-service/inventory.yaml + assertions: + - violations: no + - name: example-allowed-with-service + object: samples/must-have-probes-on-service/example_allowed_with_service.yaml + inventory: + - samples/must-have-probes-on-service/inventory.yaml + assertions: + - violations: no + - name: example-disallowed-with-service + object: samples/must-have-probes-on-service/example_disallowed_with_service.yaml + inventory: + - samples/must-have-probes-on-service/inventory.yaml + assertions: + - violations: yes diff --git a/library/general/requiredprobes/sync.yaml b/library/general/requiredprobes/sync.yaml new file mode 100644 index 000000000..433294b0f --- /dev/null +++ b/library/general/requiredprobes/sync.yaml @@ -0,0 +1,11 @@ +apiVersion: config.gatekeeper.sh/v1alpha1 +kind: Config +metadata: + name: config + namespace: "gatekeeper-system" +spec: + sync: + syncOnly: + - group: "" + version: "v1" + kind: "Service" diff --git a/library/general/requiredprobes/template.yaml b/library/general/requiredprobes/template.yaml index 26417b101..b61c3bfdc 100644 --- a/library/general/requiredprobes/template.yaml +++ b/library/general/requiredprobes/template.yaml @@ -4,7 +4,17 @@ metadata: name: k8srequiredprobes annotations: metadata.gatekeeper.sh/title: "Required Probes" - metadata.gatekeeper.sh/version: 1.0.0 + metadata.gatekeeper.sh/version: 1.1.0 + metadata.gatekeeper.sh/requiresSyncData: | + "[ + [ + { + "groups":[""], + "versions": ["v1"], + "kinds": ["Service"] + } + ] + ]" description: Requires Pods to have readiness and/or liveness probes. spec: crd: @@ -15,6 +25,9 @@ spec: openAPIV3Schema: type: object properties: + onlyServices: + description: "Only apply to pods that are selected by a service" + type: boolean probes: description: "A list of probes that are required (ex: `readinessProbe`)" type: array @@ -25,6 +38,10 @@ spec: type: array items: type: string + customViolationMessage: + type: string + description: >- + Custom error message generated by a violation that is appended to the standard violation message targets: - target: admission.k8s.gatekeeper.sh rego: | @@ -35,10 +52,38 @@ spec: } violation[{"msg": msg}] { + not input.parameters.onlyServices container := input.review.object.spec.containers[_] probe := input.parameters.probes[_] probe_is_missing(container, probe) - msg := get_violation_message(container, input.review, probe) + custom_msg := object.get(input.parameters, "customViolationMessage", "") + msg := trim(sprintf("Container <%v> in this <%v> has no <%v>. %v", [container.name, input.review.kind.kind, probe, custom_msg]), " ") + } + + violation[{"msg": msg}] { + input.parameters.onlyServices + container := input.review.object.spec.containers[_] + probe := input.parameters.probes[_] + probe_is_missing(container, probe) + + obj := input.review.object + svc := data.inventory.namespace[obj.metadata.namespace]["v1"]["Service"][_] + matchLabels := { [label, value] | some label; value := svc.spec.selector[label] } + labels := { [label, value] | some label; value := obj.metadata.labels[label] } + count(matchLabels - labels) == 0 + matching_ports := [p | p := svc.spec.ports[_].targetPort; has_port(p, container)] + count(matching_ports) > 0 + + custom_msg := object.get(input.parameters, "customViolationMessage", "") + msg := trim(sprintf("Container <%v> in this <%v> has no <%v> and is selected by service <%v> with targetPort(s) %v. %v", [container.name, input.review.kind.kind, probe, svc.metadata.name, matching_ports, custom_msg]), " ") + } + + has_port(targetPort, container){ + targetPort == container.ports[_].containerPort + } + + has_port(targetPort, container){ + targetPort == container.ports[_].name } probe_is_missing(ctr, probe) = true { @@ -54,7 +99,3 @@ spec: diff_fields := probe_type_set - probe_fields count(diff_fields) == count(probe_type_set) } - - get_violation_message(container, review, probe) = msg { - msg := sprintf("Container <%v> in your <%v> <%v> has no <%v>", [container.name, review.kind.kind, review.object.metadata.name, probe]) - } diff --git a/src/general/requiredprobes/constraint.tmpl b/src/general/requiredprobes/constraint.tmpl index 6b81857dc..2d18af595 100644 --- a/src/general/requiredprobes/constraint.tmpl +++ b/src/general/requiredprobes/constraint.tmpl @@ -4,7 +4,17 @@ metadata: name: k8srequiredprobes annotations: metadata.gatekeeper.sh/title: "Required Probes" - metadata.gatekeeper.sh/version: 1.0.0 + metadata.gatekeeper.sh/version: 1.1.0 + metadata.gatekeeper.sh/requiresSyncData: | + "[ + [ + { + "groups":[""], + "versions": ["v1"], + "kinds": ["Service"] + } + ] + ]" description: Requires Pods to have readiness and/or liveness probes. spec: crd: @@ -15,6 +25,9 @@ spec: openAPIV3Schema: type: object properties: + onlyServices: + description: "Only apply to pods that are selected by a service" + type: boolean probes: description: "A list of probes that are required (ex: `readinessProbe`)" type: array @@ -25,6 +38,10 @@ spec: type: array items: type: string + customViolationMessage: + type: string + description: >- + Custom error message generated by a violation that is appended to the standard violation message targets: - target: admission.k8s.gatekeeper.sh rego: | diff --git a/src/general/requiredprobes/src.rego b/src/general/requiredprobes/src.rego index 532b036d7..9b09e3f24 100644 --- a/src/general/requiredprobes/src.rego +++ b/src/general/requiredprobes/src.rego @@ -5,10 +5,38 @@ probe_type_set = probe_types { } violation[{"msg": msg}] { + not input.parameters.onlyServices container := input.review.object.spec.containers[_] probe := input.parameters.probes[_] probe_is_missing(container, probe) - msg := get_violation_message(container, input.review, probe) + custom_msg := object.get(input.parameters, "customViolationMessage", "") + msg := trim(sprintf("Container <%v> in this <%v> has no <%v>. %v", [container.name, input.review.kind.kind, probe, custom_msg]), " ") +} + +violation[{"msg": msg}] { + input.parameters.onlyServices + container := input.review.object.spec.containers[_] + probe := input.parameters.probes[_] + probe_is_missing(container, probe) + + obj := input.review.object + svc := data.inventory.namespace[obj.metadata.namespace]["v1"]["Service"][_] + matchLabels := { [label, value] | some label; value := svc.spec.selector[label] } + labels := { [label, value] | some label; value := obj.metadata.labels[label] } + count(matchLabels - labels) == 0 + matching_ports := [p | p := svc.spec.ports[_].targetPort; has_port(p, container)] + count(matching_ports) > 0 + + custom_msg := object.get(input.parameters, "customViolationMessage", "") + msg := trim(sprintf("Container <%v> in this <%v> has no <%v> and is selected by service <%v> with targetPort(s) %v. %v", [container.name, input.review.kind.kind, probe, svc.metadata.name, matching_ports, custom_msg]), " ") +} + +has_port(targetPort, container){ + targetPort == container.ports[_].containerPort +} + +has_port(targetPort, container){ + targetPort == container.ports[_].name } probe_is_missing(ctr, probe) = true { @@ -24,7 +52,3 @@ probe_field_empty(ctr, probe) = true { diff_fields := probe_type_set - probe_fields count(diff_fields) == count(probe_type_set) } - -get_violation_message(container, review, probe) = msg { - msg := sprintf("Container <%v> in your <%v> <%v> has no <%v>", [container.name, review.kind.kind, review.object.metadata.name, probe]) -} diff --git a/src/general/requiredprobes/src_test.rego b/src/general/requiredprobes/src_test.rego index a860b2e46..02eff17d2 100644 --- a/src/general/requiredprobes/src_test.rego +++ b/src/general/requiredprobes/src_test.rego @@ -335,6 +335,60 @@ test_two_ctrs_empty_liveness_in_ctr_two_both_empty_probes_in_ctr_one { count(results) == 3 } +test_one_ctr_readiness_violation_with_svc_port_name { + kind := kinds[_] + input := {"review": review([{"name": "my-container1","image": "my-image:latest", "ports": [{ "name": "http", "containerPort": "8080"}], "livenessProbe": {"tcpSocket": {"port":80}}}]), + "parameters": parameters_only_svc} + inv := inv_svc({"app.kubernetes.io/name": "test"}, [{ "name": "name-of-service-port", "port": "80", "targetPort": "http"}]) + results := violation with input as input with data.inventory as inv + count(results) == 1 +} + +test_one_ctr_readiness_violation_with_svc_port_num { + kind := kinds[_] + input := {"review": review([{"name": "my-container1","image": "my-image:latest", "ports": [{ "name": "http", "containerPort": "8080"}], "livenessProbe": {"tcpSocket": {"port":8080}}}]), + "parameters": parameters_only_svc} + inv := inv_svc({"app.kubernetes.io/name": "test"}, [{ "name": "name-of-service-port", "port": "80", "targetPort": "8080"}]) + results := violation with input as input with data.inventory as inv + count(results) == 1 +} + +test_one_ctr_readiness_violation_with_svc_multiple_port_num { + kind := kinds[_] + input := {"review": review([{"name": "my-container1","image": "my-image:latest", "ports": [{ "name": "http", "containerPort": "8080"}, { "name": "https", "containerPort": "8443"}], "livenessProbe": {"tcpSocket": {"port":8080}}}]), + "parameters": parameters_only_svc} + inv := inv_svc({"app.kubernetes.io/name": "test"}, [{ "name": "name-of-service-port", "port": "80", "targetPort": "8080"}, { "name": "name-of-service-port", "port": "443", "targetPort": "8443"}]) + results := violation with input as input with data.inventory as inv + count(results) == 1 +} + +test_one_ctr_no_violation_with_svc_port_name { + kind := kinds[_] + input := {"review": review([{"name": "my-container1","image": "my-image:latest", "ports": [{ "name": "http", "containerPort": "8080"}], "readinessProbe": {"tcpSocket": {"port":8080}}, "livenessProbe": {"tcpSocket": {"port":8080}}}]), + "parameters": parameters_only_svc} + inv := inv_svc({"app.kubernetes.io/name": "test"}, [{ "name": "name-of-service-port", "port": "80", "targetPort": "http"}]) + results := violation with input as input with data.inventory as inv + count(results) == 0 +} + +test_one_ctr_no_violation_with_svc_port_num { + kind := kinds[_] + input := {"review": review([{"name": "my-container1","image": "my-image:latest", "ports": [{ "name": "http", "containerPort": "8080"}], "readinessProbe": {"tcpSocket": {"port":8080}}, "livenessProbe": {"tcpSocket": {"port":8080}}}]), + "parameters": parameters_only_svc} + inv := inv_svc({"app.kubernetes.io/name": "test"}, [{ "name": "name-of-service-port", "port": "80", "targetPort": "8080"}]) + results := violation with input as input with data.inventory as inv + count(results) == 0 +} + +test_one_ctr_missing_both_no_violation_without_svc_port_num { + kind := kinds[_] + input := {"review": review([{"name": "my-container1","image": "my-image:latest", "ports": [{ "name": "http", "containerPort": "8080"}]}]), + "parameters": parameters_only_svc} + inv := inv_svc({"app.kubernetes.io/name": "non-matching-pod-selector"}, [{ "name": "name-of-service-port", "port": "80", "targetPort": "8080"}]) + results := violation with input as input with data.inventory as inv + count(results) == 0 +} + review(containers) = obj { obj = { "kind": { @@ -342,14 +396,44 @@ review(containers) = obj { }, "object": { "metadata": { - "name": "some-name" + "name": "some-name", + "namespace": namespace, + "labels": { + "app.kubernetes.io/name": "test" + } }, "spec": { - "containers":containers + "containers": containers } } } } +svc_out(selector, ports) = output { + output := { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "name": "example-service", + "namespace": namespace, + }, + "spec": { + "selector": selector, + "ports": ports, + }, + } +} + +inventory(obj) = output { + output := {"namespace": {namespace: {obj.apiVersion: {obj.kind: [obj]}}}} +} + +inv_svc(selector, ports) = output { + svc = svc_out(selector, ports) + output := inventory(svc) +} + +namespace := "default" parameters = {"probes": ["readinessProbe", "livenessProbe"], "probeTypes": ["tcpSocket", "httpGet", "exec"]} +parameters_only_svc = {"onlyServices": true, "probes": ["readinessProbe", "livenessProbe"], "probeTypes": ["tcpSocket", "httpGet", "exec"]} kinds = ["Pod"] diff --git a/website/docs/requiredprobes.md b/website/docs/requiredprobes.md index a09a05930..4516a17d3 100644 --- a/website/docs/requiredprobes.md +++ b/website/docs/requiredprobes.md @@ -16,7 +16,17 @@ metadata: name: k8srequiredprobes annotations: metadata.gatekeeper.sh/title: "Required Probes" - metadata.gatekeeper.sh/version: 1.0.0 + metadata.gatekeeper.sh/version: 1.1.0 + metadata.gatekeeper.sh/requiresSyncData: | + "[ + [ + { + "groups":[""], + "versions": ["v1"], + "kinds": ["Service"] + } + ] + ]" description: Requires Pods to have readiness and/or liveness probes. spec: crd: @@ -27,6 +37,9 @@ spec: openAPIV3Schema: type: object properties: + onlyServices: + description: "Only apply to pods that are selected by a service" + type: boolean probes: description: "A list of probes that are required (ex: `readinessProbe`)" type: array @@ -37,6 +50,10 @@ spec: type: array items: type: string + customViolationMessage: + type: string + description: >- + Custom error message generated by a violation that is appended to the standard violation message targets: - target: admission.k8s.gatekeeper.sh rego: | @@ -47,10 +64,38 @@ spec: } violation[{"msg": msg}] { + not input.parameters.onlyServices container := input.review.object.spec.containers[_] probe := input.parameters.probes[_] probe_is_missing(container, probe) - msg := get_violation_message(container, input.review, probe) + custom_msg := object.get(input.parameters, "customViolationMessage", "") + msg := trim(sprintf("Container <%v> in this <%v> has no <%v>. %v", [container.name, input.review.kind.kind, probe, custom_msg]), " ") + } + + violation[{"msg": msg}] { + input.parameters.onlyServices + container := input.review.object.spec.containers[_] + probe := input.parameters.probes[_] + probe_is_missing(container, probe) + + obj := input.review.object + svc := data.inventory.namespace[obj.metadata.namespace]["v1"]["Service"][_] + matchLabels := { [label, value] | some label; value := svc.spec.selector[label] } + labels := { [label, value] | some label; value := obj.metadata.labels[label] } + count(matchLabels - labels) == 0 + matching_ports := [p | p := svc.spec.ports[_].targetPort; has_port(p, container)] + count(matching_ports) > 0 + + custom_msg := object.get(input.parameters, "customViolationMessage", "") + msg := trim(sprintf("Container <%v> in this <%v> has no <%v> and is selected by service <%v> with targetPort(s) %v. %v", [container.name, input.review.kind.kind, probe, svc.metadata.name, matching_ports, custom_msg]), " ") + } + + has_port(targetPort, container){ + targetPort == container.ports[_].containerPort + } + + has_port(targetPort, container){ + targetPort == container.ports[_].name } probe_is_missing(ctr, probe) = true { @@ -67,10 +112,6 @@ spec: count(diff_fields) == count(probe_type_set) } - get_violation_message(container, review, probe) = msg { - msg := sprintf("Container <%v> in your <%v> <%v> has no <%v>", [container.name, review.kind.kind, review.object.metadata.name, probe]) - } - ``` ### Usage @@ -79,7 +120,7 @@ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper- ``` ## Examples
-block-endpoint-default-role
+container-probes
constraint @@ -90,11 +131,13 @@ kind: K8sRequiredProbes metadata: name: must-have-probes spec: + enforcementAction: warn match: kinds: - apiGroups: [""] kinds: ["Pod"] parameters: + onlyServices: false probes: ["readinessProbe", "livenessProbe"] probeTypes: ["tcpSocket", "httpGet", "exec"] @@ -246,4 +289,160 @@ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-
+
+container-probes-only-services
+ +
+constraint + +```yaml +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sRequiredProbes +metadata: + name: must-have-probes-on-service +spec: + enforcementAction: warn + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + onlyServices: true + probes: ["readinessProbe", "livenessProbe"] + probeTypes: ["tcpSocket", "httpGet", "exec"] + customViolationMessage: "See https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes for more info." + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/general/requiredprobes/samples/must-have-probes-on-service/constraint.yaml +``` + +
+ +
+example-allowed-without-service + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: test-pod1 + namespace: default + labels: + app.kubernetes.io/name: tomcat-no-svc + second-label: "example" +spec: + containers: + - name: tomcat + image: tomcat + ports: + - containerPort: 8080 + volumes: + - name: cache-volume + emptyDir: {} + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/general/requiredprobes/samples/must-have-probes-on-service/example_allowed_without_service.yaml +``` + +
+
+example-allowed-with-service + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: test-pod1 + namespace: default + labels: + app.kubernetes.io/name: tomcat +spec: + containers: + - name: tomcat + image: tomcat + ports: + - containerPort: 8080 + name: tomcat-http + livenessProbe: + tcpSocket: + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 + readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: cache-volume + emptyDir: {} + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/general/requiredprobes/samples/must-have-probes-on-service/example_allowed_with_service.yaml +``` + +
+
+example-disallowed-with-service + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: test-pod1 + namespace: default + labels: + app.kubernetes.io/name: tomcat + second-label: "example" +spec: + containers: + - name: nginx-1 + image: nginx:1.7.9 + ports: + - containerPort: 80 + livenessProbe: + # tcpSocket: + # port: 80 + # initialDelaySeconds: 5 + # periodSeconds: 10 + volumeMounts: + - mountPath: /tmp/cache + name: cache-volume + - name: tomcat + image: tomcat + ports: + - containerPort: 8080 + name: tomcat-http + readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: cache-volume + emptyDir: {} + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/general/requiredprobes/samples/must-have-probes-on-service/example_disallowed_with_service.yaml +``` + +
+ +
\ No newline at end of file