Skip to content

Commit b690395

Browse files
authored
engine+reporter: enable print() output (#629)
Any `print()` calls that happened while a query was evaluated should now be printed to stdout, with a "PRNT" label. This depends on OPA 0.34.0. Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
1 parent 14f426b commit b690395

File tree

7 files changed

+81
-14
lines changed

7 files changed

+81
-14
lines changed

acceptance.bats

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
}
7979

8080
@test "Verify command has trace flag" {
81-
run ./conftest verify --policy ./examples/kubernetes/policy --trace
81+
run ./conftest verify --policy ./examples/kubernetes/policy --trace
8282
[ "$status" -eq 0 ]
8383
[[ "$output" =~ "data.kubernetes.is_service" ]]
8484
}
@@ -92,8 +92,17 @@
9292
run ./conftest verify --policy ./examples/report/policy --policy ./examples/report/success --report fails
9393
[ "$status" -eq 0 ]
9494
[[ "${lines[0]}" =~ "data.main.test_no_missing_label: PASS" ]]
95-
[[ "${lines[1]}" =~ "--------------------------------------------------------------------------------" ]]
96-
[[ "${lines[2]}" =~ "PASS: 1/1" ]]
95+
[[ "${lines[1]}" == "--------------------------------------------------------------------------------" ]]
96+
[[ "${lines[2]}" == "PASS: 1/1" ]]
97+
}
98+
99+
@test "Verify command has report flag - success with print output" {
100+
run ./conftest verify --policy ./examples/report/policy_print --policy ./examples/report/success --report fails
101+
[ "$status" -eq 0 ]
102+
[[ "${lines[0]}" =~ "data.main.test_no_missing_label: PASS" ]]
103+
[[ "${lines[2]}" == "--------------------------------------------------------------------------------" ]]
104+
[[ "${lines[3]}" == "PASS: 1/1" ]]
105+
[[ "${lines[1]}" == ' sample' ]]
97106
}
98107

99108
@test "Verify command does not support report flag with table output" {
@@ -159,6 +168,12 @@
159168
[[ "$output" =~ "Terraform plan will change prohibited resources in the following namespaces: google_iam, google_container" ]]
160169
}
161170

