# Part 2: Policy

## What Will You Do

In this part of the self-paced exercise, you will create and enforce a "policy" comprised of two OPA Gatekeeper constraints. Ensure that

1. Container resource limits to requests does not surpass a specified ratio
2. Only container images from authorized/approved repositories are allowed to operate on clusters

---

## OPA Constraint Framework

Gatekeeper uses the [OPA Constraint Framework](https://github.com/open-policy-agent/frameworks/tree/master/constraint) to describe and enforce policy. Look there for more detailed information on their semantics and advanced usage.

### Constraint

A constraint is a declaration that its author wants a system to meet a given set of requirements. Each Constraint is written with Rego, a declarative query language used by OPA to enumerate instances of data that violate the expected state of the system. All Constraints are evaluated as a **Logical AND**. If one Constraint is not satisfied, then the whole request is rejected.

In the example below, the constraint requires users to specify labels for k8s namespaces.

```
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: ns-must-have-label-owner
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Namespace"]
  parameters:
    labels: ["owner"]
```

---

### Constraint Template

Before defining a Constraint, you need to create a Constraint Template that allows people to declare new Constraints. Each template describes both the Rego logic that enforces the Constraint and the schema for the Constraint, which includes the schema of the CRD and the parameters that can be passed into a Constraint, much like arguments to a function.

Example:

```
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredLabels
        listKind: K8sRequiredLabelsList
        plural: k8srequiredlabels
        singular: k8srequiredlabels
      validation:
        # Schema for the `parameters` field
        openAPIV3Schema:
          properties:
            labels:
              type: array
              items: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredlabels

violation[{"msg": msg, "details": {"missing_labels": missing}}] {
          provided := {label | input.review.object.metadata.labels[label]}
          required := {label | label := input.parameters.labels[_]}
          missing := required - provided
          count(missing) > 0
          msg := sprintf("you must provide labels: %v", [missing])
        }
```

---

## Step 1: Create Constraint Templates

In this step, we will create two "custom" constraint templates. The constraint templates will control the allowed repositories where images can be obtained from and sets the maximum ratio for container resource limits to requests.

### Creating a Constraint Template to allow container images from approved repositories

- Login to the Controller and select **Constraint Templates** under OPA Gatekeeper specification
- Click **New Template**
- Provide a name for the template (e.g. rafay-gatekeeper-allowedrepos-constraint-template)
- Select the **Upload files manually** option and upload the below yaml file
- Click **Save & Exit**

```
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: opak8sallowedrepos
  annotations:
    description: Requires container images to begin with a repo string from a specified
      list.
spec:
  crd:
    spec:
      names:
        kind: OPAK8sAllowedRepos
      validation:
        # Schema for the `parameters` field
        openAPIV3Schema:
          properties:
            repos:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sallowedrepos

violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          satisfied := [good | repo = input.parameters.repos[_] ; good = re_match(repo, container.image)]
          not any(satisfied)
          msg := sprintf("container <%v> has an invalid image repo <%v>, allowed repos are %v", [container.name, container.image, input.parameters.repos])
        }

violation[{"msg": msg}] {
          container := input.review.object.spec.initContainers[_]
          satisfied := [good | repo = input.parameters.repos[_] ; good = re_match(repo, container.image)]
          not any(satisfied)
          msg := sprintf("container <%v> has an invalid image repo <%v>, allowed repos are %v", [container.name, container.image, input.parameters.repos])
        }
```

### Creating a Constraint Template to check for container resource limits to requests ratio

- Login to the Controller and select **Constraint Templates** under OPA Gatekeeper specification
- Click **New Template**
- Provide a name for the template (e.g. rafay-request-limit-ratio-constraint-template)
- Select the **Upload files manually** option and upload the below yaml file
- Click **Save & Exit**

```
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: opak8scontainerratios
  annotations:
    description: Sets a maximum ratio for container resource limits to requests.
spec:
  crd:
    spec:
      names:
        kind: OPAK8sContainerRatios
      validation:
        openAPIV3Schema:
          properties:
            ratio:
              type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8scontainerratios

missing(obj, field) = true {
          not obj[field]
        }

missing(obj, field) = true {
          obj[field] == ""
        }

canonify_cpu(orig) = new {
          is_number(orig)
          new := orig * 1000
        }

canonify_cpu(orig) = new {
          not is_number(orig)
          endswith(orig, "m")
          new := to_number(replace(orig, "m", ""))
        }

canonify_cpu(orig) = new {
          not is_number(orig)
          not endswith(orig, "m")
          re_match("^[0-9]+$", orig)
          new := to_number(orig) * 1000
        }

canonify_cpu(orig) = new {
          not is_number(orig)
          not endswith(orig, "m")
          re_match("^[0-9]+[.][0-9]+$", orig)
          new := to_number(orig) * 1000
        }

# 10 ** 21
        mem_multiple("E") = 1000000000000000000000 { true }

# 10 ** 18
        mem_multiple("P") = 1000000000000000000 { true }

# 10 ** 15
        mem_multiple("T") = 1000000000000000 { true }

# 10 ** 12
        mem_multiple("G") = 1000000000000 { true }

# 10 ** 9
        mem_multiple("M") = 1000000000 { true }

# 10 ** 6
        mem_multiple("k") = 1000000 { true }

# 10 ** 3
        mem_multiple("") = 1000 { true }

# Kubernetes accepts millibyte precision when it probably shouldn't.
        # https://github.com/kubernetes/kubernetes/issues/28741
        # 10 ** 0
        mem_multiple("m") = 1 { true }

# 1000 * 2 ** 10
        mem_multiple("Ki") = 1024000 { true }

# 1000 * 2 ** 20
        mem_multiple("Mi") = 1048576000 { true }

# 1000 * 2 ** 30
        mem_multiple("Gi") = 1073741824000 { true }

# 1000 * 2 ** 40
        mem_multiple("Ti") = 1099511627776000 { true }

# 1000 * 2 ** 50
        mem_multiple("Pi") = 1125899906842624000 { true }

# 1000 * 2 ** 60
        mem_multiple("Ei") = 1152921504606846976000 { true }

get_suffix(mem) = suffix {
          not is_string(mem)
          suffix := ""
        }

get_suffix(mem) = suffix {
          is_string(mem)
          count(mem) > 0
          suffix := substring(mem, count(mem) - 1, -1)
          mem_multiple(suffix)
        }

get_suffix(mem) = suffix {
          is_string(mem)
          count(mem) > 1
          suffix := substring(mem, count(mem) - 2, -1)
          mem_multiple(suffix)
        }

get_suffix(mem) = suffix {
          is_string(mem)
          count(mem) > 1
          not mem_multiple(substring(mem, count(mem) - 1, -1))
          not mem_multiple(substring(mem, count(mem) - 2, -1))
          suffix := ""
        }

get_suffix(mem) = suffix {
          is_string(mem)
          count(mem) == 1
          not mem_multiple(substring(mem, count(mem) - 1, -1))
          suffix := ""
        }

get_suffix(mem) = suffix {
          is_string(mem)
          count(mem) == 0
          suffix := ""
        }

canonify_mem(orig) = new {
          is_number(orig)
          new := orig * 1000
        }

canonify_mem(orig) = new {
          not is_number(orig)
          suffix := get_suffix(orig)
          raw := replace(orig, suffix, "")
          re_match("^[0-9]+$", raw)
          new := to_number(raw) * mem_multiple(suffix)
        }

violation[{"msg": msg}] {
          general_violation[{"msg": msg, "field": "containers"}]
        }

violation[{"msg": msg}] {
          general_violation[{"msg": msg, "field": "initContainers"}]
        }

general_violation[{"msg": msg, "field": field}] {
          container := input.review.object.spec[field][_]
          cpu_orig := container.resources.limits.cpu
          not canonify_cpu(cpu_orig)
          msg := sprintf("container <%v> cpu limit <%v> could not be parsed", [container.name, cpu_orig])
        }

general_violation[{"msg": msg, "field": field}] {
          container := input.review.object.spec[field][_]
          mem_orig := container.resources.limits.memory
          not canonify_mem(mem_orig)
          msg := sprintf("container <%v> memory limit <%v> could not be parsed", [container.name, mem_orig])
        }

general_violation[{"msg": msg, "field": field}] {
          container := input.review.object.spec[field][_]
          cpu_orig := container.resources.requests.cpu
          not canonify_cpu(cpu_orig)
          msg := sprintf("container <%v> cpu request <%v> could not be parsed", [container.name, cpu_orig])
        }

general_violation[{"msg": msg, "field": field}] {
          container := input.review.object.spec[field][_]
          mem_orig := container.resources.requests.memory
          not canonify_mem(mem_orig)
          msg := sprintf("container <%v> memory request <%v> could not be parsed", [container.name, mem_orig])
        }

general_violation[{"msg": msg, "field": field}] {
          container := input.review.object.spec[field][_]
          not container.resources
          msg := sprintf("container <%v> has no resource limits", [container.name])
        }

general_violation[{"msg": msg, "field": field}] {
          container := input.review.object.spec[field][_]
          not container.resources.limits
          msg := sprintf("container <%v> has no resource limits", [container.name])
        }

general_violation[{"msg": msg, "field": field}] {
          container := input.review.object.spec[field][_]
          missing(container.resources.limits, "cpu")
          msg := sprintf("container <%v> has no cpu limit", [container.name])
        }

general_violation[{"msg": msg, "field": field}] {
          container := input.review.object.spec[field][_]
          missing(container.resources.limits, "memory")
          msg := sprintf("container <%v> has no memory limit", [container.name])
        }

general_violation[{"msg": msg, "field": field}] {
          container := input.review.object.spec[field][_]
          not container.resources.requests
          msg := sprintf("container <%v> has no resource requests", [container.name])
        }

general_violation[{"msg": msg, "field": field}] {
          container := input.review.object.spec[field][_]
          missing(container.resources.requests, "cpu")
          msg := sprintf("container <%v> has no cpu request", [container.name])
        }

general_violation[{"msg": msg, "field": field}] {
          container := input.review.object.spec[field][_]
          missing(container.resources.requests, "memory")
          msg := sprintf("container <%v> has no memory request", [container.name])
        }

general_violation[{"msg": msg, "field": field}] {
          container := input.review.object.spec[field][_]
          cpu_limits_orig := container.resources.limits.cpu
          cpu_limits := canonify_cpu(cpu_limits_orig)
          cpu_requests_orig := container.resources.requests.cpu
          cpu_requests := canonify_cpu(cpu_requests_orig)
          cpu_ratio := input.parameters.ratio
          to_number(cpu_limits) > to_number(cpu_ratio) * to_number(cpu_requests)
          msg := sprintf("container <%v> cpu limit <%v> is higher than the maximum allowed ratio of <%v>", [container.name, cpu_limits_orig, cpu_ratio])
        }

general_violation[{"msg": msg, "field": field}] {
          container := input.review.object.spec[field][_]
          mem_limits_orig := container.resources.limits.memory
          mem_requests_orig := container.resources.requests.memory
          mem_limits := canonify_mem(mem_limits_orig)
          mem_requests := canonify_mem(mem_requests_orig)
          mem_ratio := input.parameters.ratio
          to_number(mem_limits) > to_number(mem_ratio) * to_number(mem_requests)
          msg := sprintf("container <%v> memory limit <%v> is higher than the maximum allowed ratio of <%v>", [container.name, mem_limits_orig, mem_ratio])
        }
```

---

## Step 2: Create Constraints

In this step, we will create two constraints that are associated with the previously created constraint templates.

### Creating a Constraint to allow container images from approved repositories

- Login to the Controller and select **Constraints** under OPA Gatekeeper specification
- Click **New Constraint**
- Provide a name for the constraint (e.g. rafay-gatekeeper-allowedrepos-constraint) and select the Constraint Template (rafay-gatekeeper-allowedrepos-constraint-template)
- Specify the version name (e.g. v1)
- Select the **Upload files manually** option and upload the below yaml file
- Click **Save Changes**

```
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: OPAK8sAllowedRepos
metadata:
  name: opa-allowed-repos
spec:
  enforcementAction: deny
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
  parameters:
    repos:
      - "amazonaws.com" # ECR registry for EKS cluster
      - "k8s.gcr.io" # Kubernetes registry
      - "docker.io" # bitnami registry
```

### Creating a Constraint to check for container resource limits to requests ratio

- Login to the Controller and select **Constraints** under OPA Gatekeeper specification
- Click **New Constraint**
- Provide a name for the constraint (e.g. rafay-request-limit-ratio-constraint) and select the Constraint Template (rafay-request-limit-ratio-constraint-template)
- Specify the version name (e.g. v1)
- Select the **Upload files manually** option and upload the below yaml file
- Click **Save Changes**

```
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: OPAK8sContainerRatios
metadata:
  name: opa-container-must-meet-ratio
spec:
  enforcementAction: warn
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
  parameters:
    ratio: "1"
```

---

## Step 3: Create Policy

In this step, we will create a policy which will assemble together multiple constraints that were created in the prior steps.

- Login to the Controller and select **Policies** under OPA Gatekeeper section
- Click **New Policy**, provide a name (e.g. opa-gs-policy) and a version (e.g. v1)
- Click **Add Constraint** and select the constraints previously created along with their versions
- Click **Save Changes**

---

## Recap

Congratulations! At this point, you have successfully created a policy with two custom constraints. In the next step, you will add this policy to a cluster blueprint and deploy it on a fleet of managed clusters.
