Skip to content

Commit 8497550

Browse files
authored
Adding --rego-v1 flag to check cmd (#6430)
When enabled, checked module(s) must be compliant with OPA 1.0 Rego. Fixes: #6429 Signed-off-by: Johan Fylling <johan.dev@fylling.se>
1 parent 26a02e4 commit 8497550

File tree

5 files changed

+146
-2
lines changed

5 files changed

+146
-2
lines changed

ast/parser.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ type ParserOptions struct {
106106
SkipRules bool
107107
JSONOptions *astJSON.Options
108108
unreleasedKeywords bool // TODO(sr): cleanup
109+
RegoV1Compatible bool
109110
}
110111

111112
// NewParser creates and initializes a Parser.

ast/parser_ext.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ func ParseModuleWithOpts(filename, input string, popts ParserOptions) (*Module,
477477
if err != nil {
478478
return nil, err
479479
}
480-
return parseModule(filename, stmts, comments)
480+
return parseModule(filename, stmts, comments, popts.RegoV1Compatible)
481481
}
482482

483483
// ParseBody returns exactly one body.
@@ -637,7 +637,7 @@ func ParseStatementsWithOpts(filename, input string, popts ParserOptions) ([]Sta
637637
return stmts, comments, nil
638638
}
639639

640-
func parseModule(filename string, stmts []Statement, comments []*Comment) (*Module, error) {
640+
func parseModule(filename string, stmts []Statement, comments []*Comment, regoV1Compatible bool) (*Module, error) {
641641

642642
if len(stmts) == 0 {
643643
return nil, NewError(ParseErr, &Location{File: filename}, "empty module")
@@ -658,6 +658,7 @@ func parseModule(filename string, stmts []Statement, comments []*Comment) (*Modu
658658

659659
// The comments slice only holds comments that were not their own statements.
660660
mod.Comments = append(mod.Comments, comments...)
661+
mod.regoV1Compatible = regoV1Compatible
661662

662663
for i, stmt := range stmts[1:] {
663664
switch stmt := stmt.(type) {

cmd/check.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type checkParams struct {
2525
capabilities *capabilitiesFlag
2626
schema *schemaFlags
2727
strict bool
28+
regoV1 bool
2829
}
2930

3031
func newCheckParams() checkParams {
@@ -64,6 +65,7 @@ func checkModules(params checkParams, args []string) error {
6465
if params.bundleMode {
6566
for _, path := range args {
6667
b, err := loader.NewFileLoader().
68+
WithRegoV1Compatible(params.regoV1).
6769
WithSkipBundleVerification(true).
6870
WithProcessAnnotation(true).
6971
WithCapabilities(capabilities).
@@ -82,6 +84,7 @@ func checkModules(params checkParams, args []string) error {
8284
}
8385

8486
result, err := loader.NewFileLoader().
87+
WithRegoV1Compatible(params.regoV1).
8588
WithProcessAnnotation(true).
8689
WithCapabilities(capabilities).
8790
Filtered(args, f.Apply)
@@ -165,5 +168,6 @@ func init() {
165168
addCapabilitiesFlag(checkCommand.Flags(), checkParams.capabilities)
166169
addSchemaFlags(checkCommand.Flags(), checkParams.schema)
167170
addStrictFlag(checkCommand.Flags(), &checkParams.strict, false)
171+
checkCommand.Flags().BoolVar(&checkParams.regoV1, "rego-v1", false, "check for Rego v1 compatibility (policies must also be compatible with current OPA version)")
168172
RootCommand.AddCommand(checkCommand)
169173
}

cmd/check_test.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,134 @@ p {
236236
t.Fatalf("unexpected error from eval with inlined schema, got: %v", err)
237237
}
238238
}
239+
240+
func TestCheckRegoV1(t *testing.T) {
241+
cases := []struct {
242+
note string
243+
policy string
244+
expErrs []string
245+
}{
246+
{
247+
note: "rego.v1 imported, v1 compliant",
248+
policy: `package test
249+
import rego.v1
250+
p contains x if {
251+
x := [1,2,3]
252+
}`,
253+
},
254+
{
255+
note: "rego.v1 imported, NOT v1 compliant (parser)",
256+
policy: `package test
257+
import rego.v1
258+
p contains x {
259+
x := [1,2,3]
260+
}
261+
262+
q.r`,
263+
expErrs: []string{
264+
"test.rego:3: rego_parse_error: `if` keyword is required before rule body",
265+
"test.rego:7: rego_parse_error: `contains` keyword is required for partial set rules",
266+
},
267+
},
268+
{
269+
note: "rego.v1 imported, NOT v1 compliant (compiler)",
270+
policy: `package test
271+
import rego.v1
272+
273+
import data.foo
274+
import data.bar as foo
275+
`,
276+
expErrs: []string{
277+
"test.rego:5: rego_compile_error: import must not shadow import data.foo",
278+
},
279+
},
280+
{
281+
note: "keywords imported, v1 compliant",
282+
policy: `package test
283+
import future.keywords.if
284+
import future.keywords.contains
285+
p contains x if {
286+
x := [1,2,3]
287+
}`,
288+
},
289+
{
290+
note: "keywords imported, NOT v1 compliant",
291+
policy: `package test
292+
import future.keywords.contains
293+
p contains x {
294+
x := [1,2,3]
295+
}
296+
297+
q.r`,
298+
expErrs: []string{
299+
"test.rego:3: rego_parse_error: `if` keyword is required before rule body",
300+
"test.rego:7: rego_parse_error: `contains` keyword is required for partial set rules",
301+
},
302+
},
303+
{
304+
note: "keywords imported, NOT v1 compliant (compiler)",
305+
policy: `package test
306+
import future.keywords.if
307+
308+
input := 1 if {
309+
1 == 2
310+
}`,
311+
expErrs: []string{
312+
"test.rego:4: rego_compile_error: rules must not shadow input (use a different rule name)",
313+
},
314+
},
315+
{
316+
note: "no imports, v1 compliant",
317+
policy: `package test
318+
p := 1
319+
`,
320+
},
321+
{
322+
note: "no imports, NOT v1 compliant but v0 compliant (compiler)",
323+
policy: `package test
324+
p.x`,
325+
expErrs: []string{
326+
"test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules",
327+
},
328+
},
329+
{
330+
note: "no imports, v1 compliant but NOT v0 compliant",
331+
policy: `package test
332+
p contains x if {
333+
x := [1,2,3]
334+
}`,
335+
expErrs: []string{
336+
"test.rego:2: rego_parse_error: var cannot be used for rule name", // This error actually appears three times: once for 'p'; once for 'contains'; and once for 'x'. All are interpreted as [invalid] rule declarations with no value and body.
337+
"test.rego:2: rego_parse_error: `if` keyword is required before rule body",
338+
},
339+
},
340+
}
341+
342+
for _, tc := range cases {
343+
t.Run(tc.note, func(t *testing.T) {
344+
files := map[string]string{
345+
"test.rego": tc.policy,
346+
}
347+
348+
test.WithTempFS(files, func(root string) {
349+
params := newCheckParams()
350+
params.regoV1 = true
351+
352+
err := checkModules(params, []string{root})
353+
switch {
354+
case err != nil && len(tc.expErrs) > 0:
355+
for _, expErr := range tc.expErrs {
356+
if !strings.Contains(err.Error(), expErr) {
357+
t.Fatalf("expected err:\n\n%v\n\ngot:\n\n%v", expErr, err)
358+
}
359+
}
360+
return // don't read back bundle below
361+
case err != nil && len(tc.expErrs) == 0:
362+
t.Fatalf("unexpected error: %v", err)
363+
case err == nil && len(tc.expErrs) > 0:
364+
t.Fatalf("expected error:\n\n%v\n\ngot: none", tc.expErrs)
365+
}
366+
})
367+
})
368+
}
369+
}

loader/loader.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ type FileLoader interface {
102102
WithProcessAnnotation(bool) FileLoader
103103
WithCapabilities(*ast.Capabilities) FileLoader
104104
WithJSONOptions(*astJSON.Options) FileLoader
105+
106+
WithRegoV1Compatible(bool) FileLoader
105107
}
106108

107109
// NewFileLoader returns a new FileLoader instance.
@@ -181,6 +183,11 @@ func (fl *fileLoader) WithJSONOptions(opts *astJSON.Options) FileLoader {
181183
return fl
182184
}
183185

186+
func (fl *fileLoader) WithRegoV1Compatible(compatible bool) FileLoader {
187+
fl.opts.RegoV1Compatible = compatible
188+
return fl
189+
}
190+
184191
// All returns a Result object loaded (recursively) from the specified paths.
185192
func (fl fileLoader) All(paths []string) (*Result, error) {
186193
return fl.Filtered(paths, nil)

0 commit comments

Comments
 (0)