171+
@test "Supports print() output" {
172+
run ./conftest test -p examples/report/policy_print/labels.rego examples/kubernetes/deployment.yaml --no-color
173+
[ "$status" -eq 1 ]
174+
[[ "${lines[0]}" == "PRNT examples/report/policy_print/labels.rego:12: hello-kubernetes" ]]
175+
}
176+
162177
@test "Can parse hcl1 files" {
163178
run ./conftest test -p examples/hcl1/policy/gke.rego examples/hcl1/gke.tf
164179
[ "$status" -eq 0 ]

examples/report/policy/labels.rego

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ deny[msg] {
1111
input.kind = "Deployment"
1212
not required_deployment_labels
1313
trace("just testing notes flag")
14-
msg = sprintf("%s must include Kubernetes recommended labels: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels", [name])
14+
msg := sprintf("%s must include Kubernetes recommended labels: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels", [name])
1515
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package main
2+
3+
name = input.metadata.name
4+
5+
required_deployment_labels {
6+
input.metadata.labels["app.kubernetes.io/name"]
7+
input.metadata.labels["app.kubernetes.io/instance"]
8+
}
9+
10+
deny[msg] {
11+
input.kind = "Deployment"
12+
print(name)
13+
not required_deployment_labels
14+
msg := sprintf("%s must include Kubernetes recommended labels: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels", [name])
15+
}

internal/runner/verify.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@ func (r *VerifyRunner) Run(ctx context.Context) ([]output.CheckResult, []*tester
5050
engine.EnableTracing()
5151
}
5252

53-
runner := tester.NewRunner().SetCompiler(engine.Compiler()).SetStore(engine.Store()).SetModules(engine.Modules()).EnableTracing(enableTracing).SetRuntime(engine.Runtime())
53+
runner := tester.NewRunner().
54+
SetCompiler(engine.Compiler()).
55+
SetStore(engine.Store()).
56+
SetModules(engine.Modules()).
57+
EnableTracing(enableTracing).
58+
SetRuntime(engine.Runtime())
5459
ch, err := runner.RunTests(ctx, nil)
5560
if err != nil {
5661
return nil, nil, fmt.Errorf("running tests: %w", err)

output/result.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import "fmt"
66
type Result struct {
77
Message string `json:"msg"`
88
Metadata map[string]interface{} `json:"metadata,omitempty"`
9+
Outputs []string `json:"outputs,omitempty"`
910
}
1011

1112
// NewResult creates a new result. An error is returned if the
@@ -52,6 +53,10 @@ type QueryResult struct {
5253
// Traces represents a single trace of how the query was
5354
// evaluated. Each trace value is a trace line.
5455
Traces []string `json:"traces"`
56+
57+
// Output represents anything print()'ed during the query
58+
// evaluation. Each value is a print() call's result.
59+
Outputs []string `json:"outputs,omitempty"`
5560
}
5661

5762
// Passed returns true if all of the results in the query

output/standard.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ func (s *Standard) Output(results []CheckResult) error {
5353
return nil
5454
}
5555

56+
s.outputPrints(results, colorizer)
57+
5658
var totalFailures int
5759
var totalExceptions int
5860
var totalWarnings int
@@ -150,6 +152,16 @@ func (s *Standard) Output(results []CheckResult) error {
150152
return nil
151153
}
152154

155+
func (s *Standard) outputPrints(results []CheckResult, colorizer aurora.Aurora) {
156+
for _, result := range results {
157+
for _, query := range result.Queries {
158+
for _, t := range query.Outputs {
159+
fmt.Fprintln(s.Writer, colorizer.Colorize("PRNT ", aurora.BlueFg), "", t)
160+
}
161+
}
162+
}
163+
}
164+
153165
func (s *Standard) outputTrace(results []CheckResult, colorizer aurora.Aurora) {
154166
for _, result := range results {
155167
for _, query := range result.Queries {
@@ -169,7 +181,7 @@ func (s *Standard) outputTrace(results []CheckResult, colorizer aurora.Aurora) {
169181
}
170182
}
171183

172-
// outputs results as a report - similar to OPA test output
184+
// Report outputs results similar to OPA test output
173185
func (s *Standard) Report(results []*tester.Result, flag string) error {
174186
reporter := tester.PrettyReporter{
175187
Verbose: true,
@@ -192,7 +204,8 @@ func (s *Standard) Report(results []*tester.Result, flag string) error {
192204
return nil
193205
}
194206

195-
// Filter traces - returns only failed traces
207+
// filterTrace returns the traces according to flag: only "fails" or "notes", or, with
208+
// flag = "full", all of them
196209
func filterTrace(trace []*topdown.Event, flag string) []*topdown.Event {
197210
if flag == "full" {
198211
return trace

policy/engine.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/open-policy-agent/opa/rego"
1919
"github.com/open-policy-agent/opa/storage"
2020
"github.com/open-policy-agent/opa/storage/inmem"
21+
"github.com/open-policy-agent/opa/topdown/print"
2122
"github.com/open-policy-agent/opa/version"
2223
)
2324

@@ -40,13 +41,15 @@ func Load(ctx context.Context, policyPaths []string) (*Engine, error) {
4041
return nil, fmt.Errorf("no policies found in %v", policyPaths)
4142
}
4243

43-
compiler, err := policies.Compiler()
44-
if err != nil {
45-
return nil, fmt.Errorf("get compiler: %w", err)
44+
modules := policies.ParsedModules()
45+
compiler := ast.NewCompiler().WithEnablePrintStatements(true)
46+
compiler.Compile(modules)
47+
if compiler.Failed() {
48+
return nil, fmt.Errorf("get compiler: %w", compiler.Errors)
4649
}
4750

48-
policyContents := make(map[string]string)
49-
for path, module := range policies.ParsedModules() {
51+
policyContents := make(map[string]string, len(modules))
52+
for path, module := range modules {
5053
path = filepath.Clean(path)
5154
path = filepath.ToSlash(path)
5255

@@ -327,8 +330,7 @@ func (e *Engine) check(ctx context.Context, path string, config interface{}, nam
327330
checkResult.Warnings = append(checkResult.Warnings, warnings...)
328331
checkResult.Exceptions = append(checkResult.Exceptions, exceptions...)
329332

330-
checkResult.Queries = append(checkResult.Queries, exceptionQueryResult)
331-
checkResult.Queries = append(checkResult.Queries, ruleQueryResult)
333+
checkResult.Queries = append(checkResult.Queries, exceptionQueryResult, ruleQueryResult)
332334
}
333335

334336
// Only a single success result is returned when a given rule succeeds, even if there are multiple occurrences
@@ -384,13 +386,15 @@ func (e *Engine) addFileInfo(ctx context.Context, path string) error {
384386
// data.main.deny to query the deny rule in the main namespace
385387
// data.main.warn to query the warn rule in the main namespace
386388
func (e *Engine) query(ctx context.Context, input interface{}, query string) (output.QueryResult, error) {
389+
ph := printHook{s: &[]string{}}
387390
options := []func(r *rego.Rego){
388391
rego.Input(input),
389392
rego.Query(query),
390393
rego.Compiler(e.Compiler()),
391394
rego.Store(e.Store()),
392395
rego.Runtime(e.Runtime()),
393396
rego.Trace(e.trace),
397+
rego.PrintHook(ph),
394398
}
395399

396400
regoInstance := rego.New(options...)
@@ -456,6 +460,7 @@ func (e *Engine) query(ctx context.Context, input interface{}, query string) (ou
456460
Query: query,
457461
Results: results,
458462
Traces: traces,
463+
Outputs: *ph.s,
459464
}
460465

461466
return queryResult, nil
@@ -488,3 +493,12 @@ func removeRulePrefix(rule string) string {
488493

489494
return rule
490495
}
496+
497+
type printHook struct {
498+
s *[]string
499+
}
500+
501+
func (ph printHook) Print(pctx print.Context, msg string) error {
502+
*ph.s = append(*ph.s, fmt.Sprintf("%v: %s\n", pctx.Location, msg))
503+
return nil
504+
}

0 commit comments

Comments
 (0)