|
1 | 1 | # How to write Preflight Packages |
2 | 2 |
|
3 | | -We are writing a comprehensive manual on how to create your own packages. In the meanwhile, we have [this basic tutorial on how to encode a policy into Preflight Packages](https://blog.jetstack.io/blog/introducing-preflight/#encoding-your-policy-into-preflight-packages). |
| 3 | +## What is a Preflight package? |
| 4 | + |
| 5 | +A Preflight package contains the definition of a policy. A policy is a set of rules that Preflight will check in your cluster. |
| 6 | + |
| 7 | +Preflight packages are made of two well distinguished parts, the _policy manifest_ and the _Rego_ definition of the rules. |
| 8 | + |
| 9 | +<img align="center" width="460" height="300" src="./images/preflight_package.png"> |
| 10 | + |
| 11 | +## Writing the _policy manifest_ |
| 12 | + |
| 13 | +The _policy manifest_ is a _YAML_ file that contains information about your policy. You can see [here](https://godoc.org/github.com/jetstack/preflight/pkg/packaging#PolicyManifest) the schema of this file. |
| 14 | + |
| 15 | +There is some metadata for the package, such as the name and the description. |
| 16 | + |
| 17 | +The rules of your policy must also be declared here. They are organized in sections. Every section can have a name, a description, and a list of rules. |
| 18 | + |
| 19 | +Every rule has its own name, description, useful links, and remediation instructions. This guide has a dedicated section about how to add rules. |
| 20 | + |
| 21 | +### Choose your _data-gatherers_ |
| 22 | + |
| 23 | +Preflight evaluates the policies against data that it fetches from different sources. These sources are the _data-gatherers_. You can see [here](./datagatherers) a list of the available _data-gatherers_ and their documentation. In the future, Preflight will support sourcing data from external _data-gatherers_ (#24). |
| 24 | + |
| 25 | +The _data-gatherers_ your package requires should be declared in the _policy manifest_. |
| 26 | + |
| 27 | +```yaml |
| 28 | +schema-version: "1.0.0" |
| 29 | +id: "mypackage" |
| 30 | +namespace: "examples.jetstack.io" |
| 31 | +package-version: "1.0.0" |
| 32 | +data-gatherers: |
| 33 | +- k8s/pods |
| 34 | +- gke |
| 35 | +... |
| 36 | +``` |
| 37 | + |
| 38 | +### Versioning |
| 39 | + |
| 40 | +Preflight packages are intended to evolve. Policies can be changed, new rules can be added or deleted, and all the metadata can mutate. |
| 41 | + |
| 42 | +In order to ease keeping track of those changes, Preflight packages have a version tag. That version is specified with the `package-version` property in the _policy manifest_. |
| 43 | + |
| 44 | +### The minimal _policy manifest_ |
| 45 | + |
| 46 | +Let's just write the minimal _policy manifest_ possible. |
| 47 | + |
| 48 | +First, create a directory for this new package. We are going to create this new package under the `examples.jetstack.io` namespace, and we are going to name it `podsbestpractices`. |
| 49 | + |
| 50 | +Then create the `policy-manifest.yaml` file. The following fields are mandatory: |
| 51 | + |
| 52 | +- `schema-version`: indicates which schema is being used for the _policy manifest_. For the moment, there is only version `1.0.0`. |
| 53 | + |
| 54 | +- `namespace`, `id`, and `package-version`: these properties identify the package. `namespace` must be a FQDN and it is encouraged that `package-version` uses semver. |
| 55 | + |
| 56 | +Then, you should also declare the _data-gatherers_ that your rules are going to need. For this example, let's just use `k8s/pods`. |
| 57 | + |
| 58 | +Finally, it's time to declare the rules for the policy. Rules are organized into sections. Every section has an ID, a name, and a description. Also, every rule has its own ID, name, and description. Additionally, rules can have other metadata like a remediation advice or a set of related links. |
| 59 | + |
| 60 | +For simplicity's sake, this example contains just one section with one rule. |
| 61 | + |
| 62 | +``` |
| 63 | +# preflight-packages/examples.jetstack.io/podsbestpractices/policy-manifest.yaml |
| 64 | +
|
| 65 | +schema-version: "1.0.0" |
| 66 | +id: "podsbestpractices" |
| 67 | +namespace: "examples.jetstack.io" |
| 68 | +package-version: "1.0.0" |
| 69 | +root-query: "data.pods" # the concept of `root-query` is explained later in this doc |
| 70 | +data-gatherers: |
| 71 | +- k8s/pods |
| 72 | +sections: |
| 73 | + - id: images |
| 74 | + name: Images |
| 75 | + description: "Restrictions over the images." |
| 76 | + rules: |
| 77 | + - id: tag_not_latest |
| 78 | + name: "Tag is not latest" |
| 79 | + description: > |
| 80 | + Avoid using "latest" as tag for the image since. |
| 81 | + remediation: > |
| 82 | + Change your manifest and edit the Pod template so the image is pinned to a certain tag. |
| 83 | + links: |
| 84 | + - "https://kubernetes.io/docs/concepts/containers/images/" |
| 85 | +``` |
| 86 | + |
| 87 | +## Writing the policy definition in Rego |
| 88 | + |
| 89 | +In the previous section, we created the _policy manifest_, which contains a human readable description of the rules in our policy. Now it's time to define the same rules in a language that is machine readable. |
| 90 | + |
| 91 | +### The Rego package |
| 92 | + |
| 93 | +Preflight relies on Open Policy Agent as the policy engine. Rego is OPA's language to define policies. You can find a comprenhensive [documentation](https://www.openpolicyagent.org/docs/latest/policy-language/). |
| 94 | + |
| 95 | +You can have multiple Rego files inside the directory of a Preflight package. All the Rego rules corresponding to the _policy manifest_ rules must be in the same Rego package, and that package must be indicated in the _policy manifest_ using the `root-query` property. |
| 96 | + |
| 97 | +For instance, this snippet shows an arbitrary Rego rule in a package named `podsbestpractices`: |
| 98 | + |
| 99 | +``` |
| 100 | +package pods |
| 101 | +
|
| 102 | +import input["k8s/pods"] as pods |
| 103 | +
|
| 104 | +preflight_tag_not_latest { |
| 105 | + true |
| 106 | +} |
| 107 | +``` |
| 108 | + |
| 109 | +As you can identify, the Rego package for that policy is `pods`. In this case, OPA's `root-query` is `data.pods`, and that is why in the previous section, `policy-manifest.yaml` contains `root-query: "data.pods"`. |
| 110 | + |
| 111 | +### Writing Rego rules |
| 112 | + |
| 113 | +Rego can be challenging at the beginning because it does not behaves like a traditional programming language. It is strongly recommended to read ["The Basics"](https://www.openpolicyagent.org/docs/latest/policy-language/#the-basics). Also, it is useful to have the [language refence](https://www.openpolicyagent.org/docs/latest/policy-reference/) at hand. |
| 114 | + |
| 115 | +You will get faster as you write more Rego rules. In order to speed up this process, it's best to write tests for your rules, even if you think they are not needed. It means you can iterate fast while writing rules and make sure the rules are doing what you intended. It is conventional to name the test files for `policy.rego` as `policy_test.rego`. |
| 116 | + |
| 117 | + |
| 118 | +This example contains the definition for the `tag_no_latest` rule. As you can see, there is the convention within Preflight to add `preflight_` as prefix to the rule ID when that is written in Rego (related issue #27). |
| 119 | + |
| 120 | +``` |
| 121 | +# preflight-packages/examples.jetstack.io/podsbestpractices/policy.rego |
| 122 | +
|
| 123 | +package pods |
| 124 | +
|
| 125 | +import input["k8s/pods"] as pods |
| 126 | +
|
| 127 | +default preflight_tag_not_latest = false |
| 128 | +preflight_tag_not_latest { |
| 129 | + count(containers_using_latest) == 0 |
| 130 | +} |
| 131 | +
|
| 132 | +format_container(pod, container) = name { |
| 133 | + name := { |
| 134 | + "namespace": pod.metadata.namespace, |
| 135 | + "pod": pod.metadata.name, |
| 136 | + "image": container.image, |
| 137 | + "name": container.name |
| 138 | + } |
| 139 | +} |
| 140 | +
|
| 141 | +all_containers[container_name] { |
| 142 | + pod := pods.items[_] |
| 143 | + container := pod.spec.containers[_] |
| 144 | + container_name = format_container(pod, container) |
| 145 | +} |
| 146 | +
|
| 147 | +containers_using_latest[container] { |
| 148 | + container := all_containers[_] |
| 149 | + re_match(".*:latest", container.image) |
| 150 | +} |
| 151 | +
|
| 152 | +containers_using_latest[container] { |
| 153 | + container := all_containers[_] |
| 154 | + not re_match(".*:.+", container.image) |
| 155 | +} |
| 156 | +``` |
| 157 | + |
| 158 | +### Testing Rego |
| 159 | + |
| 160 | +As mentioned before, it is very useful to [write tests for the Rego rules](https://www.openpolicyagent.org/docs/latest/policy-testing/). |
| 161 | + |
| 162 | +This snippet contains a testsuite for the previous Rego code. |
| 163 | + |
| 164 | +``` |
| 165 | +# preflight-packages/examples.jetstack.io/podsbestpractices/policy_test.rego |
| 166 | +
|
| 167 | +package pods |
| 168 | +
|
| 169 | +pods(x) = y { y := {"k8s/pods": {"items": x }} } |
| 170 | +
|
| 171 | +test_tag_not_latest_no_pods { |
| 172 | + preflight_tag_not_latest with input as pods([]) |
| 173 | +} |
| 174 | +test_tag_not_latest_v1 { |
| 175 | + preflight_tag_not_latest with input as pods([ |
| 176 | + { |
| 177 | + "metadata": { "namespace": "default", "name": "p1" }, |
| 178 | + "spec": { "containers":[ |
| 179 | + {"name": "c1", "image": "golang:v1"}, |
| 180 | + ]} |
| 181 | + } |
| 182 | + ]) |
| 183 | +} |
| 184 | +test_tag_not_latest_latest { |
| 185 | + not preflight_tag_not_latest with input as pods([ |
| 186 | + { |
| 187 | + "metadata": { "namespace": "default", "name": "p1" }, |
| 188 | + "spec": { "containers":[ |
| 189 | + {"name": "c1", "image": "golang:latest"} |
| 190 | + ]} |
| 191 | + } |
| 192 | + ]) |
| 193 | +} |
| 194 | +test_tag_not_latest_latest_implicit { |
| 195 | + not preflight_tag_not_latest with input as pods([ |
| 196 | + { |
| 197 | + "metadata": { "namespace": "default", "name": "p1" }, |
| 198 | + "spec": { "containers":[ |
| 199 | + {"name": "c1", "image": "golang"} |
| 200 | + ]} |
| 201 | + } |
| 202 | + ]) |
| 203 | +} |
| 204 | +test_tag_not_latest_latest_multiple { |
| 205 | + not preflight_tag_not_latest with input as pods([ |
| 206 | + { |
| 207 | + "metadata": { "namespace": "default", "name": "p1" }, |
| 208 | + "spec": { "containers":[ |
| 209 | + {"name": "c1", "image": "golang:v1"} |
| 210 | + ]} |
| 211 | + }, |
| 212 | + { |
| 213 | + "metadata": { "namespace": "default", "name": "p2"}, |
| 214 | + "spec": { "containers":[ |
| 215 | + {"name": "c1", "image": "golang:v2"}, |
| 216 | + {"name": "c2", "image": "golang:latest"}, |
| 217 | + ]} |
| 218 | + } |
| 219 | + ]) |
| 220 | +} |
| 221 | +``` |
| 222 | + |
| 223 | +Soon, Preflight will be able to run Rego tests inside Preflight packages (#26), but unfortunatelly this is not possible yet. |
| 224 | + |
| 225 | +However it is possible to run these tests directly with the [OPA command line](https://www.openpolicyagent.org/docs/latest/#running-opa): |
| 226 | + |
| 227 | +``` |
| 228 | +opa test ./preflight-packages/examples.jetstack.io/podsbestpractices |
| 229 | +``` |
| 230 | + |
| 231 | +## Lint your packages |
| 232 | + |
| 233 | +The Preflight command line has a built-in linter. This helps to make sure that the package follows the best practices. |
| 234 | + |
| 235 | +You can lint your package by running: |
| 236 | + |
| 237 | +``` |
| 238 | +preflight package lint ./preflight-packages/examples.jetstack.io/podsbestpractices |
| 239 | +``` |
| 240 | + |
| 241 | +## Configure Preflight to use your package |
| 242 | + |
| 243 | +The last step would be to tell Preflight to actually use these new package. That configuration goes into the `preflight.yaml` file. For this example, it would look like this: |
| 244 | + |
| 245 | +```yaml |
| 246 | +# preflight.yaml |
| 247 | +cluster-name: my-cluster |
| 248 | + |
| 249 | +data-gatherers: |
| 250 | + k8s/pods: |
| 251 | + kubeconfig: ~/.kube/config |
| 252 | + |
| 253 | +package-sources: |
| 254 | +- type: local |
| 255 | + dir: preflight-packages/ |
| 256 | + |
| 257 | +enabled-packages: |
| 258 | + - "examples.jetstack.io/podsbestpractice" |
| 259 | + |
| 260 | +outputs: |
| 261 | +- type: local |
| 262 | + path: ./output |
| 263 | + format: json |
| 264 | +- type: cli |
| 265 | +``` |
0 commit comments