mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
d2a415e25d
A tiny first step to have more tooling correctly report virtual and base document conflicts, as detailed in #7694. This PR fixes the `opa check` command to report conflicts of this type when the `-b`/`--bundle` flag is provided. The bundle flag is required as without that, `opa check` should only verify policies and not load data at all. While I was in the `cmd` directory, I got annoyed with how many of these commands store the same constants for their `--format` flag, so I decided to fix that too, even if it wasn't related to what I originally planned to do. I hope it's not too distracting. Signed-off-by: Anders Eknert <anders@styra.com>
574 lines
12 KiB
Go
574 lines
12 KiB
Go
// Copyright 2024 The OPA Authors. All rights reserved.
|
|
// Use of this source code is governed by an Apache2
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"maps"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/open-policy-agent/opa/cmd/formats"
|
|
"github.com/open-policy-agent/opa/internal/file/archive"
|
|
"github.com/open-policy-agent/opa/v1/util/test"
|
|
)
|
|
|
|
func TestDeps_DefaultRegoVersion(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
module string
|
|
query string
|
|
expErrs []string
|
|
}{
|
|
{
|
|
note: "v0 module",
|
|
module: `package test
|
|
a[x] {
|
|
x := 42
|
|
}`,
|
|
query: `data.test.p`,
|
|
expErrs: []string{
|
|
"test.rego:2: rego_parse_error: `if` keyword is required before rule body",
|
|
"test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules",
|
|
},
|
|
},
|
|
{
|
|
note: "v1 module",
|
|
module: `package test
|
|
a contains x if {
|
|
x := 42
|
|
}`,
|
|
query: `data.test.a`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
files := map[string]string{
|
|
"test.rego": tc.module,
|
|
}
|
|
|
|
test.WithTempFS(files, func(rootPath string) {
|
|
params := newDepsCommandParams()
|
|
_ = params.outputFormat.Set(formats.Pretty)
|
|
|
|
for f := range files {
|
|
_ = params.dataPaths.Set(filepath.Join(rootPath, f))
|
|
}
|
|
|
|
err := deps([]string{tc.query}, params, io.Discard)
|
|
|
|
if len(tc.expErrs) > 0 {
|
|
if err == nil {
|
|
t.Fatalf("Expected error but got nil")
|
|
}
|
|
for _, expErr := range tc.expErrs {
|
|
if !strings.Contains(err.Error(), expErr) {
|
|
t.Fatalf("Expected error:\n\n%s\n\ngot:\n\n%s", expErr, err.Error())
|
|
}
|
|
}
|
|
} else if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDepsCompatibleFlags(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
v0Compatible bool
|
|
v1Compatible bool
|
|
module string
|
|
query string
|
|
expErrs []string
|
|
}{
|
|
{
|
|
note: "v0, no keywords",
|
|
v0Compatible: true,
|
|
module: `package test
|
|
p[3] {
|
|
input.x = 1
|
|
}`,
|
|
query: `data.test.p`,
|
|
},
|
|
{
|
|
note: "v0, keywords not imported, but used",
|
|
v0Compatible: true,
|
|
module: `package test
|
|
p contains 3 if {
|
|
input.x = 1
|
|
}`,
|
|
query: `data.test.p`,
|
|
expErrs: []string{
|
|
"rego_parse_error: var cannot be used for rule name",
|
|
"rego_parse_error: number cannot be used for rule name",
|
|
},
|
|
},
|
|
{
|
|
note: "v0, keywords imported",
|
|
v0Compatible: true,
|
|
module: `package test
|
|
import future.keywords
|
|
p contains 3 if {
|
|
input.x = 1
|
|
}`,
|
|
query: `data.test.p`,
|
|
},
|
|
{
|
|
note: "v0, rego.v1 imported",
|
|
v0Compatible: true,
|
|
module: `package test
|
|
import rego.v1
|
|
p contains 3 if {
|
|
input.x = 1
|
|
}`,
|
|
query: `data.test.p`,
|
|
},
|
|
{
|
|
note: "v1, no keywords",
|
|
v1Compatible: true,
|
|
module: `package test
|
|
p[3] {
|
|
input.x = 1
|
|
}`,
|
|
query: `data.test.p`,
|
|
expErrs: []string{
|
|
"rego_parse_error: `if` keyword is required before rule body",
|
|
"rego_parse_error: `contains` keyword is required for partial set rules",
|
|
},
|
|
},
|
|
{
|
|
note: "v1, no keyword imports",
|
|
v1Compatible: true,
|
|
module: `package test
|
|
p contains 3 if {
|
|
input.x = 1
|
|
}`,
|
|
query: `data.test.p`,
|
|
},
|
|
{
|
|
note: "v1, keywords imported",
|
|
v1Compatible: true,
|
|
module: `package test
|
|
import future.keywords
|
|
p contains 3 if {
|
|
input.x = 1
|
|
}`,
|
|
query: `data.test.p`,
|
|
},
|
|
{
|
|
note: "v1, rego.v1 imported",
|
|
v1Compatible: true,
|
|
module: `package test
|
|
import rego.v1
|
|
p contains 3 if {
|
|
input.x = 1
|
|
}`,
|
|
query: `data.test.p`,
|
|
},
|
|
// v0 takes precedence over v1
|
|
{
|
|
note: "v0+v1, no keywords",
|
|
v0Compatible: true,
|
|
v1Compatible: true,
|
|
module: `package test
|
|
p[3] {
|
|
input.x = 1
|
|
}`,
|
|
query: `data.test.p`,
|
|
},
|
|
{
|
|
note: "v0+v1, keywords not imported, but used",
|
|
v0Compatible: true,
|
|
v1Compatible: true,
|
|
module: `package test
|
|
p contains 3 if {
|
|
input.x = 1
|
|
}`,
|
|
query: `data.test.p`,
|
|
expErrs: []string{
|
|
"rego_parse_error: var cannot be used for rule name",
|
|
"rego_parse_error: number cannot be used for rule name",
|
|
},
|
|
},
|
|
{
|
|
note: "v0+v1, keywords imported",
|
|
v0Compatible: true,
|
|
v1Compatible: true,
|
|
module: `package test
|
|
import future.keywords
|
|
p contains 3 if {
|
|
input.x = 1
|
|
}`,
|
|
query: `data.test.p`,
|
|
},
|
|
{
|
|
note: "v0+v1, rego.v1 imported",
|
|
v0Compatible: true,
|
|
v1Compatible: true,
|
|
module: `package test
|
|
import rego.v1
|
|
p contains 3 if {
|
|
input.x = 1
|
|
}`,
|
|
query: `data.test.p`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
files := map[string]string{
|
|
"test.rego": tc.module,
|
|
}
|
|
|
|
test.WithTempFS(files, func(rootPath string) {
|
|
params := newDepsCommandParams()
|
|
params.v0Compatible = tc.v0Compatible
|
|
params.v1Compatible = tc.v1Compatible
|
|
_ = params.outputFormat.Set(formats.Pretty)
|
|
|
|
for f := range files {
|
|
_ = params.dataPaths.Set(filepath.Join(rootPath, f))
|
|
}
|
|
|
|
err := deps([]string{tc.query}, params, io.Discard)
|
|
|
|
if len(tc.expErrs) > 0 {
|
|
if err == nil {
|
|
t.Fatalf("Expected error but got nil")
|
|
}
|
|
for _, expErr := range tc.expErrs {
|
|
if !strings.Contains(err.Error(), expErr) {
|
|
t.Fatalf("Expected error:\n\n%s\n\ngot:\n\n%s", expErr, err.Error())
|
|
}
|
|
}
|
|
} else if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDepsV1WithBundleRegoVersion(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
files map[string]string
|
|
query string
|
|
expErrs []string
|
|
}{
|
|
{
|
|
note: "v0.x bundle, no keywords",
|
|
files: map[string]string{
|
|
".manifest": `{"rego_version": 0}`,
|
|
"policy.rego": `package test
|
|
p[3] {
|
|
input.x = 1
|
|
}`,
|
|
},
|
|
query: `data.test.p`,
|
|
},
|
|
{
|
|
note: "v0.x bundle, keywords not imported, but used",
|
|
files: map[string]string{
|
|
".manifest": `{"rego_version": 0}`,
|
|
"policy.rego": `package test
|
|
p contains 3 if {
|
|
input.x = 1
|
|
}`,
|
|
},
|
|
query: `data.test.p`,
|
|
expErrs: []string{
|
|
"rego_parse_error: var cannot be used for rule name",
|
|
"rego_parse_error: number cannot be used for rule name",
|
|
},
|
|
},
|
|
{
|
|
note: "v0.x bundle, keywords imported",
|
|
files: map[string]string{
|
|
".manifest": `{"rego_version": 0}`,
|
|
"policy.rego": `package test
|
|
import future.keywords
|
|
p contains 3 if {
|
|
input.x = 1
|
|
}`,
|
|
},
|
|
query: `data.test.p`,
|
|
},
|
|
{
|
|
note: "v0.x bundle, rego.v1 imported",
|
|
files: map[string]string{
|
|
".manifest": `{"rego_version": 0}`,
|
|
"policy.rego": `package test
|
|
import rego.v1
|
|
p contains 3 if {
|
|
input.x = 1
|
|
}`,
|
|
},
|
|
query: `data.test.p`,
|
|
},
|
|
{
|
|
note: "v0 bundle, v1 per-file override",
|
|
files: map[string]string{
|
|
".manifest": `{
|
|
"rego_version": 0,
|
|
"file_rego_versions": {
|
|
"/policy2.rego": 1
|
|
}
|
|
}`,
|
|
"policy1.rego": `package test
|
|
p[3] {
|
|
input.x = 1
|
|
}`,
|
|
"policy2.rego": `package test
|
|
p contains 4 if {
|
|
input.x = 1
|
|
}`,
|
|
},
|
|
},
|
|
{
|
|
note: "v0 bundle, v1 per-file override (glob)",
|
|
files: map[string]string{
|
|
".manifest": `{
|
|
"rego_version": 0,
|
|
"file_rego_versions": {
|
|
"/bar/*.rego": 1
|
|
}
|
|
}`,
|
|
"foo/policy1.rego": `package test
|
|
p[3] {
|
|
input.x = 1
|
|
}`,
|
|
"bar/policy2.rego": `package test
|
|
p contains 4 if {
|
|
input.x = 1
|
|
}`,
|
|
},
|
|
},
|
|
{
|
|
note: "v0 bundle, v1 per-file override, incompliant",
|
|
files: map[string]string{
|
|
".manifest": `{
|
|
"rego_version": 0,
|
|
"file_rego_versions": {
|
|
"/policy2.rego": 1
|
|
}
|
|
}`,
|
|
"policy1.rego": `package test
|
|
p[3] {
|
|
input.x = 1
|
|
}`,
|
|
"policy2.rego": `package test
|
|
p[4] {
|
|
input.x = 1
|
|
}`,
|
|
},
|
|
expErrs: []string{
|
|
"rego_parse_error: `if` keyword is required before rule body",
|
|
"rego_parse_error: `contains` keyword is required for partial set rules",
|
|
},
|
|
},
|
|
{
|
|
note: "v1.0 bundle, no keywords",
|
|
files: map[string]string{
|
|
".manifest": `{"rego_version": 1}`,
|
|
"policy.rego": `package test
|
|
p[3] {
|
|
input.x = 1
|
|
}`,
|
|
},
|
|
query: `data.test.p`,
|
|
expErrs: []string{
|
|
"rego_parse_error: `if` keyword is required before rule body",
|
|
"rego_parse_error: `contains` keyword is required for partial set rules",
|
|
},
|
|
},
|
|
{
|
|
note: "v1.0 bundle, no keyword imports",
|
|
files: map[string]string{
|
|
".manifest": `{"rego_version": 1}`,
|
|
"policy.rego": `package test
|
|
p contains 3 if {
|
|
input.x = 1
|
|
}`,
|
|
},
|
|
query: `data.test.p`,
|
|
},
|
|
{
|
|
note: "v1.0 bundle, keywords imported",
|
|
files: map[string]string{
|
|
".manifest": `{"rego_version": 1}`,
|
|
"policy.rego": `package test
|
|
import future.keywords
|
|
p contains 3 if {
|
|
input.x = 1
|
|
}`,
|
|
},
|
|
query: `data.test.p`,
|
|
},
|
|
{
|
|
note: "v1.0 bundle, rego.v1 imported",
|
|
files: map[string]string{
|
|
".manifest": `{"rego_version": 1}`,
|
|
"policy.rego": `package test
|
|
import rego.v1
|
|
p contains 3 if {
|
|
input.x = 1
|
|
}`,
|
|
},
|
|
query: `data.test.p`,
|
|
},
|
|
{
|
|
note: "v1 bundle, v0 per-file override",
|
|
files: map[string]string{
|
|
".manifest": `{
|
|
"rego_version": 1,
|
|
"file_rego_versions": {
|
|
"/policy1.rego": 0
|
|
}
|
|
}`,
|
|
"policy1.rego": `package test
|
|
p[3] {
|
|
input.x = 1
|
|
}`,
|
|
"policy2.rego": `package test
|
|
p contains 4 if {
|
|
input.x = 1
|
|
}`,
|
|
},
|
|
},
|
|
{
|
|
note: "v1 bundle, v0 per-file override (glob)",
|
|
files: map[string]string{
|
|
".manifest": `{
|
|
"rego_version": 1,
|
|
"file_rego_versions": {
|
|
"/foo/*.rego": 0
|
|
}
|
|
}`,
|
|
"foo/policy1.rego": `package test
|
|
p[3] {
|
|
input.x = 1
|
|
}`,
|
|
"bar/policy2.rego": `package test
|
|
p contains 4 if {
|
|
input.x = 1
|
|
}`,
|
|
},
|
|
},
|
|
{
|
|
note: "v1 bundle, v0 per-file override, incompliant",
|
|
files: map[string]string{
|
|
".manifest": `{
|
|
"rego_version": 1,
|
|
"file_rego_versions": {
|
|
"/policy1.rego": 0
|
|
}
|
|
}`,
|
|
"policy1.rego": `package test
|
|
p contains 3 if {
|
|
input.x = 1
|
|
}`,
|
|
"policy2.rego": `package test
|
|
p contains 4 if {
|
|
input.x = 1
|
|
}`,
|
|
},
|
|
expErrs: []string{
|
|
"rego_parse_error: var cannot be used for rule name",
|
|
"rego_parse_error: number cannot be used for rule name",
|
|
},
|
|
},
|
|
}
|
|
|
|
bundleTypeCases := []struct {
|
|
note string
|
|
tar bool
|
|
}{
|
|
{
|
|
"bundle dir", false,
|
|
},
|
|
{
|
|
"bundle tar", true,
|
|
},
|
|
}
|
|
|
|
v1CompatibleFlagCases := []struct {
|
|
note string
|
|
used bool
|
|
}{
|
|
{
|
|
"no --v1-compatible", false,
|
|
},
|
|
{
|
|
"--v1-compatible", true,
|
|
},
|
|
}
|
|
|
|
for _, bundleType := range bundleTypeCases {
|
|
for _, v1CompatibleFlag := range v1CompatibleFlagCases {
|
|
for _, tc := range tests {
|
|
t.Run(fmt.Sprintf("%s, %s, %s", bundleType.note, v1CompatibleFlag.note, tc.note), func(t *testing.T) {
|
|
files := map[string]string{}
|
|
|
|
if bundleType.tar {
|
|
files["bundle.tar.gz"] = ""
|
|
} else {
|
|
maps.Copy(files, tc.files)
|
|
}
|
|
|
|
test.WithTempFS(files, func(root string) {
|
|
p := root
|
|
if bundleType.tar {
|
|
p = filepath.Join(root, "bundle.tar.gz")
|
|
files := make([][2]string, 0, len(tc.files))
|
|
for k, v := range tc.files {
|
|
files = append(files, [2]string{k, v})
|
|
}
|
|
buf := archive.MustWriteTarGz(files)
|
|
bf, err := os.Create(p)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
_, err = bf.Write(buf.Bytes())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
params := newDepsCommandParams()
|
|
if err := params.bundlePaths.Set(p); err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
params.v1Compatible = v1CompatibleFlag.used
|
|
_ = params.outputFormat.Set(formats.Pretty)
|
|
|
|
err := deps([]string{tc.query}, params, io.Discard)
|
|
|
|
if len(tc.expErrs) > 0 {
|
|
if err == nil {
|
|
t.Fatalf("Expected error but got nil")
|
|
}
|
|
for _, expErr := range tc.expErrs {
|
|
if !strings.Contains(err.Error(), expErr) {
|
|
t.Fatalf("Expected error:\n\n%s\n\ngot:\n\n%s", expErr, err.Error())
|
|
}
|
|
}
|
|
} else if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|