mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
Rego v1 capabilities and keywords update (#7216)
* Separating v0- and v1 keywords * Adding `rego_v1` capability feature Signed-off-by: Johan Fylling <johan.dev@fylling.se>
This commit is contained in:
@@ -352,23 +352,23 @@ jobs:
|
||||
version: edge
|
||||
|
||||
- name: Test policies
|
||||
run: opa test build/policy
|
||||
run: opa test --v0-compatible build/policy
|
||||
|
||||
- name: Ensure proper formatting
|
||||
run: opa fmt --list --fail build/policy
|
||||
run: opa fmt --v0-compatible --list --fail build/policy
|
||||
|
||||
- name: Run file policy checks on changed files
|
||||
run: |
|
||||
curl --silent --fail --header 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' -o files.json \
|
||||
https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files
|
||||
|
||||
opa eval -d build/policy/files.rego -d build/policy/helpers.rego --format values --input files.json \
|
||||
opa eval --v0-compatible -d build/policy/files.rego -d build/policy/helpers.rego --format values --input files.json \
|
||||
--fail-defined 'data.files.deny[message]'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Show input on failure
|
||||
run: opa eval --input files.json --format pretty input
|
||||
run: opa eval --v0-compatible --input files.json --format pretty input
|
||||
if: ${{ failure() }}
|
||||
|
||||
- name: Setup Hugo
|
||||
@@ -386,4 +386,4 @@ jobs:
|
||||
cd docs
|
||||
make dev-generate hugo-production-build
|
||||
cd -
|
||||
opa eval 'data.integrations.deny[message]' -i docs/website/public/index.json -d build/policy/integrations.rego --format=values --fail-defined
|
||||
opa eval --v0-compatible 'data.integrations.deny[message]' -i docs/website/public/index.json -d build/policy/integrations.rego --format=values --fail-defined
|
||||
|
||||
+2
-1
@@ -21,6 +21,7 @@ type VersionIndex = v1.VersionIndex
|
||||
// heads, they wouldn't be able to parse them.
|
||||
const FeatureRefHeadStringPrefixes = v1.FeatureRefHeadStringPrefixes
|
||||
const FeatureRefHeads = v1.FeatureRefHeads
|
||||
const FeatureRegoV1 = v1.FeatureRegoV1
|
||||
const FeatureRegoV1Import = v1.FeatureRegoV1Import
|
||||
|
||||
// Capabilities defines a structure containing data that describes the capabilities
|
||||
@@ -33,7 +34,7 @@ type WasmABIVersion = v1.WasmABIVersion
|
||||
|
||||
// CapabilitiesForThisVersion returns the capabilities of this version of OPA.
|
||||
func CapabilitiesForThisVersion() *Capabilities {
|
||||
return v1.CapabilitiesForThisVersion()
|
||||
return v1.CapabilitiesForThisVersion(v1.CapabilitiesRegoVersion(DefaultRegoVersion))
|
||||
}
|
||||
|
||||
// LoadCapabilitiesJSON loads a JSON serialized capabilities structure from the reader r.
|
||||
|
||||
@@ -43,3 +43,7 @@ type ParserOptions = v1.ParserOptions
|
||||
func NewParser() *Parser {
|
||||
return v1.NewParser().WithRegoVersion(DefaultRegoVersion)
|
||||
}
|
||||
|
||||
func IsFutureKeyword(s string) bool {
|
||||
return v1.IsFutureKeywordForRegoVersion(s, RegoV0)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
|
||||
package files
|
||||
|
||||
import future.keywords.contains
|
||||
import future.keywords.if
|
||||
import future.keywords.in
|
||||
import rego.v1
|
||||
|
||||
import data.helpers.basename
|
||||
import data.helpers.directory
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
package files_test
|
||||
|
||||
import future.keywords.in
|
||||
import rego.v1
|
||||
|
||||
import data.files.deny
|
||||
|
||||
test_deny_invalid_yaml_file {
|
||||
test_deny_invalid_yaml_file if {
|
||||
expected := "invalid.yaml is an invalid YAML file: {null{}}"
|
||||
expected in deny with data.files.yaml_file_contents as {"invalid.yaml": "{null{}}"}
|
||||
with data.files.changes as {"invalid.yaml": {"status": "modified"}}
|
||||
}
|
||||
|
||||
test_allow_valid_yaml_file {
|
||||
test_allow_valid_yaml_file if {
|
||||
count(deny) == 0 with data.files.yaml_file_contents as {"valid.yaml": "foo: bar"}
|
||||
with data.files.changes as {"valid.yaml": {"status": "modified"}}
|
||||
}
|
||||
|
||||
test_deny_invalid_json_file {
|
||||
test_deny_invalid_json_file if {
|
||||
expected := "invalid.json is an invalid JSON file: }}}"
|
||||
expected in deny with data.files.json_file_contents as {"invalid.json": "}}}"}
|
||||
with data.files.changes as {"invalid.json": {"status": "modified"}}
|
||||
}
|
||||
|
||||
test_allow_valid_json_file {
|
||||
test_allow_valid_json_file if {
|
||||
count(deny) == 0 with data.files.json_file_contents as {"valid.json": "{\"foo\": \"bar\"}"}
|
||||
with data.files.changes as {"valid.json": {"status": "modified"}}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package helpers
|
||||
|
||||
import future.keywords.if
|
||||
import future.keywords.in
|
||||
import rego.v1
|
||||
|
||||
last_indexof(string, search) := i if {
|
||||
all := [i | chars := split(string, ""); chars[i] == search]
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package integrations
|
||||
|
||||
import future.keywords.contains
|
||||
import future.keywords.if
|
||||
import future.keywords.in
|
||||
import rego.v1
|
||||
|
||||
allowed_image_extensions := ["png", "svg"]
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package integrations_test
|
||||
|
||||
import future.keywords.in
|
||||
import rego.v1
|
||||
|
||||
messages_for_key(key, output) = messages {
|
||||
messages_for_key(key, output) = messages if {
|
||||
messages := {m |
|
||||
some e
|
||||
output[e]
|
||||
@@ -15,18 +15,18 @@ messages_for_key(key, output) = messages {
|
||||
|
||||
print_if(true, _, _, _) = true
|
||||
|
||||
print_if(false, key, false, output) := false {
|
||||
print_if(false, key, false, output) := false if {
|
||||
print("Exp:", {})
|
||||
print("Got: ", messages_for_key(key, output))
|
||||
}
|
||||
|
||||
print_if(false, key, expected, output) := false {
|
||||
print_if(false, key, expected, output) := false if {
|
||||
is_string(expected)
|
||||
print("Exp:", expected)
|
||||
print("Got:", messages_for_key(key, output))
|
||||
}
|
||||
|
||||
test_integration_has_valid_key {
|
||||
test_integration_has_valid_key if {
|
||||
output := data.integrations.deny with input as {"integrations": {"/integrations/in.valid/": {"link": "https://example.com/", "title": "Example"}}}
|
||||
|
||||
key := "key"
|
||||
@@ -40,7 +40,7 @@ test_integration_has_valid_key {
|
||||
print_if(result, key, message, output)
|
||||
}
|
||||
|
||||
test_integration_has_required_fields_missing {
|
||||
test_integration_has_required_fields_missing if {
|
||||
output := data.integrations.deny with input as {"integrations": {"/integrations/regal/": {}}}
|
||||
|
||||
key := "fields"
|
||||
@@ -53,7 +53,7 @@ test_integration_has_required_fields_missing {
|
||||
print_if(result, key, message, output)
|
||||
}
|
||||
|
||||
test_integration_has_required_fields_present {
|
||||
test_integration_has_required_fields_present if {
|
||||
output := data.integrations.deny with input as {"integrations": {"/integrations/regal/": {"title": "Regal"}}}
|
||||
|
||||
key := "fields"
|
||||
@@ -66,7 +66,7 @@ test_integration_has_required_fields_present {
|
||||
print_if(result, key, false, output)
|
||||
}
|
||||
|
||||
test_integration_has_content_missing {
|
||||
test_integration_has_content_missing if {
|
||||
output := data.integrations.deny with input as {"integrations": {"/integrations/regal/": {}}}
|
||||
|
||||
key := "content"
|
||||
@@ -79,7 +79,7 @@ test_integration_has_content_missing {
|
||||
print_if(result, key, message, output)
|
||||
}
|
||||
|
||||
test_integration_has_content_blank {
|
||||
test_integration_has_content_blank if {
|
||||
output := data.integrations.deny with input as {"integrations": {"/integrations/regal/": {"content": "\t\t\n "}}}
|
||||
|
||||
key := "content"
|
||||
@@ -92,7 +92,7 @@ test_integration_has_content_blank {
|
||||
print_if(result, key, message, output)
|
||||
}
|
||||
|
||||
test_integration_has_content_present {
|
||||
test_integration_has_content_present if {
|
||||
output := data.integrations.deny with input as {"integrations": {"/integrations/regal/": {"content": "foobar"}}}
|
||||
|
||||
key := "content"
|
||||
@@ -103,7 +103,7 @@ test_integration_has_content_present {
|
||||
print_if(result, key, false, output)
|
||||
}
|
||||
|
||||
test_every_integration_has_image_missing {
|
||||
test_every_integration_has_image_missing if {
|
||||
output := data.integrations.deny with input as {
|
||||
"images": ["reegal.png"],
|
||||
"integrations": {"/integrations/regal/": {}},
|
||||
@@ -119,7 +119,7 @@ test_every_integration_has_image_missing {
|
||||
print_if(result, key, message, output)
|
||||
}
|
||||
|
||||
test_every_integration_has_image_present {
|
||||
test_every_integration_has_image_present if {
|
||||
output := data.integrations.deny with input as {
|
||||
"images": ["regal.png"],
|
||||
"integrations": {"regal": {}},
|
||||
@@ -133,7 +133,7 @@ test_every_integration_has_image_present {
|
||||
print_if(result, key, false, output)
|
||||
}
|
||||
|
||||
test_every_integration_has_image_missing_but_permitted {
|
||||
test_every_integration_has_image_missing_but_permitted if {
|
||||
output := data.integrations.deny with input as {
|
||||
"images": ["reegal.png"],
|
||||
"integrations": {"regal": {"allow_missing_image": true}},
|
||||
@@ -148,7 +148,7 @@ test_every_integration_has_image_missing_but_permitted {
|
||||
print_if(result, key, false, output)
|
||||
}
|
||||
|
||||
test_every_image_has_integration_missing {
|
||||
test_every_image_has_integration_missing if {
|
||||
output := data.integrations.deny with input as {
|
||||
"images": ["regal.png"],
|
||||
"integrations": {"foobar": {}},
|
||||
@@ -164,7 +164,7 @@ test_every_image_has_integration_missing {
|
||||
print_if(result, key, message, output)
|
||||
}
|
||||
|
||||
test_every_image_has_integration_present {
|
||||
test_every_image_has_integration_present if {
|
||||
output := data.integrations.deny with input as {
|
||||
"images": ["regal.png"],
|
||||
"integrations": {"/integrations/regal/": {}},
|
||||
@@ -178,7 +178,7 @@ test_every_image_has_integration_present {
|
||||
print_if(result, key, false, output)
|
||||
}
|
||||
|
||||
test_integration_organizations_missing {
|
||||
test_integration_organizations_missing if {
|
||||
output := data.integrations.deny with input as {
|
||||
"organizations": {"/organizations/stira/": {}},
|
||||
"integrations": {"/integrations/regal/": {"inventors": ["styra"]}},
|
||||
@@ -194,7 +194,7 @@ test_integration_organizations_missing {
|
||||
print_if(result, key, message, output)
|
||||
}
|
||||
|
||||
test_integration_organizations_present {
|
||||
test_integration_organizations_present if {
|
||||
output := data.integrations.deny with input as {
|
||||
"organizations": {"/organizations/styra/": {}},
|
||||
"integrations": {"/integrations/regal/": {"inventors": ["styra"]}},
|
||||
@@ -208,7 +208,7 @@ test_integration_organizations_present {
|
||||
print_if(result, key, false, output)
|
||||
}
|
||||
|
||||
test_integration_softwares_missing {
|
||||
test_integration_softwares_missing if {
|
||||
output := data.integrations.deny with input as {
|
||||
"softwares": {"/softwares/mars/": {}},
|
||||
"integrations": {"/integrations/regal/": {"software": ["terraform"]}},
|
||||
@@ -224,7 +224,7 @@ test_integration_softwares_missing {
|
||||
print_if(result, key, message, output)
|
||||
}
|
||||
|
||||
test_integration_softwares_present {
|
||||
test_integration_softwares_present if {
|
||||
output := data.integrations.deny with input as {
|
||||
"softwares": {"/softwares/terraform/": {}},
|
||||
"integrations": {"/integrations/regal/": {"software": ["terraform"]}},
|
||||
@@ -238,7 +238,7 @@ test_integration_softwares_present {
|
||||
print_if(result, key, false, output)
|
||||
}
|
||||
|
||||
test_software_has_required_fields_missing {
|
||||
test_software_has_required_fields_missing if {
|
||||
output := data.integrations.deny with input as {"softwares": {"/softwares/terraform/": {}}}
|
||||
|
||||
key := "fields"
|
||||
@@ -251,7 +251,7 @@ test_software_has_required_fields_missing {
|
||||
print_if(result, key, message, output)
|
||||
}
|
||||
|
||||
test_software_has_required_fields_present {
|
||||
test_software_has_required_fields_present if {
|
||||
output := data.integrations.deny with input as {"softwares": {"terraform": {"link": "https://www.terraform.io/", "title": "Terraform"}}}
|
||||
|
||||
key := "fields"
|
||||
@@ -263,7 +263,7 @@ test_software_has_required_fields_present {
|
||||
print_if(result, key, false, output)
|
||||
}
|
||||
|
||||
test_organization_has_required_labels {
|
||||
test_organization_has_required_labels if {
|
||||
output := data.integrations.deny with input as {"organizations": {"/organizations/styra/": {}}}
|
||||
|
||||
key := "fields"
|
||||
@@ -276,7 +276,7 @@ test_organization_has_required_labels {
|
||||
print_if(result, key, message, output)
|
||||
}
|
||||
|
||||
test_organization_has_required_fields_present {
|
||||
test_organization_has_required_fields_present if {
|
||||
output := data.integrations.deny with input as {"organizations": {"styra": {"link": "https://styra.com/", "title": "Styra"}}}
|
||||
|
||||
key := "fields"
|
||||
@@ -288,7 +288,7 @@ test_organization_has_required_fields_present {
|
||||
print_if(result, key, false, output)
|
||||
}
|
||||
|
||||
test_organization_has_valid_key {
|
||||
test_organization_has_valid_key if {
|
||||
output := data.integrations.deny with input as {"organizations": {"/organizations/sty.ra/": {"link": "https://styra.com/", "title": "Styra"}}}
|
||||
|
||||
key := "key"
|
||||
@@ -302,7 +302,7 @@ test_organization_has_valid_key {
|
||||
print_if(result, key, message, output)
|
||||
}
|
||||
|
||||
test_organization_has_one_or_more_integrations_none {
|
||||
test_organization_has_one_or_more_integrations_none if {
|
||||
output := data.integrations.deny with input as {"organizations": {"/organizations/foobar/": {}}, "integrations": {}}
|
||||
|
||||
key := "orphaned_org"
|
||||
@@ -315,7 +315,7 @@ test_organization_has_one_or_more_integrations_none {
|
||||
print_if(result, key, message, output)
|
||||
}
|
||||
|
||||
test_organization_has_one_or_more_integrations_one {
|
||||
test_organization_has_one_or_more_integrations_one if {
|
||||
output := data.integrations.deny with input as {"organizations": {"/organizations/foobaz/": {}}, "integrations": {"/integrations/foobar/": {"inventors": ["foobaz"]}}}
|
||||
|
||||
key := "orphaned_org"
|
||||
@@ -326,7 +326,7 @@ test_organization_has_one_or_more_integrations_one {
|
||||
print_if(result, key, false, output)
|
||||
}
|
||||
|
||||
test_organization_has_one_or_more_integrations_speaker {
|
||||
test_organization_has_one_or_more_integrations_speaker if {
|
||||
output := data.integrations.deny with input as {"organizations": {"foobaz": {}}, "integrations": {"foobar": {"videos": [{"speakers": [{"organization": "foobaz"}]}]}}}
|
||||
|
||||
key := "orphaned_org"
|
||||
@@ -337,7 +337,7 @@ test_organization_has_one_or_more_integrations_speaker {
|
||||
print_if(result, key, false, output)
|
||||
}
|
||||
|
||||
test_software_has_one_or_more_integrations_none {
|
||||
test_software_has_one_or_more_integrations_none if {
|
||||
output := data.integrations.deny with input as {"softwares": {"/softwares/foobar/": {}}, "integrations": {}}
|
||||
|
||||
key := "orphaned_software"
|
||||
@@ -350,7 +350,7 @@ test_software_has_one_or_more_integrations_none {
|
||||
print_if(result, key, message, output)
|
||||
}
|
||||
|
||||
test_software_has_one_or_more_integrations_one {
|
||||
test_software_has_one_or_more_integrations_one if {
|
||||
output := data.integrations.deny with input as {"softwares": {"foobaz": {}}, "integrations": {"foobar": {"software": ["foobaz"]}}}
|
||||
|
||||
key := "orphaned_software"
|
||||
@@ -361,7 +361,7 @@ test_software_has_one_or_more_integrations_one {
|
||||
print_if(result, key, false, output)
|
||||
}
|
||||
|
||||
test_software_has_valid_key {
|
||||
test_software_has_valid_key if {
|
||||
output := data.integrations.deny with input as {"softwares": {"/softwares/in.valid/": {"link": "https://example.com/", "title": "Example"}}}
|
||||
|
||||
key := "key"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1
-9
@@ -4819,12 +4819,6 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"future_keywords": [
|
||||
"contains",
|
||||
"every",
|
||||
"if",
|
||||
"in"
|
||||
],
|
||||
"wasm_abi_versions": [
|
||||
{
|
||||
"version": 1,
|
||||
@@ -4836,8 +4830,6 @@
|
||||
}
|
||||
],
|
||||
"features": [
|
||||
"rule_head_ref_string_prefixes",
|
||||
"rule_head_refs",
|
||||
"rego_v1_import"
|
||||
"rego_v1"
|
||||
]
|
||||
}
|
||||
|
||||
+3
-2
@@ -62,8 +62,9 @@ func newBenchmarkEvalParams() benchmarkCommandParams {
|
||||
evalPrettyOutput,
|
||||
benchmarkGoBenchOutput,
|
||||
}),
|
||||
target: util.NewEnumFlag(compile.TargetRego, []string{compile.TargetRego, compile.TargetWasm}),
|
||||
schema: &schemaFlags{},
|
||||
target: util.NewEnumFlag(compile.TargetRego, []string{compile.TargetRego, compile.TargetWasm}),
|
||||
schema: &schemaFlags{},
|
||||
capabilities: newcapabilitiesFlag(),
|
||||
},
|
||||
gracefulShutdownPeriod: 10,
|
||||
}
|
||||
|
||||
+13
-9
@@ -57,6 +57,17 @@ func newBuildParams() buildParams {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *buildParams) regoVersion() ast.RegoVersion {
|
||||
if p.v0Compatible {
|
||||
// v0 takes precedence over v1
|
||||
return ast.RegoV0
|
||||
}
|
||||
if p.v1Compatible {
|
||||
return ast.RegoV1
|
||||
}
|
||||
return ast.DefaultRegoVersion
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
buildParams := newBuildParams()
|
||||
@@ -292,7 +303,7 @@ func dobuild(params buildParams, args []string) error {
|
||||
if params.capabilities.C != nil {
|
||||
capabilities = params.capabilities.C
|
||||
} else {
|
||||
capabilities = ast.CapabilitiesForThisVersion()
|
||||
capabilities = ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(params.regoVersion()))
|
||||
}
|
||||
|
||||
compiler := compile.New().
|
||||
@@ -311,14 +322,7 @@ func dobuild(params buildParams, args []string) error {
|
||||
WithPartialNamespace(params.ns).
|
||||
WithFollowSymlinks(params.followSymlinks)
|
||||
|
||||
regoVersion := ast.DefaultRegoVersion
|
||||
if params.v0Compatible {
|
||||
// v0 takes precedence over v1
|
||||
regoVersion = ast.RegoV0
|
||||
} else if params.v1Compatible {
|
||||
regoVersion = ast.RegoV1
|
||||
}
|
||||
compiler = compiler.WithRegoVersion(regoVersion)
|
||||
compiler = compiler.WithRegoVersion(params.regoVersion())
|
||||
|
||||
if params.revision.isSet {
|
||||
compiler = compiler.WithRevision(*params.revision.v)
|
||||
|
||||
+459
-2
@@ -108,6 +108,7 @@ p { is_foo("bar") }`,
|
||||
caps: func() string {
|
||||
c := ast.CapabilitiesForThisVersion()
|
||||
c.FutureKeywords = []string{"in"}
|
||||
c.Features = []string{}
|
||||
j, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -120,6 +121,23 @@ import future.keywords.in
|
||||
p if "opa" in input.tools`,
|
||||
err: "rego_parse_error: unexpected keyword, must be one of [in]",
|
||||
},
|
||||
{
|
||||
note: "future kw NOT defined in caps, rego-v1 feature",
|
||||
caps: func() string {
|
||||
c := ast.CapabilitiesForThisVersion()
|
||||
c.FutureKeywords = []string{"in"}
|
||||
c.Features = []string{ast.FeatureRegoV1}
|
||||
j, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(j)
|
||||
}(),
|
||||
policy: `package test
|
||||
import future.keywords.if
|
||||
import future.keywords.in
|
||||
p if "opa" in input.tools`,
|
||||
},
|
||||
{
|
||||
note: "future kw are defined in caps",
|
||||
caps: func() string {
|
||||
@@ -136,6 +154,34 @@ import future.keywords.if
|
||||
import future.keywords.in
|
||||
p if "opa" in input.tools`,
|
||||
},
|
||||
{
|
||||
note: "rego.v1 imported AND defined in capabilities",
|
||||
caps: func() string {
|
||||
c := ast.CapabilitiesForThisVersion()
|
||||
c.Features = []string{ast.FeatureRegoV1Import}
|
||||
j, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(j)
|
||||
}(),
|
||||
policy: `package test
|
||||
import rego.v1`,
|
||||
},
|
||||
{
|
||||
note: "rego.v1 imported AND rego-v1 in capabilities",
|
||||
caps: func() string {
|
||||
c := ast.CapabilitiesForThisVersion()
|
||||
c.Features = []string{ast.FeatureRegoV1}
|
||||
j, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(j)
|
||||
}(),
|
||||
policy: `package test
|
||||
import rego.v1`,
|
||||
},
|
||||
}
|
||||
|
||||
// add same tests for bundle-mode == true:
|
||||
@@ -1098,7 +1144,7 @@ p contains 2 if {
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "multiple bundles with different rego-versions (v0-compatible)",
|
||||
note: "multiple bundles with different rego-versions, v0-compatible",
|
||||
v0Compatible: true,
|
||||
roots: []string{"bundle1", "bundle2"},
|
||||
files: map[string]string{
|
||||
@@ -1133,10 +1179,60 @@ p[4] {
|
||||
input.x == 1
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
// capabilities inferred from --v0-compatible doesn't include rego_v1 feature, which must be respected
|
||||
"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "multiple bundles with different rego-versions, v0-compatible, rego_v1 capabilities feature",
|
||||
v0Compatible: true,
|
||||
roots: []string{"bundle1", "bundle2"},
|
||||
files: map[string]string{
|
||||
"bundle1/.manifest": `{
|
||||
"roots": ["test1"],
|
||||
"rego_version": 0,
|
||||
"file_rego_versions": {
|
||||
"*/test2.rego": 1
|
||||
}
|
||||
}`,
|
||||
"bundle1/test1.rego": `package test1
|
||||
p[1] {
|
||||
input.x == 1
|
||||
}`,
|
||||
"bundle1/test2.rego": `package test1
|
||||
p contains 2 if {
|
||||
input.x == 1
|
||||
}`,
|
||||
"bundle2/.manifest": `{
|
||||
"roots": ["test2"],
|
||||
"rego_version": 1,
|
||||
"file_rego_versions": {
|
||||
"*/test4.rego": 0
|
||||
}
|
||||
}`,
|
||||
"bundle2/test3.rego": `package test2
|
||||
p contains 3 if {
|
||||
input.x == 1
|
||||
}`,
|
||||
"bundle2/test4.rego": `package test2
|
||||
p[4] {
|
||||
input.x == 1
|
||||
}`,
|
||||
"capabilities.json": func() string {
|
||||
caps := ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0))
|
||||
caps.Features = append(caps.Features, ast.FeatureRegoV1)
|
||||
bs, err := json.Marshal(caps)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(bs)
|
||||
}(),
|
||||
},
|
||||
expManifest: `{"revision":"","roots":["test1","test2"],"rego_version":0,"file_rego_versions":{"%ROOT%/bundle1/test2.rego":1,"%ROOT%/bundle2/test3.rego":1}}`,
|
||||
},
|
||||
{
|
||||
note: "multiple bundles with different rego-versions (v1-compatible)",
|
||||
note: "multiple bundles with different rego-versions, v1-compatible",
|
||||
v1Compatible: true,
|
||||
roots: []string{"bundle1", "bundle2"},
|
||||
files: map[string]string{
|
||||
@@ -1184,6 +1280,10 @@ p[4] {
|
||||
params.v0Compatible = tc.v0Compatible
|
||||
params.v1Compatible = tc.v1Compatible
|
||||
|
||||
if _, ok := tc.files["capabilities.json"]; ok {
|
||||
_ = params.capabilities.Set(path.Join(root, "capabilities.json"))
|
||||
}
|
||||
|
||||
var roots []string
|
||||
if len(tc.roots) == 0 {
|
||||
roots = []string{root}
|
||||
@@ -1259,6 +1359,7 @@ func TestBuildBundleFromOtherBundles(t *testing.T) {
|
||||
note string
|
||||
v0Compatible bool
|
||||
v1Compatible bool
|
||||
capabilities *ast.Capabilities
|
||||
bundles map[string]bundleInfo
|
||||
expBundle bundleInfo
|
||||
expErrs []string
|
||||
@@ -1520,6 +1621,37 @@ p {
|
||||
"policy_1.rego": `package test
|
||||
q contains 1 if {
|
||||
input.x == 1
|
||||
}`,
|
||||
},
|
||||
},
|
||||
expErrs: []string{
|
||||
// capabilities inferred from --v0-compatible doesn't include rego_v1 feature, which must be respected
|
||||
"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "single v1 bundle, v0 per-file override, --v0-compatible, rego_v1 capabilities feature",
|
||||
v0Compatible: true,
|
||||
capabilities: func() *ast.Capabilities {
|
||||
caps := ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0))
|
||||
caps.Features = append(caps.Features, ast.FeatureRegoV1)
|
||||
return caps
|
||||
}(),
|
||||
bundles: map[string]bundleInfo{
|
||||
"bundle.tar.gz": {
|
||||
".manifest": `{
|
||||
"rego_version": 1,
|
||||
"file_rego_versions": {
|
||||
"/policy_0.rego": 0
|
||||
}
|
||||
}`,
|
||||
"policy_0.rego": `package test
|
||||
p {
|
||||
input.x == 1
|
||||
}`,
|
||||
"policy_1.rego": `package test
|
||||
q contains 1 if {
|
||||
input.x == 1
|
||||
}`,
|
||||
},
|
||||
},
|
||||
@@ -1558,6 +1690,35 @@ p {
|
||||
"policy.rego": `package test2
|
||||
q contains 1 if {
|
||||
input.x == 1
|
||||
}`,
|
||||
},
|
||||
},
|
||||
expErrs: []string{
|
||||
// capabilities inferred from --v0-compatible doesn't include rego_v1 feature, which must be respected
|
||||
"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v0 bundle + v1 bundle, --v0-compatible, rego_v1 capabilities feature",
|
||||
v0Compatible: true,
|
||||
capabilities: func() *ast.Capabilities {
|
||||
caps := ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0))
|
||||
caps.Features = append(caps.Features, ast.FeatureRegoV1)
|
||||
return caps
|
||||
}(),
|
||||
bundles: map[string]bundleInfo{
|
||||
"bundle_v0.tar.gz": {
|
||||
".manifest": `{"roots": ["test1"], "rego_version": 0}`,
|
||||
"policy.rego": `package test1
|
||||
p {
|
||||
input.x == 1
|
||||
}`,
|
||||
},
|
||||
"bundle_v1.tar.gz": {
|
||||
".manifest": `{"roots": ["test2"], "rego_version": 1}`,
|
||||
"policy.rego": `package test2
|
||||
q contains 1 if {
|
||||
input.x == 1
|
||||
}`,
|
||||
},
|
||||
},
|
||||
@@ -1649,6 +1810,10 @@ q contains 1 if {
|
||||
params.v0Compatible = tc.v0Compatible
|
||||
params.v1Compatible = tc.v1Compatible
|
||||
|
||||
if tc.capabilities != nil {
|
||||
params.capabilities.C = tc.capabilities
|
||||
}
|
||||
|
||||
err := dobuild(params, roots)
|
||||
if tc.expErrs != nil {
|
||||
if err == nil {
|
||||
@@ -1837,6 +2002,298 @@ p contains x if {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildWithRegoV1Capability(t *testing.T) {
|
||||
tests := []struct {
|
||||
note string
|
||||
v0Compatible bool
|
||||
capabilities *ast.Capabilities
|
||||
files map[string]string
|
||||
expFiles map[string]string
|
||||
expErrs []string
|
||||
}{
|
||||
{
|
||||
note: "v0 module, v0-compatible, no capabilities",
|
||||
v0Compatible: true,
|
||||
files: map[string]string{
|
||||
"test.rego": `package test
|
||||
p[x] {
|
||||
x := 42
|
||||
}`,
|
||||
},
|
||||
expFiles: map[string]string{
|
||||
".manifest": `{"revision":"","roots":[""],"rego_version":0}
|
||||
`,
|
||||
"test.rego": `package test
|
||||
|
||||
p[x] {
|
||||
x := 42
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v0 module, v0-compatible, v0 capabilities",
|
||||
v0Compatible: true,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
||||
files: map[string]string{
|
||||
"test.rego": `package test
|
||||
p[x] {
|
||||
x := 42
|
||||
}`,
|
||||
},
|
||||
expFiles: map[string]string{
|
||||
".manifest": `{"revision":"","roots":[""],"rego_version":0}
|
||||
`,
|
||||
"test.rego": `package test
|
||||
|
||||
p[x] {
|
||||
x := 42
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v0 module, v0-compatible, v1 capabilities",
|
||||
v0Compatible: true,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
||||
files: map[string]string{
|
||||
"test.rego": `package test
|
||||
p[x] {
|
||||
x := 42
|
||||
}`,
|
||||
},
|
||||
expFiles: map[string]string{
|
||||
".manifest": `{"revision":"","roots":[""],"rego_version":0}
|
||||
`,
|
||||
"test.rego": `package test
|
||||
|
||||
p[x] {
|
||||
x := 42
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
note: "v0 module, not v0-compatible, no capabilities",
|
||||
files: map[string]string{
|
||||
"test.rego": `package test
|
||||
p[x] {
|
||||
x := 42
|
||||
}`,
|
||||
},
|
||||
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: "v0 module, not v0-compatible, v0 capabilities",
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
||||
files: map[string]string{
|
||||
"test.rego": `package test
|
||||
p[x] {
|
||||
x := 42
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v0 module, not v0-compatible, v1 capabilities",
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
||||
files: map[string]string{
|
||||
"test.rego": `package test
|
||||
p[x] {
|
||||
x := 42
|
||||
}`,
|
||||
},
|
||||
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, v0-compatible, no capabilities",
|
||||
v0Compatible: true,
|
||||
files: map[string]string{
|
||||
"test.rego": `package test
|
||||
|
||||
p contains x if {
|
||||
x := 42
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
"test.rego:3: rego_parse_error: var cannot be used for rule name",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 module, v0-compatible, v0 capabilities",
|
||||
v0Compatible: true,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
||||
files: map[string]string{
|
||||
"test.rego": `package test
|
||||
|
||||
p contains x if {
|
||||
x := 42
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
"test.rego:3: rego_parse_error: var cannot be used for rule name",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 module, v0-compatible, v1 capabilities",
|
||||
v0Compatible: true,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
||||
files: map[string]string{
|
||||
"test.rego": `package test
|
||||
|
||||
p contains x if {
|
||||
x := 42
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
"test.rego:3: rego_parse_error: var cannot be used for rule name",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
note: "v1 module, not v0-compatible, no capabilities",
|
||||
files: map[string]string{
|
||||
"test.rego": `package test
|
||||
|
||||
p contains x if {
|
||||
x := 42
|
||||
}`,
|
||||
},
|
||||
expFiles: map[string]string{
|
||||
".manifest": `{"revision":"","roots":[""],"rego_version":1}
|
||||
`,
|
||||
"test.rego": `package test
|
||||
|
||||
p contains x if {
|
||||
x := 42
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 module, not v0-compatible, v0 capabilities",
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
||||
files: map[string]string{
|
||||
"test.rego": `package test
|
||||
|
||||
p contains x if {
|
||||
x := 42
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 module, not v0-compatible, v1 capabilities",
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
||||
files: map[string]string{
|
||||
"test.rego": `package test
|
||||
|
||||
p contains x if {
|
||||
x := 42
|
||||
}`,
|
||||
},
|
||||
expFiles: map[string]string{
|
||||
".manifest": `{"revision":"","roots":[""],"rego_version":1}
|
||||
`,
|
||||
"test.rego": `package test
|
||||
|
||||
p contains x if {
|
||||
x := 42
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
test.WithTempFS(tc.files, func(root string) {
|
||||
params := newBuildParams()
|
||||
params.outputFile = path.Join(root, "bundle.tar.gz")
|
||||
params.v0Compatible = tc.v0Compatible
|
||||
params.capabilities.C = tc.capabilities
|
||||
|
||||
err := dobuild(params, []string{root})
|
||||
|
||||
if len(tc.expErrs) > 0 {
|
||||
if err == nil {
|
||||
t.Fatal("expected error but got nil")
|
||||
}
|
||||
for _, expErr := range tc.expErrs {
|
||||
if !strings.Contains(err.Error(), expErr) {
|
||||
t.Fatalf("expected error:\n\n%v\n\ngot:\n\n%v", expErr, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fl := loader.NewFileLoader()
|
||||
_, err = fl.AsBundle(params.outputFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that manifest is not written given no input manifest and no other flags
|
||||
f, err := os.Open(params.outputFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
gr, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tr := tar.NewReader(gr)
|
||||
|
||||
foundFiles := map[string]struct{}{}
|
||||
for {
|
||||
f, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
foundFiles[path.Base(f.Name)] = struct{}{}
|
||||
expectedFile := tc.expFiles[path.Base(f.Name)]
|
||||
if expectedFile != "" {
|
||||
data, err := io.ReadAll(tr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actualFile := string(data)
|
||||
if actualFile != expectedFile {
|
||||
t.Fatalf("expected file %s to be:\n\n%v\n\ngot:\n\n%v", f.Name, expectedFile, actualFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for expectedFile := range tc.expFiles {
|
||||
if _, ok := foundFiles[expectedFile]; !ok {
|
||||
t.Fatalf("expected file %s not found in bundle, got: %v", expectedFile, foundFiles)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildWithCompatibleFlags(t *testing.T) {
|
||||
tests := []struct {
|
||||
note string
|
||||
|
||||
+14
-5
@@ -15,9 +15,17 @@ import (
|
||||
)
|
||||
|
||||
type capabilitiesParams struct {
|
||||
showCurrent bool
|
||||
version string
|
||||
file string
|
||||
showCurrent bool
|
||||
version string
|
||||
file string
|
||||
v0Compatible bool
|
||||
}
|
||||
|
||||
func (p *capabilitiesParams) regoVersion() ast.RegoVersion {
|
||||
if p.v0Compatible {
|
||||
return ast.RegoV0
|
||||
}
|
||||
return ast.DefaultRegoVersion
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -84,7 +92,8 @@ Print the capabilities of a capabilities file
|
||||
}
|
||||
capabilitiesCommand.Flags().BoolVar(&capabilitiesParams.showCurrent, "current", false, "print current capabilities")
|
||||
capabilitiesCommand.Flags().StringVar(&capabilitiesParams.version, "version", "", "print capabilities of a specific version")
|
||||
capabilitiesCommand.Flags().StringVar(&capabilitiesParams.file, "file", "", "print current capabilities")
|
||||
capabilitiesCommand.Flags().StringVar(&capabilitiesParams.file, "file", "", "print capabilities defined by a file")
|
||||
addV0CompatibleFlag(capabilitiesCommand.Flags(), &capabilitiesParams.v0Compatible, false)
|
||||
|
||||
RootCommand.AddCommand(capabilitiesCommand)
|
||||
}
|
||||
@@ -100,7 +109,7 @@ func doCapabilities(params capabilitiesParams) (string, error) {
|
||||
} else if len(params.file) > 0 {
|
||||
c, err = ast.LoadCapabilitiesFile(params.file)
|
||||
} else if params.showCurrent {
|
||||
c = ast.CapabilitiesForThisVersion()
|
||||
c = ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(params.regoVersion()))
|
||||
} else {
|
||||
return showVersions()
|
||||
}
|
||||
|
||||
@@ -5,9 +5,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/open-policy-agent/opa/v1/ast"
|
||||
"github.com/open-policy-agent/opa/v1/util/test"
|
||||
)
|
||||
|
||||
@@ -74,3 +78,64 @@ func TestCapabilitiesFile(t *testing.T) {
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func TestCapabilitiesCurrent(t *testing.T) {
|
||||
tests := []struct {
|
||||
note string
|
||||
v0Compatible bool
|
||||
expFeatures []string
|
||||
expFutureKeywords []string
|
||||
}{
|
||||
{
|
||||
note: "current",
|
||||
expFeatures: []string{
|
||||
ast.FeatureRegoV1,
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "current --v0-compatible",
|
||||
v0Compatible: true,
|
||||
expFeatures: []string{
|
||||
ast.FeatureRefHeadStringPrefixes,
|
||||
ast.FeatureRefHeads,
|
||||
ast.FeatureRegoV1Import,
|
||||
},
|
||||
expFutureKeywords: []string{
|
||||
"in",
|
||||
"every",
|
||||
"contains",
|
||||
"if",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
// These are sorted in the output
|
||||
sort.Strings(tc.expFutureKeywords)
|
||||
sort.Strings(tc.expFeatures)
|
||||
|
||||
params := capabilitiesParams{
|
||||
showCurrent: true,
|
||||
v0Compatible: tc.v0Compatible,
|
||||
}
|
||||
capsStr, err := doCapabilities(params)
|
||||
if err != nil {
|
||||
t.Fatal("expected success", err)
|
||||
}
|
||||
|
||||
caps, err := ast.LoadCapabilitiesJSON(bytes.NewReader([]byte(capsStr)))
|
||||
if err != nil {
|
||||
t.Fatal("expected success", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(caps.Features, tc.expFeatures) {
|
||||
t.Errorf("expected features:\n\n%v\n\nbut got:\n\n%v", tc.expFeatures, caps.Features)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(caps.FutureKeywords, tc.expFutureKeywords) {
|
||||
t.Errorf("expected future keywords:\n\n%v\n\nbut got:\n\n%v", tc.expFutureKeywords, caps.FutureKeywords)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -73,7 +73,7 @@ func checkModules(params checkParams, args []string) error {
|
||||
if params.capabilities.C != nil {
|
||||
capabilities = params.capabilities.C
|
||||
} else {
|
||||
capabilities = ast.CapabilitiesForThisVersion()
|
||||
capabilities = ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(params.regoVersion()))
|
||||
}
|
||||
|
||||
ss, err := loader.Schemas(params.schema.path)
|
||||
@@ -194,6 +194,7 @@ func init() {
|
||||
addCapabilitiesFlag(checkCommand.Flags(), checkParams.capabilities)
|
||||
addSchemaFlags(checkCommand.Flags(), checkParams.schema)
|
||||
addStrictFlag(checkCommand.Flags(), &checkParams.strict, false)
|
||||
// FIXME: Rename or add new flag with same effect? '--rego-v1' will become even more confusing in 1.0, as what it actually means is check that module is compatible with BOTH v0 and v1.
|
||||
addRegoV1FlagWithDescription(checkCommand.Flags(), &checkParams.regoV1, false,
|
||||
"check for Rego v1 compatibility (policies must also be compatible with current OPA version)")
|
||||
addV0CompatibleFlag(checkCommand.Flags(), &checkParams.v0Compatible, false)
|
||||
|
||||
@@ -54,6 +54,7 @@ p { is_foo("bar") }`,
|
||||
caps: func() string {
|
||||
c := ast.CapabilitiesForThisVersion()
|
||||
c.FutureKeywords = []string{"in"}
|
||||
c.Features = []string{}
|
||||
j, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -66,6 +67,23 @@ import future.keywords.in
|
||||
p if "opa" in input.tools`,
|
||||
err: "rego_parse_error: unexpected keyword, must be one of [in]",
|
||||
},
|
||||
{
|
||||
note: "future kw NOT defined in caps, rego-v1 feature",
|
||||
caps: func() string {
|
||||
c := ast.CapabilitiesForThisVersion()
|
||||
c.FutureKeywords = []string{"in"}
|
||||
c.Features = []string{ast.FeatureRegoV1}
|
||||
j, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(j)
|
||||
}(),
|
||||
policy: `package test
|
||||
import future.keywords.if
|
||||
import future.keywords.in
|
||||
p if "opa" in input.tools`,
|
||||
},
|
||||
{
|
||||
note: "future kw are defined in caps",
|
||||
caps: func() string {
|
||||
@@ -109,6 +127,20 @@ import rego.v1`,
|
||||
return string(j)
|
||||
}(),
|
||||
policy: `package test
|
||||
import rego.v1`,
|
||||
},
|
||||
{
|
||||
note: "rego.v1 imported AND rego-v1 in capabilities",
|
||||
caps: func() string {
|
||||
c := ast.CapabilitiesForThisVersion()
|
||||
c.Features = []string{ast.FeatureRegoV1}
|
||||
j, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(j)
|
||||
}(),
|
||||
policy: `package test
|
||||
import rego.v1`,
|
||||
},
|
||||
}
|
||||
@@ -461,6 +493,170 @@ a contains x if {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckWithRegoV1Capability(t *testing.T) {
|
||||
cases := []struct {
|
||||
note string
|
||||
v0Compatible bool
|
||||
capabilities *ast.Capabilities
|
||||
policy string
|
||||
expErrs []string
|
||||
}{
|
||||
{
|
||||
note: "v0 module, v0-compatible, no capabilities",
|
||||
v0Compatible: true,
|
||||
policy: `package test
|
||||
a[x] {
|
||||
x := 42
|
||||
}`,
|
||||
},
|
||||
{
|
||||
note: "v0 module, v0-compatible, v0 capabilities",
|
||||
v0Compatible: true,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
||||
policy: `package test
|
||||
a[x] {
|
||||
x := 42
|
||||
}`,
|
||||
},
|
||||
{
|
||||
note: "v0 module, v0-compatible, v1 capabilities",
|
||||
v0Compatible: true,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
||||
policy: `package test
|
||||
a[x] {
|
||||
x := 42
|
||||
}`,
|
||||
},
|
||||
|
||||
{
|
||||
note: "v0 module, not v0-compatible, no capabilities",
|
||||
policy: `package test
|
||||
a[x] {
|
||||
x := 42
|
||||
}`,
|
||||
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: "v0 module, not v0-compatible, v0 capabilities",
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
||||
policy: `package test
|
||||
a[x] {
|
||||
x := 42
|
||||
}`,
|
||||
expErrs: []string{
|
||||
"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v0 module, not v0-compatible, v1 capabilities",
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
||||
policy: `package test
|
||||
a[x] {
|
||||
x := 42
|
||||
}`,
|
||||
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, v0-compatible, no capabilities",
|
||||
v0Compatible: true,
|
||||
policy: `package test
|
||||
a contains x if {
|
||||
x := 42
|
||||
}`,
|
||||
expErrs: []string{
|
||||
"test.rego:2: rego_parse_error: var cannot be used for rule name",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 module, v0-compatible, v0 capabilities",
|
||||
v0Compatible: true,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
||||
policy: `package test
|
||||
a contains x if {
|
||||
x := 42
|
||||
}`,
|
||||
expErrs: []string{
|
||||
"test.rego:2: rego_parse_error: var cannot be used for rule name",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 module, v0-compatible, v1 capabilities",
|
||||
v0Compatible: true,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
||||
policy: `package test
|
||||
a contains x if {
|
||||
x := 42
|
||||
}`,
|
||||
expErrs: []string{
|
||||
"test.rego:2: rego_parse_error: var cannot be used for rule name",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
note: "v1 module, not v0-compatible, no capabilities",
|
||||
policy: `package test
|
||||
a contains x if {
|
||||
x := 42
|
||||
}`,
|
||||
},
|
||||
{
|
||||
note: "v1 module, not v0-compatible, v0 capabilities",
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
||||
policy: `package test
|
||||
a contains x if {
|
||||
x := 42
|
||||
}`,
|
||||
expErrs: []string{
|
||||
"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 module, not v0-compatible, v1 capabilities",
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
||||
policy: `package test
|
||||
a contains x if {
|
||||
x := 42
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"test.rego": tc.policy,
|
||||
}
|
||||
|
||||
test.WithTempFS(files, func(root string) {
|
||||
params := newCheckParams()
|
||||
params.v0Compatible = tc.v0Compatible
|
||||
params.capabilities.C = tc.capabilities
|
||||
|
||||
err := checkModules(params, []string{root})
|
||||
switch {
|
||||
case err != nil && len(tc.expErrs) > 0:
|
||||
for _, expErr := range tc.expErrs {
|
||||
if !strings.Contains(err.Error(), expErr) {
|
||||
t.Fatalf("expected err:\n\n%v\n\ngot:\n\n%v", expErr, err)
|
||||
}
|
||||
}
|
||||
return // don't read back bundle below
|
||||
case err != nil && len(tc.expErrs) == 0:
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
case err == nil && len(tc.expErrs) > 0:
|
||||
t.Fatalf("expected error:\n\n%v\n\ngot: none", tc.expErrs)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckCompatibleFlags(t *testing.T) {
|
||||
cases := []struct {
|
||||
note string
|
||||
|
||||
+4
-2
@@ -705,8 +705,10 @@ func setupEval(args []string, params evalCommandParams) (*evalContext, error) {
|
||||
regoArgs = append(regoArgs, rego.BuiltinErrorList(&builtInErrors))
|
||||
}
|
||||
|
||||
if params.capabilities != nil {
|
||||
if params.capabilities.C != nil {
|
||||
regoArgs = append(regoArgs, rego.Capabilities(params.capabilities.C))
|
||||
} else {
|
||||
regoArgs = append(regoArgs, rego.Capabilities(ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(params.regoVersion()))))
|
||||
}
|
||||
|
||||
if params.strict {
|
||||
@@ -878,7 +880,7 @@ func generateOptimizedBundle(params evalCommandParams, asBundle bool, filter loa
|
||||
if params.capabilities.C != nil {
|
||||
capabilities = params.capabilities.C
|
||||
} else {
|
||||
capabilities = ast.CapabilitiesForThisVersion()
|
||||
capabilities = ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(params.regoVersion()))
|
||||
}
|
||||
|
||||
compiler := compile.New().
|
||||
|
||||
@@ -2907,6 +2907,224 @@ func TestEvalPolicyWithCompatibleFlags(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvalPolicyWithRegoV1Capability(t *testing.T) {
|
||||
tests := []struct {
|
||||
note string
|
||||
v0Compatible bool
|
||||
capabilities *ast.Capabilities
|
||||
modules map[string]string
|
||||
expErrs []string
|
||||
}{
|
||||
{
|
||||
note: "v0 module, v0-compatible, no capabilities",
|
||||
v0Compatible: true,
|
||||
modules: map[string]string{
|
||||
"test.rego": `package test
|
||||
allow {
|
||||
1 < 2
|
||||
}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v0 module, v0-compatible, v0 capabilities",
|
||||
v0Compatible: true,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
||||
modules: map[string]string{
|
||||
"test.rego": `package test
|
||||
allow {
|
||||
1 < 2
|
||||
}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v0 module, v0-compatible, v1 capabilities",
|
||||
v0Compatible: true,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
||||
modules: map[string]string{
|
||||
"test.rego": `package test
|
||||
allow {
|
||||
1 < 2
|
||||
}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v0 module, not v0-compatible, no capabilities",
|
||||
v0Compatible: false,
|
||||
modules: map[string]string{
|
||||
"test.rego": `package test
|
||||
allow {
|
||||
1 < 2
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
"test.rego:2: rego_parse_error: `if` keyword is required before rule body",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v0 module, not v0-compatible, v0 capabilities",
|
||||
v0Compatible: false,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
||||
modules: map[string]string{
|
||||
"test.rego": `package test
|
||||
allow {
|
||||
1 < 2
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v0 module, not v0-compatible, v1 capabilities",
|
||||
v0Compatible: false,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
||||
modules: map[string]string{
|
||||
"test.rego": `package test
|
||||
allow {
|
||||
1 < 2
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
"test.rego:2: rego_parse_error: `if` keyword is required before rule body",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
note: "v1 module, v0-compatible, no capabilities",
|
||||
v0Compatible: true,
|
||||
modules: map[string]string{
|
||||
"test.rego": `package test
|
||||
allow if {
|
||||
1 < 2
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
"test.rego:2: rego_parse_error: var cannot be used for rule name",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 module, v0-compatible, v0 capabilities",
|
||||
v0Compatible: true,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
||||
modules: map[string]string{
|
||||
"test.rego": `package test
|
||||
allow if {
|
||||
1 < 2
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
"test.rego:2: rego_parse_error: var cannot be used for rule name",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 module, v0-compatible, v1 capabilities",
|
||||
v0Compatible: true,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
||||
modules: map[string]string{
|
||||
"test.rego": `package test
|
||||
allow if {
|
||||
1 < 2
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
"test.rego:2: rego_parse_error: var cannot be used for rule name",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 module, not v0-compatible, no capabilities",
|
||||
v0Compatible: false,
|
||||
modules: map[string]string{
|
||||
"test.rego": `package test
|
||||
allow if {
|
||||
1 < 2
|
||||
}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 module, not v0-compatible, v0 capabilities",
|
||||
v0Compatible: false,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
||||
modules: map[string]string{
|
||||
"test.rego": `package test
|
||||
allow if {
|
||||
1 < 2
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 module, not v0-compatible, v1 capabilities",
|
||||
v0Compatible: false,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
||||
modules: map[string]string{
|
||||
"test.rego": `package test
|
||||
allow if {
|
||||
1 < 2
|
||||
}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
setup := []struct {
|
||||
name string
|
||||
commandParams func(params *evalCommandParams, path string)
|
||||
}{
|
||||
{
|
||||
name: "Files",
|
||||
commandParams: func(params *evalCommandParams, path string) {
|
||||
params.dataPaths = newrepeatedStringFlag([]string{path})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Bundle",
|
||||
commandParams: func(params *evalCommandParams, path string) {
|
||||
if err := params.bundlePaths.Set(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range setup {
|
||||
for _, tc := range tests {
|
||||
t.Run(fmt.Sprintf("%s: %s", s.name, tc.note), func(t *testing.T) {
|
||||
test.WithTempFS(tc.modules, func(path string) {
|
||||
params := newEvalCommandParams()
|
||||
s.commandParams(¶ms, path)
|
||||
_ = params.outputFormat.Set(evalPrettyOutput)
|
||||
params.v0Compatible = tc.v0Compatible
|
||||
params.capabilities.C = tc.capabilities
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
defined, err := eval([]string{"data.test.allow"}, params, &buf)
|
||||
|
||||
if len(tc.expErrs) > 0 {
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got none")
|
||||
}
|
||||
|
||||
actual := buf.String()
|
||||
for _, expErr := range tc.expErrs {
|
||||
if !strings.Contains(actual, expErr) {
|
||||
t.Fatalf("expected error:\n\n%v\n\ngot\n\n%v", expErr, actual)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v, buf: %s", err, buf.String())
|
||||
} else if !defined {
|
||||
t.Fatal("expected result to be defined")
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvalPolicyWithBundleRegoVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
note string
|
||||
|
||||
+15
-16
@@ -54,7 +54,7 @@ func TestDoInspect(t *testing.T) {
|
||||
}
|
||||
|
||||
res := `{
|
||||
"capabilities": {},
|
||||
"capabilities": {"features": ["rego_v1"]},
|
||||
"manifest": {"revision": "rev", "roots": ["foo", "bar", "fuz", "baz", "a", "x"]},
|
||||
"signatures_config": {},
|
||||
"namespaces": {"data": ["/data.json"], "data.foo": ["/example/foo.rego"]}
|
||||
@@ -63,7 +63,7 @@ func TestDoInspect(t *testing.T) {
|
||||
exp := util.MustUnmarshalJSON([]byte(res))
|
||||
result := util.MustUnmarshalJSON(out.Bytes())
|
||||
if !reflect.DeepEqual(exp, result) {
|
||||
t.Fatalf("expected inspect output to be %v, got %v", exp, result)
|
||||
t.Fatalf("expected inspect output to be:\n\n%v\n\ngot:\n\n%v", exp, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1075,6 +1075,9 @@ p if {
|
||||
"type": "function"
|
||||
}
|
||||
}
|
||||
],
|
||||
"features": [
|
||||
"rego_v1"
|
||||
]
|
||||
}
|
||||
}`,
|
||||
@@ -1084,7 +1087,6 @@ p if {
|
||||
note: "known ref replaced inside 'with' stmt",
|
||||
files: [][2]string{
|
||||
{"/policy.rego", `package test
|
||||
import rego.v1
|
||||
|
||||
foo.bar(_) := false
|
||||
|
||||
@@ -1113,7 +1115,7 @@ test_p if {
|
||||
},
|
||||
"capabilities": {
|
||||
"features": [
|
||||
"rego_v1_import"
|
||||
"rego_v1"
|
||||
]
|
||||
}
|
||||
}`,
|
||||
@@ -1122,7 +1124,6 @@ test_p if {
|
||||
note: "unknown ref replaced inside 'with' stmt",
|
||||
files: [][2]string{
|
||||
{"/policy.rego", `package test
|
||||
import rego.v1
|
||||
|
||||
p if {
|
||||
data.foo.bar(42)
|
||||
@@ -1149,7 +1150,7 @@ test_p if {
|
||||
},
|
||||
"capabilities": {
|
||||
"features": [
|
||||
"rego_v1_import"
|
||||
"rego_v1"
|
||||
]
|
||||
}
|
||||
}`,
|
||||
@@ -1158,7 +1159,6 @@ test_p if {
|
||||
note: "unknown built-in (var) replaced inside 'with' stmt",
|
||||
files: [][2]string{
|
||||
{"/policy.rego", `package test
|
||||
import rego.v1
|
||||
|
||||
p if {
|
||||
foo(42)
|
||||
@@ -1185,7 +1185,7 @@ test_p if {
|
||||
},
|
||||
"capabilities": {
|
||||
"features": [
|
||||
"rego_v1_import"
|
||||
"rego_v1"
|
||||
]
|
||||
}
|
||||
}`,
|
||||
@@ -1194,7 +1194,6 @@ test_p if {
|
||||
note: "unknown built-in (ref) replaced inside 'with' stmt",
|
||||
files: [][2]string{
|
||||
{"/policy.rego", `package test
|
||||
import rego.v1
|
||||
|
||||
p if {
|
||||
foo.bar(42)
|
||||
@@ -1221,7 +1220,7 @@ test_p if {
|
||||
},
|
||||
"capabilities": {
|
||||
"features": [
|
||||
"rego_v1_import"
|
||||
"rego_v1"
|
||||
]
|
||||
}
|
||||
}`,
|
||||
@@ -1230,7 +1229,6 @@ test_p if {
|
||||
note: "call replaced by unknown data ref inside 'with' stmt",
|
||||
files: [][2]string{
|
||||
{"/policy.rego", `package test
|
||||
import rego.v1
|
||||
|
||||
p if {
|
||||
foo(42)
|
||||
@@ -1277,7 +1275,7 @@ test_p if {
|
||||
}
|
||||
],
|
||||
"features": [
|
||||
"rego_v1_import"
|
||||
"rego_v1"
|
||||
]
|
||||
}
|
||||
}`,
|
||||
@@ -1286,7 +1284,6 @@ test_p if {
|
||||
note: "call replaced by unknown built-in (var) inside 'with' stmt",
|
||||
files: [][2]string{
|
||||
{"/policy.rego", `package test
|
||||
import rego.v1
|
||||
|
||||
p if {
|
||||
foo(42)
|
||||
@@ -1314,7 +1311,7 @@ test_p if {
|
||||
},
|
||||
"capabilities": {
|
||||
"features": [
|
||||
"rego_v1_import"
|
||||
"rego_v1"
|
||||
]
|
||||
}
|
||||
}`,
|
||||
@@ -1323,7 +1320,6 @@ test_p if {
|
||||
note: "call replaced by unknown built-in (ref) inside 'with' stmt",
|
||||
files: [][2]string{
|
||||
{"/policy.rego", `package test
|
||||
import rego.v1
|
||||
|
||||
p if {
|
||||
foo(42)
|
||||
@@ -1371,7 +1367,7 @@ test_p if {
|
||||
}
|
||||
],
|
||||
"features": [
|
||||
"rego_v1_import"
|
||||
"rego_v1"
|
||||
]
|
||||
}
|
||||
}`,
|
||||
@@ -1509,6 +1505,9 @@ p if {
|
||||
},
|
||||
"infix": "=="
|
||||
}
|
||||
],
|
||||
"features": [
|
||||
"rego_v1"
|
||||
]
|
||||
}
|
||||
}`)
|
||||
|
||||
+8
-2
@@ -116,11 +116,17 @@ func opaTest(args []string, testParams testCommandParams) (int, error) {
|
||||
var bundles map[string]*bundle.Bundle
|
||||
var store storage.Store
|
||||
|
||||
popts := ast.ParserOptions{
|
||||
RegoVersion: testParams.RegoVersion(),
|
||||
Capabilities: testParams.capabilities.C,
|
||||
ProcessAnnotation: true,
|
||||
}
|
||||
|
||||
if testParams.bundleMode {
|
||||
bundles, err = tester.LoadBundlesWithRegoVersion(args, filter.Apply, testParams.RegoVersion())
|
||||
bundles, err = tester.LoadBundlesWithParserOptions(args, filter.Apply, popts)
|
||||
store = inmem.NewWithOpts(inmem.OptRoundTripOnWrite(false))
|
||||
} else {
|
||||
modules, store, err = tester.LoadWithRegoVersion(args, filter.Apply, testParams.RegoVersion())
|
||||
modules, store, err = tester.LoadWithParserOptions(args, filter.Apply, popts)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -2578,6 +2578,318 @@ test_l if {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWithRegoV1Capability(t *testing.T) {
|
||||
tests := []struct {
|
||||
note string
|
||||
v0Compatible bool
|
||||
capabilities *ast.Capabilities
|
||||
files map[string]string
|
||||
expErrs []string
|
||||
}{
|
||||
{
|
||||
note: "v0 module, v0-compatible, no capabilities",
|
||||
v0Compatible: true,
|
||||
files: map[string]string{
|
||||
"/test.rego": `package test
|
||||
|
||||
l1 := {1, 3, 5}
|
||||
l2[v] {
|
||||
v := l1[_]
|
||||
}
|
||||
|
||||
test_l {
|
||||
l1 == l2
|
||||
}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v0 module, v0-compatible, v0 capabilities",
|
||||
v0Compatible: true,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
||||
files: map[string]string{
|
||||
"/test.rego": `package test
|
||||
|
||||
l1 := {1, 3, 5}
|
||||
l2[v] {
|
||||
v := l1[_]
|
||||
}
|
||||
|
||||
test_l {
|
||||
l1 == l2
|
||||
}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v0 module, v0-compatible, v1 capabilities",
|
||||
v0Compatible: true,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
||||
files: map[string]string{
|
||||
"/test.rego": `package test
|
||||
|
||||
l1 := {1, 3, 5}
|
||||
l2[v] {
|
||||
v := l1[_]
|
||||
}
|
||||
|
||||
test_l {
|
||||
l1 == l2
|
||||
}`,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
note: "v0 module, not v0-compatible, no capabilities",
|
||||
files: map[string]string{
|
||||
"/test.rego": `package test
|
||||
|
||||
l1 := {1, 3, 5}
|
||||
l2[v] {
|
||||
v := l1[_]
|
||||
}
|
||||
|
||||
test_l {
|
||||
l1 == l2
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
"test.rego:4: rego_parse_error: `if` keyword is required before rule body",
|
||||
"test.rego:4: rego_parse_error: `contains` keyword is required for partial set rules",
|
||||
"test.rego:8: rego_parse_error: `if` keyword is required before rule body",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v0 module, not v0-compatible, v0 capabilities",
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
||||
files: map[string]string{
|
||||
"/test.rego": `package test
|
||||
|
||||
l1 := {1, 3, 5}
|
||||
l2[v] {
|
||||
v := l1[_]
|
||||
}
|
||||
|
||||
test_l {
|
||||
l1 == l2
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v0 module, not v0-compatible, v1 capabilities",
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
||||
files: map[string]string{
|
||||
"/test.rego": `package test
|
||||
|
||||
l1 := {1, 3, 5}
|
||||
l2[v] {
|
||||
v := l1[_]
|
||||
}
|
||||
|
||||
test_l {
|
||||
l1 == l2
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
"test.rego:4: rego_parse_error: `if` keyword is required before rule body",
|
||||
"test.rego:4: rego_parse_error: `contains` keyword is required for partial set rules",
|
||||
"test.rego:8: rego_parse_error: `if` keyword is required before rule body",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
note: "v1 module, v0-compatible, no capabilities",
|
||||
v0Compatible: true,
|
||||
files: map[string]string{
|
||||
"/test.rego": `package test
|
||||
|
||||
l1 := {1, 3, 5}
|
||||
l2 contains v if {
|
||||
v := l1[_]
|
||||
}
|
||||
|
||||
test_l if {
|
||||
l1 == l2
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
"test.rego:4: rego_parse_error: var cannot be used for rule name",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 module, v0-compatible, v0 capabilities",
|
||||
v0Compatible: true,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
||||
files: map[string]string{
|
||||
"/test.rego": `package test
|
||||
|
||||
l1 := {1, 3, 5}
|
||||
l2 contains v if {
|
||||
v := l1[_]
|
||||
}
|
||||
|
||||
test_l if {
|
||||
l1 == l2
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
"test.rego:4: rego_parse_error: var cannot be used for rule name",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 module, v0-compatible, v1 capabilities",
|
||||
v0Compatible: true,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
||||
files: map[string]string{
|
||||
"/test.rego": `package test
|
||||
|
||||
l1 := {1, 3, 5}
|
||||
l2 contains v if {
|
||||
v := l1[_]
|
||||
}
|
||||
|
||||
test_l if {
|
||||
l1 == l2
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
"test.rego:4: rego_parse_error: var cannot be used for rule name",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
note: "v1 module, not v0-compatible, no capabilities",
|
||||
files: map[string]string{
|
||||
"/test.rego": `package test
|
||||
|
||||
l1 := {1, 3, 5}
|
||||
l2 contains v if {
|
||||
v := l1[_]
|
||||
}
|
||||
|
||||
test_l if {
|
||||
l1 == l2
|
||||
}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 module, not v0-compatible, v0 capabilities",
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
||||
files: map[string]string{
|
||||
"/test.rego": `package test
|
||||
|
||||
l1 := {1, 3, 5}
|
||||
l2 contains v if {
|
||||
v := l1[_]
|
||||
}
|
||||
|
||||
test_l if {
|
||||
l1 == l2
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 module, not v0-compatible, v1 capabilities",
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
||||
files: map[string]string{
|
||||
"/test.rego": `package test
|
||||
|
||||
l1 := {1, 3, 5}
|
||||
l2 contains v if {
|
||||
v := l1[_]
|
||||
}
|
||||
|
||||
test_l if {
|
||||
l1 == l2
|
||||
}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
loadTypes := []loadType{loadFile, loadBundle, loadTarball}
|
||||
|
||||
for _, tc := range tests {
|
||||
for _, loadType := range loadTypes {
|
||||
t.Run(fmt.Sprintf("%s (%s)", tc.note, loadType), func(t *testing.T) {
|
||||
var files map[string]string
|
||||
if loadType != loadTarball {
|
||||
files = tc.files
|
||||
}
|
||||
test.WithTempFS(files, func(root string) {
|
||||
if loadType == loadTarball {
|
||||
f, err := os.Create(filepath.Join(root, "bundle.tar.gz"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testBundle := bundle.Bundle{
|
||||
Data: map[string]interface{}{},
|
||||
}
|
||||
for k, v := range tc.files {
|
||||
testBundle.Modules = append(testBundle.Modules, bundle.ModuleFile{
|
||||
Path: k,
|
||||
Raw: []byte(v),
|
||||
})
|
||||
}
|
||||
|
||||
if err := bundle.Write(f, testBundle); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
var errBuf bytes.Buffer
|
||||
|
||||
testParams := newTestCommandParams()
|
||||
testParams.bundleMode = loadType == loadBundle
|
||||
testParams.count = 1
|
||||
testParams.output = &buf
|
||||
testParams.errOutput = &errBuf
|
||||
testParams.v0Compatible = tc.v0Compatible
|
||||
testParams.capabilities.C = tc.capabilities
|
||||
|
||||
var paths []string
|
||||
if loadType == loadTarball {
|
||||
paths = []string{filepath.Join(root, "bundle.tar.gz")}
|
||||
} else {
|
||||
paths = []string{root}
|
||||
}
|
||||
|
||||
exitCode, _ := opaTest(paths, testParams)
|
||||
if len(tc.expErrs) > 0 {
|
||||
if exitCode == 0 {
|
||||
t.Fatalf("expected non-zero exit code")
|
||||
}
|
||||
|
||||
for _, expErr := range tc.expErrs {
|
||||
if actual := errBuf.String(); !strings.Contains(actual, expErr) {
|
||||
t.Fatalf("expected error output to contain:\n\n%q\n\nbut got:\n\n%q", expErr, actual)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("unexpected exit code: %d", exitCode)
|
||||
}
|
||||
|
||||
if errBuf.Len() > 0 {
|
||||
t.Fatalf("expected no error output but got:\n\n%q", buf.String())
|
||||
}
|
||||
|
||||
expected := "PASS: 1/1"
|
||||
if actual := buf.String(); !strings.Contains(actual, expected) {
|
||||
t.Fatalf("expected output to contain:\n\n%s\n\nbut got:\n\n%q", expected, actual)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_CompatibleFlags(t *testing.T) {
|
||||
tests := []struct {
|
||||
note string
|
||||
|
||||
+44
-9
@@ -52,6 +52,7 @@ var minVersionIndex = func() VersionIndex {
|
||||
// heads, they wouldn't be able to parse them.
|
||||
const FeatureRefHeadStringPrefixes = "rule_head_ref_string_prefixes"
|
||||
const FeatureRefHeads = "rule_head_refs"
|
||||
const FeatureRegoV1 = "rego_v1"
|
||||
const FeatureRegoV1Import = "rego_v1_import"
|
||||
|
||||
// Capabilities defines a structure containing data that describes the capabilities
|
||||
@@ -83,8 +84,30 @@ type WasmABIVersion struct {
|
||||
Minor int `json:"minor_version"`
|
||||
}
|
||||
|
||||
type CapabilitiesOptions struct {
|
||||
regoVersion RegoVersion
|
||||
}
|
||||
|
||||
func newCapabilitiesOptions(opts []CapabilitiesOption) CapabilitiesOptions {
|
||||
co := CapabilitiesOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(&co)
|
||||
}
|
||||
return co
|
||||
}
|
||||
|
||||
type CapabilitiesOption func(*CapabilitiesOptions)
|
||||
|
||||
func CapabilitiesRegoVersion(regoVersion RegoVersion) CapabilitiesOption {
|
||||
return func(o *CapabilitiesOptions) {
|
||||
o.regoVersion = regoVersion
|
||||
}
|
||||
}
|
||||
|
||||
// CapabilitiesForThisVersion returns the capabilities of this version of OPA.
|
||||
func CapabilitiesForThisVersion() *Capabilities {
|
||||
func CapabilitiesForThisVersion(opts ...CapabilitiesOption) *Capabilities {
|
||||
co := newCapabilitiesOptions(opts)
|
||||
|
||||
f := &Capabilities{}
|
||||
|
||||
for _, vers := range capabilities.ABIVersions() {
|
||||
@@ -97,17 +120,29 @@ func CapabilitiesForThisVersion() *Capabilities {
|
||||
return f.Builtins[i].Name < f.Builtins[j].Name
|
||||
})
|
||||
|
||||
for kw := range futureKeywords {
|
||||
f.FutureKeywords = append(f.FutureKeywords, kw)
|
||||
}
|
||||
sort.Strings(f.FutureKeywords)
|
||||
if co.regoVersion == RegoV0 || co.regoVersion == RegoV0CompatV1 {
|
||||
for kw := range allFutureKeywords {
|
||||
f.FutureKeywords = append(f.FutureKeywords, kw)
|
||||
}
|
||||
|
||||
f.Features = []string{
|
||||
FeatureRefHeadStringPrefixes,
|
||||
FeatureRefHeads,
|
||||
FeatureRegoV1Import,
|
||||
f.Features = []string{
|
||||
FeatureRefHeadStringPrefixes,
|
||||
FeatureRefHeads,
|
||||
FeatureRegoV1Import,
|
||||
}
|
||||
} else {
|
||||
for kw := range futureKeywords {
|
||||
f.FutureKeywords = append(f.FutureKeywords, kw)
|
||||
}
|
||||
|
||||
f.Features = []string{
|
||||
FeatureRegoV1,
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(f.FutureKeywords)
|
||||
sort.Strings(f.Features)
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
|
||||
@@ -9,25 +9,40 @@ import (
|
||||
|
||||
func TestParserCatchesIllegalCapabilities(t *testing.T) {
|
||||
tests := []struct {
|
||||
note string
|
||||
regoVersion RegoVersion
|
||||
note string
|
||||
regoVersion RegoVersion
|
||||
capabilities Capabilities
|
||||
expErr string
|
||||
}{
|
||||
{
|
||||
note: "v0",
|
||||
note: "v0, bad future keyword",
|
||||
regoVersion: RegoV0,
|
||||
capabilities: Capabilities{
|
||||
FutureKeywords: []string{"deadbeef"},
|
||||
},
|
||||
expErr: "illegal capabilities: unknown keyword: deadbeef",
|
||||
},
|
||||
{
|
||||
note: "v1",
|
||||
note: "v1, bad future keyword",
|
||||
regoVersion: RegoV1,
|
||||
capabilities: Capabilities{
|
||||
Features: []string{FeatureRegoV1},
|
||||
FutureKeywords: []string{"deadbeef"},
|
||||
},
|
||||
expErr: "illegal capabilities: unknown keyword: deadbeef",
|
||||
},
|
||||
{
|
||||
note: "v1, no rego_v1 feature",
|
||||
regoVersion: RegoV1,
|
||||
capabilities: Capabilities{},
|
||||
expErr: "illegal capabilities: rego_v1 feature required for parsing v1 Rego",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
var opts ParserOptions
|
||||
opts.Capabilities = &Capabilities{
|
||||
FutureKeywords: []string{"deadbeef"},
|
||||
}
|
||||
opts.Capabilities = &tc.capabilities
|
||||
|
||||
opts.RegoVersion = tc.regoVersion
|
||||
|
||||
@@ -36,7 +51,7 @@ func TestParserCatchesIllegalCapabilities(t *testing.T) {
|
||||
t.Fatal("expected error")
|
||||
} else if errs, ok := err.(Errors); !ok || len(errs) != 1 {
|
||||
t.Fatal("expected exactly one error but got:", err)
|
||||
} else if errs[0].Code != ParseErr || errs[0].Message != "illegal capabilities: unknown keyword: deadbeef" {
|
||||
} else if errs[0].Code != ParseErr || errs[0].Message != tc.expErr {
|
||||
t.Fatal("unexpected error:", err)
|
||||
}
|
||||
})
|
||||
|
||||
+78
-17
@@ -117,7 +117,7 @@ type Compiler struct {
|
||||
// with the key being the generated name and value being the original.
|
||||
RewrittenVars map[Var]Var
|
||||
|
||||
// Capabliities required by the modules that were compiled.
|
||||
// Capabilities required by the modules that were compiled.
|
||||
Required *Capabilities
|
||||
|
||||
localvargen *localVarGenerator
|
||||
@@ -332,7 +332,7 @@ func NewCompiler() *Compiler {
|
||||
{"InitLocalVarGen", "compile_stage_init_local_var_gen", c.initLocalVarGen},
|
||||
{"RewriteRuleHeadRefs", "compile_stage_rewrite_rule_head_refs", c.rewriteRuleHeadRefs},
|
||||
{"CheckKeywordOverrides", "compile_stage_check_keyword_overrides", c.checkKeywordOverrides},
|
||||
{"CheckDuplicateImports", "compile_stage_check_duplicate_imports", c.checkDuplicateImports},
|
||||
{"CheckDuplicateImports", "compile_stage_check_imports", c.checkImports},
|
||||
{"RemoveImports", "compile_stage_remove_imports", c.removeImports},
|
||||
{"SetModuleTree", "compile_stage_set_module_tree", c.setModuleTree},
|
||||
{"SetRuleTree", "compile_stage_set_rule_tree", c.setRuleTree}, // depends on RewriteRuleHeadRefs
|
||||
@@ -971,21 +971,46 @@ func (c *Compiler) buildRequiredCapabilities() {
|
||||
features := map[string]struct{}{}
|
||||
|
||||
// extract required keywords from modules
|
||||
|
||||
keywords := map[string]struct{}{}
|
||||
futureKeywordsPrefix := Ref{FutureRootDocument, StringTerm("keywords")}
|
||||
for _, name := range c.sorted {
|
||||
for _, imp := range c.imports[name] {
|
||||
mod := c.Modules[name]
|
||||
path := imp.Path.Value.(Ref)
|
||||
switch {
|
||||
case path.Equal(RegoV1CompatibleRef):
|
||||
features[FeatureRegoV1Import] = struct{}{}
|
||||
if !c.moduleIsRegoV1(mod) {
|
||||
features[FeatureRegoV1Import] = struct{}{}
|
||||
}
|
||||
case path.HasPrefix(futureKeywordsPrefix):
|
||||
if len(path) == 2 {
|
||||
for kw := range futureKeywords {
|
||||
keywords[kw] = struct{}{}
|
||||
if c.moduleIsRegoV1(mod) {
|
||||
for kw := range futureKeywords {
|
||||
keywords[kw] = struct{}{}
|
||||
}
|
||||
} else {
|
||||
for kw := range allFutureKeywords {
|
||||
keywords[kw] = struct{}{}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
keywords[string(path[2].Value.(String))] = struct{}{}
|
||||
kw := string(path[2].Value.(String))
|
||||
if c.moduleIsRegoV1(mod) {
|
||||
for allowedKw := range futureKeywords {
|
||||
if kw == allowedKw {
|
||||
keywords[kw] = struct{}{}
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for allowedKw := range allFutureKeywords {
|
||||
if kw == allowedKw {
|
||||
keywords[kw] = struct{}{}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -996,13 +1021,19 @@ func (c *Compiler) buildRequiredCapabilities() {
|
||||
// extract required features from modules
|
||||
|
||||
for _, name := range c.sorted {
|
||||
for _, rule := range c.Modules[name].Rules {
|
||||
refLen := len(rule.Head.Reference)
|
||||
if refLen >= 3 {
|
||||
if refLen > len(rule.Head.Reference.ConstantPrefix()) {
|
||||
features[FeatureRefHeads] = struct{}{}
|
||||
} else {
|
||||
features[FeatureRefHeadStringPrefixes] = struct{}{}
|
||||
mod := c.Modules[name]
|
||||
|
||||
if c.moduleIsRegoV1(mod) {
|
||||
features[FeatureRegoV1] = struct{}{}
|
||||
} else {
|
||||
for _, rule := range mod.Rules {
|
||||
refLen := len(rule.Head.Reference)
|
||||
if refLen >= 3 {
|
||||
if refLen > len(rule.Head.Reference.ConstantPrefix()) {
|
||||
features[FeatureRefHeads] = struct{}{}
|
||||
} else {
|
||||
features[FeatureRefHeadStringPrefixes] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1725,12 +1756,22 @@ func (c *Compiler) GetAnnotationSet() *AnnotationSet {
|
||||
return c.annotationSet
|
||||
}
|
||||
|
||||
func (c *Compiler) checkDuplicateImports() {
|
||||
func (c *Compiler) checkImports() {
|
||||
modules := make([]*Module, 0, len(c.Modules))
|
||||
|
||||
supportsRegoV1Import := c.capabilities.ContainsFeature(FeatureRegoV1Import) ||
|
||||
c.capabilities.ContainsFeature(FeatureRegoV1)
|
||||
|
||||
for _, name := range c.sorted {
|
||||
mod := c.Modules[name]
|
||||
if c.strict || c.moduleIsRegoV1(mod) {
|
||||
|
||||
for _, imp := range mod.Imports {
|
||||
if !supportsRegoV1Import && Compare(imp.Path, RegoV1CompatibleRef) == 0 {
|
||||
c.err(NewError(CompileErr, imp.Loc(), "rego.v1 import is not supported"))
|
||||
}
|
||||
}
|
||||
|
||||
if c.strict || c.moduleIsRegoV1Compatible(mod) {
|
||||
modules = append(modules, mod)
|
||||
}
|
||||
}
|
||||
@@ -1744,7 +1785,7 @@ func (c *Compiler) checkDuplicateImports() {
|
||||
func (c *Compiler) checkKeywordOverrides() {
|
||||
for _, name := range c.sorted {
|
||||
mod := c.Modules[name]
|
||||
if c.strict || c.moduleIsRegoV1(mod) {
|
||||
if c.strict || c.moduleIsRegoV1Compatible(mod) {
|
||||
errs := checkRootDocumentOverrides(mod)
|
||||
for _, err := range errs {
|
||||
c.err(err)
|
||||
@@ -1756,6 +1797,23 @@ func (c *Compiler) checkKeywordOverrides() {
|
||||
func (c *Compiler) moduleIsRegoV1(mod *Module) bool {
|
||||
if mod.regoVersion == RegoUndefined {
|
||||
switch c.defaultRegoVersion {
|
||||
case RegoUndefined:
|
||||
c.err(NewError(CompileErr, mod.Package.Loc(), "cannot determine rego version for module"))
|
||||
return false
|
||||
case RegoV1:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return mod.regoVersion == RegoV1
|
||||
}
|
||||
|
||||
func (c *Compiler) moduleIsRegoV1Compatible(mod *Module) bool {
|
||||
if mod.regoVersion == RegoUndefined {
|
||||
switch c.defaultRegoVersion {
|
||||
case RegoUndefined:
|
||||
c.err(NewError(CompileErr, mod.Package.Loc(), "cannot determine rego version for module"))
|
||||
return false
|
||||
case RegoV1, RegoV0CompatV1:
|
||||
return true
|
||||
}
|
||||
@@ -1896,6 +1954,9 @@ func (c *Compiler) rewriteRuleHeadRefs() {
|
||||
cannotSpeakStringPrefixRefs = false
|
||||
case FeatureRefHeads:
|
||||
cannotSpeakGeneralRefs = false
|
||||
case FeatureRegoV1:
|
||||
cannotSpeakStringPrefixRefs = false
|
||||
cannotSpeakGeneralRefs = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5800,7 +5861,7 @@ func safetyErrorSlice(unsafe unsafeVars, rewritten map[Var]Var) (result Errors)
|
||||
v = w
|
||||
}
|
||||
if !v.IsGenerated() {
|
||||
if _, ok := futureKeywords[string(v)]; ok {
|
||||
if _, ok := allFutureKeywords[string(v)]; ok {
|
||||
result = append(result, NewError(UnsafeVarErr, pair.Loc,
|
||||
"var %[1]v is unsafe (hint: `import future.keywords.%[1]v` to import a future keyword)", v))
|
||||
continue
|
||||
|
||||
+216
-5
@@ -9463,9 +9463,10 @@ func TestCompilerBuildRequiredCapabilities(t *testing.T) {
|
||||
`,
|
||||
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
||||
builtins: []string{"eq", "gt"},
|
||||
features: []string{"rego_v1"},
|
||||
},
|
||||
{
|
||||
note: "rego.v1 import",
|
||||
note: "rego.v1 import, v0 module",
|
||||
module: `
|
||||
package x
|
||||
|
||||
@@ -9473,19 +9474,64 @@ func TestCompilerBuildRequiredCapabilities(t *testing.T) {
|
||||
|
||||
p if { true }
|
||||
`,
|
||||
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV0}},
|
||||
features: []string{"rego_v1_import"},
|
||||
},
|
||||
{
|
||||
note: "future.keywords wildcard",
|
||||
note: "rego.v1 import, v1 module",
|
||||
module: `
|
||||
package x
|
||||
|
||||
import rego.v1
|
||||
|
||||
p if { true }
|
||||
`,
|
||||
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
||||
features: []string{"rego_v1"},
|
||||
},
|
||||
{
|
||||
note: "rego.v1 import, default rego-version module (v1)",
|
||||
module: `
|
||||
package x
|
||||
|
||||
import rego.v1
|
||||
|
||||
p if { true }
|
||||
`,
|
||||
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
||||
features: []string{"rego_v1"},
|
||||
},
|
||||
{
|
||||
note: "future.keywords wildcard, v0 module",
|
||||
module: `
|
||||
package x
|
||||
|
||||
import future.keywords
|
||||
`,
|
||||
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV0}},
|
||||
keywords: []string{"contains", "every", "if", "in"},
|
||||
},
|
||||
{
|
||||
note: "future.keywords specific",
|
||||
note: "future.keywords wildcard, v1 module",
|
||||
module: `
|
||||
package x
|
||||
|
||||
import future.keywords
|
||||
`,
|
||||
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
||||
features: []string{"rego_v1"},
|
||||
},
|
||||
{
|
||||
note: "future.keywords wildcard, default rego-version module (v1)",
|
||||
module: `
|
||||
package x
|
||||
|
||||
import future.keywords
|
||||
`,
|
||||
features: []string{"rego_v1"},
|
||||
},
|
||||
{
|
||||
note: "future.keywords specific, v0 module",
|
||||
module: `
|
||||
package x
|
||||
|
||||
@@ -9494,8 +9540,34 @@ func TestCompilerBuildRequiredCapabilities(t *testing.T) {
|
||||
import future.keywords.contains
|
||||
import future.keywords.every
|
||||
`,
|
||||
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV0}},
|
||||
keywords: []string{"contains", "every", "if", "in"},
|
||||
},
|
||||
{
|
||||
note: "future.keywords specific, v1 module",
|
||||
module: `
|
||||
package x
|
||||
|
||||
import future.keywords.in
|
||||
import future.keywords.if
|
||||
import future.keywords.contains
|
||||
import future.keywords.every
|
||||
`,
|
||||
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
||||
features: []string{"rego_v1"},
|
||||
},
|
||||
{
|
||||
note: "future.keywords specific, default rego-version module (v1)",
|
||||
module: `
|
||||
package x
|
||||
|
||||
import future.keywords.in
|
||||
import future.keywords.if
|
||||
import future.keywords.contains
|
||||
import future.keywords.every
|
||||
`,
|
||||
features: []string{"rego_v1"},
|
||||
},
|
||||
{
|
||||
note: "rewriting erases assignment",
|
||||
module: `
|
||||
@@ -9505,6 +9577,7 @@ func TestCompilerBuildRequiredCapabilities(t *testing.T) {
|
||||
`,
|
||||
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
||||
builtins: []string{"assign", "eq"},
|
||||
features: []string{"rego_v1"},
|
||||
},
|
||||
{
|
||||
note: "rewriting erases equals",
|
||||
@@ -9515,6 +9588,7 @@ func TestCompilerBuildRequiredCapabilities(t *testing.T) {
|
||||
`,
|
||||
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
||||
builtins: []string{"eq", "equal"},
|
||||
features: []string{"rego_v1"},
|
||||
},
|
||||
{
|
||||
note: "rewriting erases print",
|
||||
@@ -9525,6 +9599,7 @@ func TestCompilerBuildRequiredCapabilities(t *testing.T) {
|
||||
`,
|
||||
opts: CompileOpts{EnablePrintStatements: true, ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
||||
builtins: []string{"eq", "internal.print", "print"},
|
||||
features: []string{"rego_v1"},
|
||||
},
|
||||
|
||||
{
|
||||
@@ -9536,16 +9611,69 @@ func TestCompilerBuildRequiredCapabilities(t *testing.T) {
|
||||
`,
|
||||
opts: CompileOpts{EnablePrintStatements: false, ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
||||
builtins: []string{"print"}, // only print required because compiler will replace with true
|
||||
features: []string{"rego_v1"},
|
||||
},
|
||||
{
|
||||
note: "dots in the head",
|
||||
note: "dots in the head, v0 module",
|
||||
module: `
|
||||
package x
|
||||
|
||||
a.b.c := 7
|
||||
`,
|
||||
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV0}},
|
||||
features: []string{"rule_head_ref_string_prefixes"},
|
||||
},
|
||||
{
|
||||
note: "dots in the head, v1 module",
|
||||
module: `
|
||||
package x
|
||||
|
||||
a.b.c := 7
|
||||
`,
|
||||
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
||||
features: []string{"rego_v1"}, // rego_v1 includes rule_head_ref_string_prefixes
|
||||
},
|
||||
{
|
||||
note: "dots in the head, default rego-version module (v1)",
|
||||
module: `
|
||||
package x
|
||||
|
||||
a.b.c := 7
|
||||
`,
|
||||
features: []string{"rego_v1"}, // rego_v1 includes rule_head_ref_string_prefixes
|
||||
},
|
||||
{
|
||||
note: "dynamic dots in the head, v0 module",
|
||||
module: `
|
||||
package x
|
||||
|
||||
a[x].c[y] := z { x := "b"; y := "c"; z := "d" }
|
||||
`,
|
||||
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV0}},
|
||||
builtins: []string{"assign", "eq"},
|
||||
features: []string{"rule_head_refs"},
|
||||
},
|
||||
{
|
||||
note: "dynamic dots in the head, v1 module",
|
||||
module: `
|
||||
package x
|
||||
|
||||
a[x].c[y] := z if { x := "b"; y := "c"; z := "d" }
|
||||
`,
|
||||
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
||||
builtins: []string{"assign", "eq"},
|
||||
features: []string{"rego_v1"}, // rego_v1 includes rule_head_refs
|
||||
},
|
||||
{
|
||||
note: "dynamic dots in the head, default rego-version module (v1)",
|
||||
module: `
|
||||
package x
|
||||
|
||||
a[x].c[y] := z if { x := "b"; y := "c"; z := "d" }
|
||||
`,
|
||||
builtins: []string{"assign", "eq"},
|
||||
features: []string{"rego_v1"}, // rego_v1 includes rule_head_refs
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
@@ -10273,6 +10401,14 @@ func TestCompilerCapabilitiesFeatures(t *testing.T) {
|
||||
module: `package test
|
||||
p.q.r := 42`,
|
||||
},
|
||||
{
|
||||
note: "rego-v1 feature, ref-head rule",
|
||||
features: []string{
|
||||
FeatureRegoV1,
|
||||
},
|
||||
module: `package test
|
||||
p.q.r := 42`,
|
||||
},
|
||||
{
|
||||
note: "string-prefix-ref-head feature, general-ref-head rule",
|
||||
features: []string{
|
||||
@@ -10290,6 +10426,14 @@ func TestCompilerCapabilitiesFeatures(t *testing.T) {
|
||||
module: `package test
|
||||
p[q].r[s] := 42 if { q := "foo"; s := "bar" }`,
|
||||
},
|
||||
{
|
||||
note: "rego-v1 feature, general-ref-head rule",
|
||||
features: []string{
|
||||
FeatureRegoV1,
|
||||
},
|
||||
module: `package test
|
||||
p[q].r[s] := 42 if { q := "foo"; s := "bar" }`,
|
||||
},
|
||||
{
|
||||
note: "string-prefix-ref-head & ref-head features, general-ref-head rule",
|
||||
features: []string{
|
||||
@@ -10299,6 +10443,16 @@ func TestCompilerCapabilitiesFeatures(t *testing.T) {
|
||||
module: `package test
|
||||
p[q].r[s] := 42 if { q := "foo"; s := "bar" }`,
|
||||
},
|
||||
{
|
||||
note: "string-prefix-ref-head & ref-head & rego-v1 features, general-ref-head rule",
|
||||
features: []string{
|
||||
FeatureRefHeadStringPrefixes,
|
||||
FeatureRefHeads,
|
||||
FeatureRegoV1,
|
||||
},
|
||||
module: `package test
|
||||
p[q].r[s] := 42 if { q := "foo"; s := "bar" }`,
|
||||
},
|
||||
{
|
||||
note: "string-prefix-ref-head & ref-head features, ref-head rule",
|
||||
features: []string{
|
||||
@@ -10308,6 +10462,16 @@ func TestCompilerCapabilitiesFeatures(t *testing.T) {
|
||||
module: `package test
|
||||
p.q.r := 42`,
|
||||
},
|
||||
{
|
||||
note: "string-prefix-ref-head & ref-head & rego-v1 features, ref-head rule",
|
||||
features: []string{
|
||||
FeatureRefHeadStringPrefixes,
|
||||
FeatureRefHeads,
|
||||
FeatureRegoV1,
|
||||
},
|
||||
module: `package test
|
||||
p.q.r := 42`,
|
||||
},
|
||||
{
|
||||
note: "no features, string-prefix-ref-head with contains kw",
|
||||
features: []string{},
|
||||
@@ -10334,6 +10498,15 @@ func TestCompilerCapabilitiesFeatures(t *testing.T) {
|
||||
import future.keywords.contains
|
||||
p.x contains 1`,
|
||||
},
|
||||
{
|
||||
note: "rego-v1 feature, string-prefix-ref-head with contains kw",
|
||||
features: []string{
|
||||
FeatureRegoV1,
|
||||
},
|
||||
module: `package test
|
||||
import future.keywords.contains
|
||||
p.x contains 1`,
|
||||
},
|
||||
|
||||
{
|
||||
note: "no features, general-ref-head with contains kw",
|
||||
@@ -10362,6 +10535,41 @@ func TestCompilerCapabilitiesFeatures(t *testing.T) {
|
||||
import future.keywords
|
||||
p[x] contains 1 if x = "foo"`,
|
||||
},
|
||||
{
|
||||
note: "rego-v1 feature, general-ref-head with contains kw",
|
||||
features: []string{
|
||||
FeatureRegoV1,
|
||||
},
|
||||
module: `package test
|
||||
import future.keywords
|
||||
p[x] contains 1 if x = "foo"`,
|
||||
},
|
||||
|
||||
{
|
||||
note: "no features, rego.v1 import",
|
||||
module: `package test
|
||||
import rego.v1
|
||||
p if { true }`,
|
||||
expectedErr: "rego_compile_error: rego.v1 import is not supported",
|
||||
},
|
||||
{
|
||||
note: "rego-v1-import feature, rego.v1 import",
|
||||
module: `package test
|
||||
import rego.v1
|
||||
p if { true }`,
|
||||
features: []string{
|
||||
FeatureRegoV1Import,
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "rego-v1-import feature, rego.v1 import",
|
||||
module: `package test
|
||||
import rego.v1
|
||||
p if { true }`,
|
||||
features: []string{
|
||||
FeatureRegoV1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
@@ -10369,8 +10577,11 @@ func TestCompilerCapabilitiesFeatures(t *testing.T) {
|
||||
capabilities := CapabilitiesForThisVersion()
|
||||
capabilities.Features = tc.features
|
||||
|
||||
// Modules are parsed with full set of capabilities
|
||||
mod := module(tc.module)
|
||||
|
||||
compiler := NewCompiler().WithCapabilities(capabilities)
|
||||
compiler.Compile(map[string]*Module{"test": module(tc.module)})
|
||||
compiler.Compile(map[string]*Module{"test": mod})
|
||||
if tc.expectedErr != "" {
|
||||
if !compiler.Failed() {
|
||||
t.Fatal("expected error but got success")
|
||||
|
||||
+73
-35
@@ -283,7 +283,7 @@ func (p *Parser) parsedTermCachePush(t *Term, s0 *state) {
|
||||
func (p *Parser) futureParser() *Parser {
|
||||
q := *p
|
||||
q.s = p.save()
|
||||
q.s.s = p.s.s.WithKeywords(futureKeywords)
|
||||
q.s.s = p.s.s.WithKeywords(allFutureKeywords)
|
||||
q.cache = parsedTermCache{}
|
||||
return &q
|
||||
}
|
||||
@@ -301,7 +301,7 @@ func (p *Parser) presentParser() (*Parser, map[string]tokens.Token) {
|
||||
var cpy map[string]tokens.Token
|
||||
q := *p
|
||||
q.s = p.save()
|
||||
q.s.s, cpy = p.s.s.WithoutKeywords(futureKeywords)
|
||||
q.s.s, cpy = p.s.s.WithoutKeywords(allFutureKeywords)
|
||||
q.cache = parsedTermCache{}
|
||||
return &q, cpy
|
||||
}
|
||||
@@ -312,30 +312,45 @@ func (p *Parser) presentParser() (*Parser, map[string]tokens.Token) {
|
||||
func (p *Parser) Parse() ([]Statement, []*Comment, Errors) {
|
||||
|
||||
if p.po.Capabilities == nil {
|
||||
p.po.Capabilities = CapabilitiesForThisVersion()
|
||||
p.po.Capabilities = CapabilitiesForThisVersion(CapabilitiesRegoVersion(p.po.RegoVersion))
|
||||
}
|
||||
|
||||
allowedFutureKeywords := map[string]tokens.Token{}
|
||||
|
||||
if p.po.EffectiveRegoVersion() == RegoV1 {
|
||||
// RegoV1 includes all future keywords in the default language definition
|
||||
for k, v := range futureKeywords {
|
||||
if !p.po.Capabilities.ContainsFeature(FeatureRegoV1) {
|
||||
return nil, nil, Errors{
|
||||
&Error{
|
||||
Code: ParseErr,
|
||||
Message: "illegal capabilities: rego_v1 feature required for parsing v1 Rego",
|
||||
Location: nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// rego-v1 includes all v0 future keywords in the default language definition
|
||||
for k, v := range futureKeywordsV0 {
|
||||
allowedFutureKeywords[k] = v
|
||||
}
|
||||
|
||||
// For sake of error reporting, we still need to check that keywords in capabilities are known,
|
||||
for _, kw := range p.po.Capabilities.FutureKeywords {
|
||||
if _, ok := futureKeywords[kw]; !ok {
|
||||
return nil, nil, Errors{
|
||||
&Error{
|
||||
Code: ParseErr,
|
||||
Message: fmt.Sprintf("illegal capabilities: unknown keyword: %v", kw),
|
||||
Location: nil,
|
||||
},
|
||||
if tok, ok := futureKeywords[kw]; ok {
|
||||
allowedFutureKeywords[kw] = tok
|
||||
} else {
|
||||
// For sake of error reporting, we still need to check that keywords in capabilities are known in v0
|
||||
if _, ok := futureKeywordsV0[kw]; !ok {
|
||||
return nil, nil, Errors{
|
||||
&Error{
|
||||
Code: ParseErr,
|
||||
Message: fmt.Sprintf("illegal capabilities: unknown keyword: %v", kw),
|
||||
Location: nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// and that explicitly requested future keywords are known.
|
||||
|
||||
// Check that explicitly requested future keywords are known.
|
||||
for _, kw := range p.po.FutureKeywords {
|
||||
if _, ok := allowedFutureKeywords[kw]; !ok {
|
||||
return nil, nil, Errors{
|
||||
@@ -350,7 +365,7 @@ func (p *Parser) Parse() ([]Statement, []*Comment, Errors) {
|
||||
} else {
|
||||
for _, kw := range p.po.Capabilities.FutureKeywords {
|
||||
var ok bool
|
||||
allowedFutureKeywords[kw], ok = futureKeywords[kw]
|
||||
allowedFutureKeywords[kw], ok = allFutureKeywords[kw]
|
||||
if !ok {
|
||||
return nil, nil, Errors{
|
||||
&Error{
|
||||
@@ -361,6 +376,13 @@ func (p *Parser) Parse() ([]Statement, []*Comment, Errors) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if p.po.Capabilities.ContainsFeature(FeatureRegoV1) {
|
||||
// rego-v1 includes all v0 future keywords in the default language definition
|
||||
for k, v := range futureKeywordsV0 {
|
||||
allowedFutureKeywords[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
@@ -2145,8 +2167,7 @@ func (p *Parser) illegal(note string, a ...interface{}) {
|
||||
tokType := "token"
|
||||
if tokens.IsKeyword(p.s.tok) {
|
||||
tokType = "keyword"
|
||||
}
|
||||
if _, ok := futureKeywords[p.s.tok.String()]; ok {
|
||||
} else if _, ok := allFutureKeywords[p.s.tok.String()]; ok {
|
||||
tokType = "keyword"
|
||||
}
|
||||
|
||||
@@ -2641,16 +2662,34 @@ func convertYAMLMapKeyTypes(x any, path []string) (any, error) {
|
||||
|
||||
// futureKeywords is the source of truth for future keywords that will
|
||||
// eventually become standard keywords inside of Rego.
|
||||
var futureKeywords = map[string]tokens.Token{
|
||||
var futureKeywords = map[string]tokens.Token{}
|
||||
|
||||
// futureKeywordsV0 is the source of truth for future keywords that were
|
||||
// not yet a standard part of Rego in v0, and required importing.
|
||||
var futureKeywordsV0 = map[string]tokens.Token{
|
||||
"in": tokens.In,
|
||||
"every": tokens.Every,
|
||||
"contains": tokens.Contains,
|
||||
"if": tokens.If,
|
||||
}
|
||||
|
||||
var allFutureKeywords map[string]tokens.Token
|
||||
|
||||
func IsFutureKeyword(s string) bool {
|
||||
_, ok := futureKeywords[s]
|
||||
return ok
|
||||
return IsFutureKeywordForRegoVersion(s, RegoV1)
|
||||
}
|
||||
|
||||
func IsFutureKeywordForRegoVersion(s string, v RegoVersion) bool {
|
||||
var yes bool
|
||||
|
||||
switch v {
|
||||
case RegoV0, RegoV0CompatV1:
|
||||
_, yes = futureKeywordsV0[s]
|
||||
case RegoV1:
|
||||
_, yes = futureKeywords[s]
|
||||
}
|
||||
|
||||
return yes
|
||||
}
|
||||
|
||||
func (p *Parser) futureImport(imp *Import, allowedFutureKeywords map[string]tokens.Token) {
|
||||
@@ -2666,11 +2705,6 @@ func (p *Parser) futureImport(imp *Import, allowedFutureKeywords map[string]toke
|
||||
return
|
||||
}
|
||||
|
||||
if p.s.s.RegoV1Compatible() {
|
||||
p.errorf(imp.Path.Location, "the `%s` import implies `future.keywords`, these are therefore mutually exclusive", RegoV1CompatibleRef)
|
||||
return
|
||||
}
|
||||
|
||||
kwds := make([]string, 0, len(allowedFutureKeywords))
|
||||
for k := range allowedFutureKeywords {
|
||||
kwds = append(kwds, k)
|
||||
@@ -2700,7 +2734,7 @@ func (p *Parser) futureImport(imp *Import, allowedFutureKeywords map[string]toke
|
||||
}
|
||||
|
||||
func (p *Parser) regoV1Import(imp *Import) {
|
||||
if !p.po.Capabilities.ContainsFeature(FeatureRegoV1Import) {
|
||||
if !p.po.Capabilities.ContainsFeature(FeatureRegoV1Import) && !p.po.Capabilities.ContainsFeature(FeatureRegoV1) {
|
||||
p.errorf(imp.Path.Location, "invalid import, `%s` is not supported by current capabilities", RegoV1CompatibleRef)
|
||||
return
|
||||
}
|
||||
@@ -2724,19 +2758,23 @@ func (p *Parser) regoV1Import(imp *Import) {
|
||||
}
|
||||
|
||||
// import all future keywords with the rego.v1 import
|
||||
kwds := make([]string, 0, len(futureKeywords))
|
||||
for k := range futureKeywords {
|
||||
kwds := make([]string, 0, len(futureKeywordsV0))
|
||||
for k := range futureKeywordsV0 {
|
||||
kwds = append(kwds, k)
|
||||
}
|
||||
|
||||
if p.s.s.HasKeyword(futureKeywords) && !p.s.s.RegoV1Compatible() {
|
||||
// We have imported future keywords, but they didn't come from another `rego.v1` import.
|
||||
p.errorf(imp.Path.Location, "the `%s` import implies `future.keywords`, these are therefore mutually exclusive", RegoV1CompatibleRef)
|
||||
return
|
||||
}
|
||||
|
||||
p.s.s.SetRegoV1Compatible()
|
||||
for _, kw := range kwds {
|
||||
p.s.s.AddKeyword(kw, futureKeywords[kw])
|
||||
p.s.s.AddKeyword(kw, futureKeywordsV0[kw])
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
allFutureKeywords = map[string]tokens.Token{}
|
||||
for k, v := range futureKeywords {
|
||||
allFutureKeywords[k] = v
|
||||
}
|
||||
for k, v := range futureKeywordsV0 {
|
||||
allFutureKeywords[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1266,26 +1266,6 @@ func TestFutureImports(t *testing.T) {
|
||||
}
|
||||
assertParseModule(t, "multiple imports, all kw in options", mod, &parsed, ParserOptions{AllFutureKeywords: true})
|
||||
assertParseModule(t, "multiple imports, single in options", mod, &parsed, ParserOptions{FutureKeywords: []string{"in"}})
|
||||
|
||||
mod = `
|
||||
package p
|
||||
import rego.v1
|
||||
import future.keywords.in
|
||||
`
|
||||
// Only applies to v0, as the 'rego.v1' import is a no-op in v1
|
||||
assertParseModuleErrorMatch(t, "rego.v1 and future.keywords.in imported", mod,
|
||||
"rego_parse_error: the `rego.v1` import implies `future.keywords`, these are therefore mutually exclusive",
|
||||
ParserOptions{RegoVersion: RegoV0})
|
||||
|
||||
mod = `
|
||||
package p
|
||||
import future.keywords
|
||||
import rego.v1
|
||||
`
|
||||
// Only applies to v0, as the 'rego.v1' import is a no-op in v1
|
||||
assertParseModuleErrorMatch(t, "rego.v1 and future.keywords imported", mod,
|
||||
"rego_parse_error: the `rego.v1` import implies `future.keywords`, these are therefore mutually exclusive",
|
||||
ParserOptions{RegoVersion: RegoV0})
|
||||
}
|
||||
|
||||
func TestFutureAndRegoV1ImportsExtraction(t *testing.T) {
|
||||
@@ -1388,7 +1368,6 @@ import future.keywords
|
||||
p contains 1 if {
|
||||
input.x == 1
|
||||
}`,
|
||||
expectedErrors: []string{"rego_parse_error: the `rego.v1` import implies `future.keywords`, these are therefore mutually exclusive"},
|
||||
},
|
||||
{
|
||||
note: "`if` keyword used on rule",
|
||||
@@ -6612,23 +6591,6 @@ func assertParseModuleError(t *testing.T, msg, input string) {
|
||||
}
|
||||
}
|
||||
|
||||
func assertParseModuleErrorMatch(t *testing.T, msg, input string, expected string, opts ...ParserOptions) {
|
||||
t.Helper()
|
||||
|
||||
opt := ParserOptions{}
|
||||
if len(opts) == 1 {
|
||||
opt = opts[0]
|
||||
}
|
||||
|
||||
m, err := ParseModuleWithOpts("", input, opt)
|
||||
if err == nil {
|
||||
t.Errorf("Error on test \"%s\": expected parse error: %v (parsed)", msg, m)
|
||||
}
|
||||
if !strings.Contains(err.Error(), expected) {
|
||||
t.Errorf("Error on test \"%s\"; expected:\n\n%v\n\ngot:\n\n%v", msg, expected, err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertParsePackage(t *testing.T, msg string, input string, correct *Package) {
|
||||
assertParseOne(t, msg, input, func(parsed interface{}) {
|
||||
pkg := parsed.(*Package)
|
||||
|
||||
@@ -1395,6 +1395,13 @@
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"rego_v1": {
|
||||
"Major": 1,
|
||||
"Minor": 0,
|
||||
"Patch": 0,
|
||||
"PreRelease": "",
|
||||
"Metadata": ""
|
||||
},
|
||||
"rego_v1_import": {
|
||||
"Major": 0,
|
||||
"Minor": 59,
|
||||
|
||||
+2
-2
@@ -1082,9 +1082,9 @@ func hashBundleFiles(hash SignatureHasher, b *Bundle) ([]FileInfo, error) {
|
||||
}
|
||||
|
||||
// FormatModules formats Rego modules
|
||||
// Modules will be formatted to comply with rego-v0, but Rego compatibility of individual parsed modules will be respected (e.g. if 'rego.v1' is imported).
|
||||
// Modules will be formatted to comply with [ast.DefaultRegoVersion], but Rego compatibility of individual parsed modules will be respected (e.g. if 'rego.v1' is imported).
|
||||
func (b *Bundle) FormatModules(useModulePath bool) error {
|
||||
return b.FormatModulesForRegoVersion(ast.RegoV0, true, useModulePath)
|
||||
return b.FormatModulesForRegoVersion(ast.DefaultRegoVersion, true, useModulePath)
|
||||
}
|
||||
|
||||
// FormatModulesForRegoVersion formats Rego modules to comply with a given Rego version
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1663,7 +1663,7 @@ update {
|
||||
for _, useMemoryFS := range []bool{false, true} {
|
||||
test.WithTestFS(tc.files, useMemoryFS, func(root string, fsys fs.FS) {
|
||||
|
||||
caps := ast.CapabilitiesForThisVersion()
|
||||
caps := ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0))
|
||||
caps.Features = []string{
|
||||
ast.FeatureRefHeadStringPrefixes,
|
||||
ast.FeatureRefHeads,
|
||||
@@ -1891,6 +1891,9 @@ p if {
|
||||
ast.FeatureRefHeadStringPrefixes,
|
||||
ast.FeatureRefHeads,
|
||||
}
|
||||
if tc.modulesRegoVersion == ast.RegoV1 {
|
||||
capabilities.Features = append(capabilities.Features, ast.FeatureRegoV1)
|
||||
}
|
||||
if tc.regoV1ImportCapable {
|
||||
capabilities.Features = append(capabilities.Features, ast.FeatureRegoV1Import)
|
||||
}
|
||||
|
||||
@@ -762,6 +762,7 @@ func loadBundleFile(path string, bs []byte, m metrics.Metrics, opts ast.ParserOp
|
||||
tl := bundle.NewTarballLoaderWithBaseURL(bytes.NewBuffer(bs), path)
|
||||
br := bundle.NewCustomReader(tl).
|
||||
WithRegoVersion(opts.RegoVersion).
|
||||
WithCapabilities(opts.Capabilities).
|
||||
WithJSONOptions(opts.JSONOptions).
|
||||
WithProcessAnnotations(opts.ProcessAnnotation).
|
||||
WithMetrics(m).
|
||||
|
||||
+118
-90
@@ -18,6 +18,7 @@ import (
|
||||
internal_tracing "github.com/open-policy-agent/opa/internal/distributedtracing"
|
||||
"github.com/open-policy-agent/opa/internal/file/archive"
|
||||
"github.com/open-policy-agent/opa/internal/storage/mock"
|
||||
"github.com/open-policy-agent/opa/v1/ast"
|
||||
"github.com/open-policy-agent/opa/v1/bundle"
|
||||
"github.com/open-policy-agent/opa/v1/logging"
|
||||
"github.com/open-policy-agent/opa/v1/logging/test"
|
||||
@@ -192,116 +193,143 @@ func TestPluginStatusUpdateOnStartAndStop(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestManagerWithOPATelemetryUpdateLoop(t *testing.T) {
|
||||
// test server
|
||||
mux := http.NewServeMux()
|
||||
ts := httptest.NewServer(mux)
|
||||
|
||||
versions := []string{}
|
||||
mux.HandleFunc("/v1/version", func(w http.ResponseWriter, req *http.Request) {
|
||||
var data map[string]string
|
||||
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
versions = append(versions, data["min_compatible_version"])
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
bs, _ := json.Marshal(map[string]string{"foo": "bar"}) // dummy data
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(bs) // ignore error
|
||||
})
|
||||
defer ts.Close()
|
||||
|
||||
t.Setenv("OPA_TELEMETRY_SERVICE_URL", ts.URL)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
m, err := New([]byte{}, "test", inmem.New(), WithEnableTelemetry(true))
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
tests := []struct {
|
||||
note string
|
||||
regoVersion ast.RegoVersion
|
||||
exp []string
|
||||
}{
|
||||
{
|
||||
note: "v0 manager",
|
||||
regoVersion: ast.RegoV0,
|
||||
exp: []string{"0.36.0", "0.46.0"},
|
||||
},
|
||||
{
|
||||
note: "v1 manager",
|
||||
regoVersion: ast.RegoV1,
|
||||
exp: []string{"1.0.0", "1.0.0"},
|
||||
},
|
||||
}
|
||||
|
||||
defaultUploadIntervalSec = int64(1)
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
// test server
|
||||
mux := http.NewServeMux()
|
||||
ts := httptest.NewServer(mux)
|
||||
|
||||
err = m.Start(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
versions := []string{}
|
||||
mux.HandleFunc("/v1/version", func(w http.ResponseWriter, req *http.Request) {
|
||||
var data map[string]string
|
||||
|
||||
// add a policy to the store to trigger a telemetry update (v0.36.0)
|
||||
module := `package x
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
versions = append(versions, data["min_compatible_version"])
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
bs, _ := json.Marshal(map[string]string{"foo": "bar"}) // dummy data
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(bs) // ignore error
|
||||
})
|
||||
defer ts.Close()
|
||||
|
||||
t.Setenv("OPA_TELEMETRY_SERVICE_URL", ts.URL)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
m, err := New([]byte{}, "test", inmem.New(),
|
||||
WithEnableTelemetry(true),
|
||||
WithParserOptions(ast.ParserOptions{RegoVersion: tc.regoVersion}))
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
defaultUploadIntervalSec = int64(1)
|
||||
|
||||
err = m.Start(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
// add a policy to the store to trigger a telemetry update
|
||||
// (v0.36.0 with v0 manager)
|
||||
// (v1.0.0 with v1 manager)
|
||||
module := `package x
|
||||
p := array.reverse([1,2,3])`
|
||||
|
||||
err = storage.Txn(ctx, m.Store, storage.WriteParams, func(txn storage.Transaction) error {
|
||||
return m.Store.UpsertPolicy(ctx, txn, "policy.rego", []byte(module))
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
err = storage.Txn(ctx, m.Store, storage.WriteParams, func(txn storage.Transaction) error {
|
||||
return m.Store.UpsertPolicy(ctx, txn, "policy.rego", []byte(module))
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// add data to the store and verify there is no trigger for a telemetry update
|
||||
err = storage.Txn(ctx, m.Store, storage.WriteParams, func(txn storage.Transaction) error {
|
||||
return m.Store.Write(ctx, txn, storage.AddOp, storage.MustParsePath("/a"), `[2,1,3]`)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
// add data to the store and verify there is no trigger for a telemetry update
|
||||
err = storage.Txn(ctx, m.Store, storage.WriteParams, func(txn storage.Transaction) error {
|
||||
return m.Store.Write(ctx, txn, storage.AddOp, storage.MustParsePath("/a"), `[2,1,3]`)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// add a bundle with some policy to trigger a telemetry update (v0.46.0)
|
||||
txn := storage.NewTransactionOrDie(ctx, m.Store, storage.WriteParams)
|
||||
// add a bundle with some policy to trigger a telemetry update
|
||||
// (v0.46.0 with v0 manager)
|
||||
// (v1.0.0 with v1 manager)
|
||||
txn := storage.NewTransactionOrDie(ctx, m.Store, storage.WriteParams)
|
||||
|
||||
var archiveFiles = map[string]string{
|
||||
"/a/b/c/data.json": "[1,2,3]",
|
||||
"/policy.rego": "package foo\n import future.keywords.every",
|
||||
"/roles/policy.rego": "package bar\n import future.keywords.if\n p.a.b.c.d if { true }",
|
||||
}
|
||||
var archiveFiles = map[string]string{
|
||||
".manifest": `{"rego_version": 0}`,
|
||||
"/a/b/c/data.json": "[1,2,3]",
|
||||
"/policy.rego": "package foo\n import future.keywords.every",
|
||||
"/roles/policy.rego": "package bar\n import future.keywords.if\n p.a.b.c.d if { true }",
|
||||
}
|
||||
|
||||
files := make([][2]string, 0, len(archiveFiles))
|
||||
for name, content := range archiveFiles {
|
||||
files = append(files, [2]string{name, content})
|
||||
}
|
||||
files := make([][2]string, 0, len(archiveFiles))
|
||||
for name, content := range archiveFiles {
|
||||
files = append(files, [2]string{name, content})
|
||||
}
|
||||
|
||||
buf := archive.MustWriteTarGz(files)
|
||||
b, err := bundle.NewReader(buf).WithLazyLoadingMode(true).Read()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
buf := archive.MustWriteTarGz(files)
|
||||
b, err := bundle.NewReader(buf).WithLazyLoadingMode(true).Read()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
iterator := bundle.NewIterator(b.Raw)
|
||||
iterator := bundle.NewIterator(b.Raw)
|
||||
|
||||
params := storage.WriteParams
|
||||
params.BasePaths = []string{""}
|
||||
params := storage.WriteParams
|
||||
params.BasePaths = []string{""}
|
||||
|
||||
err = m.Store.Truncate(ctx, txn, params, iterator)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected truncate error: %v", err)
|
||||
}
|
||||
err = m.Store.Truncate(ctx, txn, params, iterator)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected truncate error: %v", err)
|
||||
}
|
||||
|
||||
if err := m.Store.Commit(ctx, txn); err != nil {
|
||||
t.Fatalf("Unexpected commit error: %v", err)
|
||||
}
|
||||
if err := m.Store.Commit(ctx, txn); err != nil {
|
||||
t.Fatalf("Unexpected commit error: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
m.Stop(ctx)
|
||||
m.Stop(ctx)
|
||||
|
||||
exp := 2
|
||||
if len(versions) != exp {
|
||||
t.Fatalf("Expected number of server calls: %+v but got: %+v", exp, len(versions))
|
||||
}
|
||||
exp := 2
|
||||
if len(versions) != exp {
|
||||
t.Fatalf("Expected number of server calls: %+v but got: %+v", exp, len(versions))
|
||||
}
|
||||
|
||||
expVers := []string{"0.36.0", "0.46.0"}
|
||||
if !reflect.DeepEqual(expVers, versions) {
|
||||
t.Fatalf("Expected OPA versions: %+v but got: %+v", expVers, versions)
|
||||
if !reflect.DeepEqual(tc.exp, versions) {
|
||||
t.Fatalf("Expected OPA versions: %+v but got: %+v", tc.exp, versions)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-3
@@ -1913,6 +1913,7 @@ func (r *Rego) loadFiles(ctx context.Context, txn storage.Transaction, m metrics
|
||||
WithMetrics(m).
|
||||
WithProcessAnnotation(true).
|
||||
WithRegoVersion(r.regoVersion).
|
||||
WithCapabilities(r.capabilities).
|
||||
Filtered(r.loadPaths.paths, r.loadPaths.filter)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1944,6 +1945,7 @@ func (r *Rego) loadBundles(_ context.Context, _ storage.Transaction, m metrics.M
|
||||
WithProcessAnnotation(true).
|
||||
WithSkipBundleVerification(r.skipBundleVerification).
|
||||
WithRegoVersion(r.regoVersion).
|
||||
WithCapabilities(r.capabilities).
|
||||
AsBundle(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading error: %s", err)
|
||||
@@ -2489,7 +2491,10 @@ func (r *Rego) partial(ctx context.Context, ectx *EvalContext) (*PartialQueries,
|
||||
}
|
||||
|
||||
// If the target rego-version is v0, and the rego.v1 import is available, then we attempt to apply it to support modules.
|
||||
if r.regoVersion == ast.RegoV0 && (r.capabilities == nil || r.capabilities.ContainsFeature(ast.FeatureRegoV1Import)) {
|
||||
if r.regoVersion == ast.RegoV0 &&
|
||||
(r.capabilities == nil ||
|
||||
r.capabilities.ContainsFeature(ast.FeatureRegoV1Import) ||
|
||||
r.capabilities.ContainsFeature(ast.FeatureRegoV1)) {
|
||||
|
||||
for i, mod := range support {
|
||||
// We can't apply the RegoV0CompatV1 version to the support module if it contains rules or vars that
|
||||
@@ -2501,7 +2506,7 @@ func (r *Rego) partial(ctx context.Context, ectx *EvalContext) (*PartialQueries,
|
||||
if name == "" && len(r.Head.Reference) > 0 {
|
||||
name = r.Head.Reference[0].Value.(ast.Var)
|
||||
}
|
||||
if ast.IsFutureKeyword(name.String()) {
|
||||
if ast.IsFutureKeywordForRegoVersion(name.String(), ast.RegoV0) {
|
||||
applyRegoVersion = false
|
||||
return true
|
||||
}
|
||||
@@ -2510,7 +2515,7 @@ func (r *Rego) partial(ctx context.Context, ectx *EvalContext) (*PartialQueries,
|
||||
|
||||
if applyRegoVersion {
|
||||
ast.WalkVars(mod, func(v ast.Var) bool {
|
||||
if ast.IsFutureKeyword(v.String()) {
|
||||
if ast.IsFutureKeywordForRegoVersion(v.String(), ast.RegoV0) {
|
||||
applyRegoVersion = false
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -137,6 +137,213 @@ p contains x if {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegoEval_Capabilities(t *testing.T) {
|
||||
tests := []struct {
|
||||
note string
|
||||
regoVersion ast.RegoVersion
|
||||
capabilities *ast.Capabilities
|
||||
module string
|
||||
expResult interface{}
|
||||
expErrs []string
|
||||
}{
|
||||
{
|
||||
note: "v0 module, rego-v0, no capabilities",
|
||||
regoVersion: ast.RegoV0,
|
||||
module: `package test
|
||||
|
||||
p[x] {
|
||||
x = ["a", "b", "c"][_]
|
||||
}`,
|
||||
expResult: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
note: "v0 module, rego-v0, v0 capabilities",
|
||||
regoVersion: ast.RegoV0,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
||||
module: `package test
|
||||
|
||||
p[x] {
|
||||
x = ["a", "b", "c"][_]
|
||||
}`,
|
||||
expResult: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
note: "v0 module, rego-v0, v1 capabilities",
|
||||
regoVersion: ast.RegoV0,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
||||
module: `package test
|
||||
|
||||
p[x] {
|
||||
x = ["a", "b", "c"][_]
|
||||
}`,
|
||||
expResult: []string{"a", "b", "c"},
|
||||
},
|
||||
|
||||
{
|
||||
note: "v0 module, rego-v1, no capabilities",
|
||||
regoVersion: ast.RegoV1,
|
||||
module: `package test
|
||||
|
||||
p[x] {
|
||||
x = ["a", "b", "c"][_]
|
||||
}`,
|
||||
expErrs: []string{
|
||||
"test.rego:3: rego_parse_error: `if` keyword is required before rule body",
|
||||
"test.rego:3: rego_parse_error: `contains` keyword is required for partial set rules",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v0 module, rego-v1, v0 capabilities",
|
||||
regoVersion: ast.RegoV1,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
||||
module: `package test
|
||||
|
||||
p[x] {
|
||||
x = ["a", "b", "c"][_]
|
||||
}`,
|
||||
expErrs: []string{
|
||||
"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v0 module, rego-v1, v1 capabilities",
|
||||
regoVersion: ast.RegoV1,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
||||
module: `package test
|
||||
|
||||
p[x] {
|
||||
x = ["a", "b", "c"][_]
|
||||
}`,
|
||||
expErrs: []string{
|
||||
"test.rego:3: rego_parse_error: `if` keyword is required before rule body",
|
||||
"test.rego:3: rego_parse_error: `contains` keyword is required for partial set rules",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
note: "v1 module, rego-v0, no capabilities",
|
||||
regoVersion: ast.RegoV0,
|
||||
module: `package test
|
||||
|
||||
p contains x if {
|
||||
some x in ["a", "b", "c"]
|
||||
}`,
|
||||
expErrs: []string{
|
||||
"test.rego:4: rego_parse_error: unexpected identifier token",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 module, rego-v0, v0 capabilities",
|
||||
regoVersion: ast.RegoV0,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
||||
module: `package test
|
||||
|
||||
p contains x if {
|
||||
some x in ["a", "b", "c"]
|
||||
}`,
|
||||
expErrs: []string{
|
||||
"test.rego:4: rego_parse_error: unexpected identifier token",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 module, rego-v0, v1 capabilities",
|
||||
regoVersion: ast.RegoV0,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
||||
module: `package test
|
||||
|
||||
p contains x if {
|
||||
some x in ["a", "b", "c"]
|
||||
}`,
|
||||
expErrs: []string{
|
||||
"test.rego:4: rego_parse_error: unexpected identifier token",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
note: "v1 module, rego-v1, no capabilities",
|
||||
regoVersion: ast.RegoV1,
|
||||
module: `package test
|
||||
|
||||
p contains x if {
|
||||
some x in ["a", "b", "c"]
|
||||
}`,
|
||||
expResult: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
note: "v1 module, rego-v1, v0 capabilities",
|
||||
regoVersion: ast.RegoV1,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
||||
module: `package test
|
||||
|
||||
p contains x if {
|
||||
some x in ["a", "b", "c"]
|
||||
}`,
|
||||
expErrs: []string{
|
||||
"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 module, rego-v1, v1 capabilities",
|
||||
regoVersion: ast.RegoV1,
|
||||
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
||||
module: `package test
|
||||
|
||||
p contains x if {
|
||||
some x in ["a", "b", "c"]
|
||||
}`,
|
||||
expResult: []string{"a", "b", "c"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"test.rego": tc.module,
|
||||
}
|
||||
|
||||
test.WithTempFS(files, func(root string) {
|
||||
ctx := context.Background()
|
||||
|
||||
pq, err := New(
|
||||
SetRegoVersion(tc.regoVersion),
|
||||
Capabilities(tc.capabilities),
|
||||
Load([]string{root}, nil),
|
||||
Query("data.test.p"),
|
||||
).PrepareForEval(ctx)
|
||||
|
||||
if tc.expErrs != nil {
|
||||
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 to contain:\n\n%q\n\nbut got:\n\n%v", expErr, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
rs, err := pq.Eval(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(rs) != 1 {
|
||||
t.Fatalf("Expected exactly one result but got:\n\n%v", rs)
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(rs[0].Expressions[0].Value, tc.expResult) {
|
||||
t.Fatalf("Expected %v but got: %v", tc.expResult, rs[0].Expressions[0].Value)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertEval(t *testing.T, r *Rego, expected string) {
|
||||
t.Helper()
|
||||
rs, err := r.Eval(context.Background())
|
||||
|
||||
+1
-6
@@ -929,12 +929,7 @@ func (r *REPL) parserOptions() (ast.ParserOptions, error) {
|
||||
if err == nil {
|
||||
for _, i := range r.modules[r.currentModuleID].Imports {
|
||||
if ast.Compare(i.Path.Value, ast.RegoV1CompatibleRef) == 0 {
|
||||
opts.RegoVersion = ast.RegoV0CompatV1
|
||||
|
||||
// ast.RegoV0CompatV1 sets parsing requirements, but doesn't imply allowed future keywords
|
||||
if r.capabilities != nil {
|
||||
opts.FutureKeywords = r.capabilities.FutureKeywords
|
||||
}
|
||||
opts.RegoVersion = ast.RegoV1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -622,6 +622,10 @@ func Load(args []string, filter loader.Filter) (map[string]*ast.Module, storage.
|
||||
// LoadWithRegoVersion returns modules and an in-memory store for running tests.
|
||||
// Modules are parsed in accordance with the given RegoVersion.
|
||||
func LoadWithRegoVersion(args []string, filter loader.Filter, regoVersion ast.RegoVersion) (map[string]*ast.Module, storage.Store, error) {
|
||||
if regoVersion == ast.RegoUndefined {
|
||||
regoVersion = ast.DefaultRegoVersion
|
||||
}
|
||||
|
||||
loaded, err := loader.NewFileLoader().
|
||||
WithRegoVersion(regoVersion).
|
||||
WithProcessAnnotation(true).
|
||||
@@ -649,6 +653,38 @@ func LoadWithRegoVersion(args []string, filter loader.Filter, regoVersion ast.Re
|
||||
return modules, store, err
|
||||
}
|
||||
|
||||
// LoadWithParserOptions returns modules and an in-memory store for running tests.
|
||||
// Modules are parsed in accordance with the given [ast.ParserOptions].
|
||||
func LoadWithParserOptions(args []string, filter loader.Filter, popts ast.ParserOptions) (map[string]*ast.Module, storage.Store, error) {
|
||||
loaded, err := loader.NewFileLoader().
|
||||
WithRegoVersion(popts.RegoVersion).
|
||||
WithCapabilities(popts.Capabilities).
|
||||
WithProcessAnnotation(popts.ProcessAnnotation).
|
||||
WithJSONOptions(popts.JSONOptions).
|
||||
Filtered(args, filter)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
store := inmem.NewFromObject(loaded.Documents)
|
||||
modules := map[string]*ast.Module{}
|
||||
ctx := context.Background()
|
||||
err = storage.Txn(ctx, store, storage.WriteParams, func(txn storage.Transaction) error {
|
||||
for _, loadedModule := range loaded.Modules {
|
||||
modules[loadedModule.Name] = loadedModule.Parsed
|
||||
|
||||
// Add the policies to the store to ensure that any future bundle
|
||||
// activations will preserve them and re-compile the module with
|
||||
// the bundle modules.
|
||||
err := store.UpsertPolicy(ctx, txn, loadedModule.Name, loadedModule.Raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return modules, store, err
|
||||
}
|
||||
|
||||
// LoadBundles will load the given args as bundles, either tarball or directory is OK.
|
||||
func LoadBundles(args []string, filter loader.Filter) (map[string]*bundle.Bundle, error) {
|
||||
return LoadBundlesWithRegoVersion(args, filter, ast.RegoV0)
|
||||
@@ -677,3 +713,29 @@ func LoadBundlesWithRegoVersion(args []string, filter loader.Filter, regoVersion
|
||||
|
||||
return bundles, nil
|
||||
}
|
||||
|
||||
// LoadBundlesWithParserOptions will load the given args as bundles, either tarball or directory is OK.
|
||||
// Bundles are parsed in accordance with the given [ast.ParserOptions].
|
||||
func LoadBundlesWithParserOptions(args []string, filter loader.Filter, popts ast.ParserOptions) (map[string]*bundle.Bundle, error) {
|
||||
if popts.RegoVersion == ast.RegoUndefined {
|
||||
popts.RegoVersion = ast.DefaultRegoVersion
|
||||
}
|
||||
|
||||
bundles := map[string]*bundle.Bundle{}
|
||||
for _, bundleDir := range args {
|
||||
b, err := loader.NewFileLoader().
|
||||
WithRegoVersion(popts.RegoVersion).
|
||||
WithCapabilities(popts.Capabilities).
|
||||
WithProcessAnnotation(popts.ProcessAnnotation).
|
||||
WithJSONOptions(popts.JSONOptions).
|
||||
WithSkipBundleVerification(true).
|
||||
WithFilter(filter).
|
||||
AsBundle(bundleDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to load bundle %s: %s", bundleDir, err)
|
||||
}
|
||||
bundles[bundleDir] = b
|
||||
}
|
||||
|
||||
return bundles, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user