Skip to content

Commit dde2e5f

Browse files
authored
Merge pull request #38 from charlieegan3/show-violations
Show violations
2 parents 6de48cc + c78e837 commit dde2e5f

File tree

13 files changed

+717
-501
lines changed

13 files changed

+717
-501
lines changed

Makefile

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ build-all-platforms:
5353
cd $(ROOT_DIR) && \
5454
mkdir -p ./bundles && \
5555
tar --transform "s/assets\/packages/preflight-packages/" -cvf $@.tmp ./preflight-packages/ && \
56-
tar --transform "s/examples\/pods.preflight.yaml/preflight.yaml/" -rvf [email protected] examples/pods.preflight.yaml && \
57-
tar --transform "s/builds\/preflight-$(GOOS)-$(GOARCH)/preflight/" -rvf [email protected] $< && \
56+
tar --transform "s/examples\/pods.preflight.yaml/preflight.yaml/" -rvf $@.tmp examples/pods.preflight.yaml && \
57+
tar --transform "s/builds\/preflight-$(GOOS)-$(GOARCH)/preflight/" -rvf $@.tmp $< && \
5858
gzip < $@.tmp > $@ && \
5959
rm $@.tmp
6060

@@ -63,6 +63,11 @@ bundle-all-platforms:
6363
$(MAKE) GOOS=darwin GOARCH=amd64 ./bundles/preflight-bundle-darwin-amd64.tgz
6464
$(MAKE) GOOS=windows GOARCH=amd64 ./bundles/preflight-bundle-windows-amd64.tgz
6565

66+
# Packages
67+
68+
package-test:
69+
cd $(ROOT_DIR) && go run ./main.go package test ./preflight-packages/examples.jetstack.io
70+
6671
# Docker image
6772

6873
build-docker-image:

api/report.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ type ReportRule struct {
6262
Success bool `json:"success"`
6363
// Value contains the raw result of the check.
6464
Value interface{} `json:"value,omitempty"`
65-
// Missing indicated wherer the REGO rule was missing or not.
65+
// Missing indicates whether the Rego rule was missing or not.
6666
Missing bool `json:"missing"`
6767
}
6868

