Writing OPA rules to lint Kubernetes YAML resource and Outputting as annotations on Pull Requests with GitHub Actions

Warning: This expects you already know about rego/opa and is more of a brain dump than a blog.

First up take a look at conftest it’s a great little CLI tool which lets you take rules you’ve written in rego/opa and run them easily.

In our case we have the following:

./rules folder containing our rego rules
./yaml folder containing yaml we want to validate

We’re going to write a rule to flag duplicate resources, ie. when you have two yamls with the same kind and name.

The rule will be written in rego then executed by conftest and when a failure occurs it’ll be shown as an annotation on the Pull Request using GitHub Actions.

Firstly for conftest we want to use the --combine option so we get a single array of all the yaml files passed into our rule. This allows us to compare the files against one another to determine if there are any duplicates.

The data structure you get looks a bit like this:

        "content": {
            "apiVersion": "thing",
            "kind": "deployment"
        "path": "path/to/yaml/file"

As well as validating the rule we also use the path property to output metadata about which file generated the warning.

We can then use jq to parse the json output from conftest and convert it to “Workflow Commands: Warning Messages” these are outputted to the console and read by GitHub Actions. With the details in the message it generates an annotation on the file in the PR like so:

Here is a gist of this wrapped together.

# Here is the basic Rego rule
package main
# deny creating duplicate resource in the same namespace
deny_duplicate_resources[{"msg": msg, "details": details}] {
i != j
currentFilePath = input[i].path
input[i].contents.kind == input[j].contents.kind
input[i].contents.metadata.name == input[j].contents.metadata.name
msg := sprintf("no duplicate resources are allowed, file: %q, name: %q, kind: %q, file with duplicate: %q", [currentFilePath, input[i].contents.metadata.name, input[i].contents.kind, input[j].path])
details := {
"file": currentFilePath,
"line": 1,
"url": "http://some.docs.link.here.something/rulex.md",
view raw 1-rule.rego hosted with ❤ by GitHub
# This runs the rule against the yaml with conftest
# Run this inside your GitHub Action
conftest test -p ./rules ./yaml –combine –no-fail -o json | jq -r -f ./convert.jq
view raw 2-run.bash hosted with ❤ by GitHub
# Get all the failure items from the conftest json output
# see: https://www.conftest.dev/options/#json
# Note as we use `–combine` with conftest we will always receive and array consisting of a single item
# To add newlines to the message '\n' has to be urlencoded to %0A
# We split the 'msg' returned by the rule with ','s replaced with newlines
# and also put the doc url on a newline
# see: https://github.com/actions/toolkit/issues/193
try .[0].failures[]
# pull out the file and msg that we care about based on the defined
# test output format
# see: ../README.md#writing-rules
| { "file": .metadata.details.file, "msg": (.msg | gsub(", "; "%0A ")), "url": .metadata.details.url}
# Format that into the structure actions wants
# see: https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions#setting-a-warning-message
| "::warning file=\(.file),line=1::\(.msg)%0A%0AAbout this rule: \(.url)"
view raw 3-convert.jq hosted with ❤ by GitHub

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s