cmd/test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ var testParams = struct {
1919

2020
var testCmd = &cobra.Command{
2121
Use: "test",
22-
Short: "Test REGO inside a Preflight package",
22+
Short: "Test Rego inside a Preflight package",
2323
Long: `This uses OPA's engine to run all the test suites
2424
inside the package.
2525

docs/how_to_write_packages.md

Lines changed: 70 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,18 @@ In order to ease keeping track of those changes, Preflight packages have a versi
4343

4444
### The minimal _policy manifest_
4545

46-
Let's just write the minimal _policy manifest_ possible.
46+
Let's write a minimal _policy manifest_ to get started.
4747

4848
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`.
4949

5050
Then create the `policy-manifest.yaml` file. The following fields are mandatory:
5151

52-
- `schema-version`: indicates which schema is being used for the _policy manifest_. For the moment, there is only version `1.0.0`.
52+
- `schema-version`: indicates which schema is being used for the _policy manifest_. For now, there is only version: `1.0.0`.
53+
- `namespace`, `id`, and `package-version`: these properties identify the package. `namespace` must be a FQDN and it is encouraged that `package-version` uses [semver](https://semver.org).
54+
- `root-query`: Name of the Rego package containing the rules backing the
55+
package (see below).
5356

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+
Then, you should also declare the _data-gatherers_ that your rules are going to need. For this example, we only need one, `k8s/pods`.
5758

5859
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.
5960

@@ -70,18 +71,18 @@ root-query: "data.pods" # the concept of `root-query` is explained later in this
7071
data-gatherers:
7172
- k8s/pods
7273
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/"
74+
- id: images
75+
name: Images
76+
description: "Restrictions over the images."
77+
rules:
78+
- id: tag_not_latest
79+
name: "Tag is not latest"
80+
description: >
81+
Avoid using "latest" as tag for the image since.
82+
remediation: >
83+
Change your manifest and edit the Pod template so the image is pinned to a certain tag.
84+
links:
85+
- "https://kubernetes.io/docs/concepts/containers/images/"
8586
```
8687

8788
## Writing the policy definition in Rego
@@ -90,142 +91,107 @@ In the previous section, we created the _policy manifest_, which contains a huma
9091

9192
### The Rego package
9293

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+
Preflight relies on Open Policy Agent as the policy engine. Rego is OPA's language to define policies. You can find their comprehensive [documentation here](https://www.openpolicyagent.org/docs/latest/policy-language/).
9495

9596
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.
9697

9798
For instance, this snippet shows an arbitrary Rego rule in a package named `podsbestpractices`:
9899

99100
```
100-
package pods
101+
package podsbestpractices
101102
102103
import input["k8s/pods"] as pods
103104
104-
preflight_tag_not_latest {
105+
preflight_tag_not_latest[message] {
105106
true
107+
message := "true was found to be true"
106108
}
107109
```
108110

109111
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"`.
110112

111113
### Writing Rego rules
112114

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`.
115+
Rego is a declarative language and has a bit of a learning curve. 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 reference](https://www.openpolicyagent.org/docs/latest/policy-reference/) to hand.
116116

117+
In order to speed up the process of writing Rego rules, it's best to write tests. 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`.
117118

118119
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).
119120

120121
```
121122
# preflight-packages/examples.jetstack.io/podsbestpractices/policy.rego
122123
123-
package pods
124+
package podsbestpractices
124125
125126
import input["k8s/pods"] as pods
126127
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-
}
128+
preflight_explicit_image_tag[message] {
129+
# find all containers in all pods
130+
pod := pods.items[_]
131+
container := pod.spec.containers[_]
132+
# validate that the image value contains an explicit tag
133+
{ re_match(`latest$`, container.image),
134+
re_match(`^[^:]+$`, container.image) } & { true } != set()
151135
152-
containers_using_latest[container] {
153-
container := all_containers[_]
154-
not re_match(".*:.+", container.image)
136+
# bind a message for reporting
137+
message := sprintf("container '%s' in pod '%s' in namespace '%s' is missing an explicit image tag", [container.name, pod.metadata.name, pod.metadata.namespace])
155138
}
156139
```
157140

158141
### Testing Rego
159142

160-
As mentioned before, it is very useful to [write tests for the Rego rules](https://www.openpolicyagent.org/docs/latest/policy-testing/).
143+
As mentioned before, it is very useful to [write tests for your Rego rules](https://www.openpolicyagent.org/docs/latest/policy-testing/).
161144

162-
This snippet contains a testsuite for the previous Rego code.
145+
This snippet contains a test case for the previous Rego code.
163146

164147
```
165148
# preflight-packages/examples.jetstack.io/podsbestpractices/policy_test.rego
166149
167-
package pods
150+
package podsbestpractices
168151
169152
pods(x) = y { y := {"k8s/pods": {"items": x }} }
170153
171-
test_tag_not_latest_no_pods {
172-
preflight_tag_not_latest with input as pods([])
154+
assert_allowed(output) = output {
155+
trace(sprintf("GOT: %s", [concat(",", output)]))
156+
trace("WANT: empty set")
157+
output == set()
173158
}
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-
])
159+
160+
assert_violates(output, messages) = output {
161+
trace(sprintf("GOT: %s", [concat(",", output)]))
162+
trace(sprintf("WANT: %s", [concat(",", messages)]))
163+
164+
output == messages
193165
}
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-
])
166+
167+
test_explicit_image_tag_no_pods {
168+
output := preflight_explicit_image_tag with input as pods([])
169+
assert_allowed(output)
203170
}
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-
])
171+
172+
test_explicit_image_tag_latest_tag {
173+
output := preflight_explicit_image_tag with input as pods([
174+
{"metadata": {
175+
"name": "foo",
176+
"namespace": "default"
177+
},
178+
"spec":{"containers":[
179+
{"name": "container-one",
180+
"image": "gcr.io/my-project/my-image:latest"}
181+
]}}])
182+
183+
assert_violates(output, {
184+
"container 'container-one' in pod 'foo' in namespace 'default' is missing an explicit image tag"
185+
})
220186
}
221187
```
222188

223-
Soon, Preflight will be able to run Rego tests inside Preflight packages (#26), but unfortunatelly this is not possible yet.
189+
Soon, Preflight will be able to run Rego tests inside Preflight packages (#26), but unfortunately this is not possible yet.
224190

225-
However it is possible to run these tests directly with the [OPA command line](https://www.openpolicyagent.org/docs/latest/#running-opa):
191+
To run our tests, we must use the [OPA command line](https://www.openpolicyagent.org/docs/latest/#running-opa) like this:
226192

227193
```
228-
opa test ./preflight-packages/examples.jetstack.io/podsbestpractices
194+
opa test -v --explain=notes ./preflight-packages/examples.jetstack.io/podsbestpractices
229195
```
230196

231197
## Lint your packages
@@ -235,7 +201,7 @@ The Preflight command line has a built-in linter. This helps to make sure that t
235201
You can lint your package by running:
236202

237203
```
238-
preflight package lint ./preflight-packages/examples.jetstack.io/podsbestpractices
204+
preflight package lint ./preflight-packages/examples.jetstack.io/podsbestpractices
239205
```
240206

241207
## Configure Preflight to use your package

pkg/exporter/json_test.go

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,6 @@ func TestJSONExport(t *testing.T) {
5555
Name: "Sample section 2",
5656
Description: "This is another section.",
5757
Rules: []packaging.Rule{
58-
packaging.Rule{
59-
ID: "r3",
60-
Name: "Another rule",
61-
Description: "This is another rule.",
62-
Manual: false,
63-
Remediation: "No remedy.",
64-
Links: []string{
65-
"http://jetstack.io/docs",
66-
"http://jetstack.io/docs2",
67-
},
68-
},
6958
packaging.Rule{
7059
ID: "r4",
7160
Name: "Another rule",
@@ -85,9 +74,8 @@ func TestJSONExport(t *testing.T) {
8574
jsonExporter := JSONExporter{}
8675

8776
rc := &results.ResultCollection{
88-
&results.Result{ID: ruleToResult("r1"), Value: true},
89-
&results.Result{ID: ruleToResult("r2"), Value: false},
90-
&results.Result{ID: ruleToResult("r3"), Value: "not a bool"},
77+
&results.Result{ID: ruleToResult("r1"), Violations: []string{}},
78+
&results.Result{ID: ruleToResult("r2"), Violations: []string{"violation"}},
9179
}
9280

9381
expectedJSON := `{
@@ -125,19 +113,6 @@ func TestJSONExport(t *testing.T) {
125113
},
126114
{
127115
"rules": [
128-
{
129-
"missing": false,
130-
"value": "not a bool",
131-
"success": false,
132-
"links": [
133-
"http://jetstack.io/docs",
134-
"http://jetstack.io/docs2"
135-
],
136-
"remediation": "No remedy.",
137-
"description": "This is another rule.",
138-
"name": "Another rule",
139-
"id": "r3"
140-
},
141116
{
142117
"missing": true,
143118
"success": false,

pkg/packaging/packaging.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type PolicyManifest struct {
3232
ID string `yaml:"id"`
3333
// DataGatherers is the list of data-gatherers the package depends on.
3434
DataGatherers []string `yaml:"data-gatherers,omitempty"`
35-
// RootQuery is the query needed in the REGO context to access the result of the checks.
35+
// RootQuery is the query needed in the Rego context to access the result of the checks.
3636
RootQuery string `yaml:"root-query"`
3737
// Name is the name of the package.
3838
Name string `yaml:"name"`

0 commit comments

Comments
 (0)