Files
Anders Eknert db035b09fc Add support for Go 1.27 & jsonv2 (#8947)
Makes OPA build and pass its tests on Go 1.27, while keeping Go 1.25 and
1.26 working. JSON output is unchanged on every supported version.

Go 1.27 json package honours `encoding.TextAppender`. Many v1 ast types
implement AppendText to build their Rego string cheaply, so on 1.27 they
would have marshalled as Rego text. Files built only with 1.27 now
implement MarshalJSONTo.

Library users should keep using `json.Marshal` etc. The MarshalJSONTo
methods are implementation details, are absent from 1.25 and 1.26
builds, and may change.

---------

Signed-off-by: Anders Eknert <anders.eknert@apple.com>
Signed-off-by: Charlie Egan <charlie_egan@apple.com>
Co-authored-by: Charlie Egan <charlie_egan@apple.com>
2026-07-30 17:41:28 +01:00

3485 lines
78 KiB
Go

//go:build !go1.27
package cmd
import (
"archive/tar"
"bytes"
"compress/gzip"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"reflect"
"slices"
"strconv"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/open-policy-agent/opa/internal/file/archive"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/loader"
"github.com/open-policy-agent/opa/v1/util"
"github.com/open-policy-agent/opa/v1/util/test"
)
func TestBuildProducesBundle(t *testing.T) {
files := map[string]string{
"test.rego": `
package test
p = 1
`,
}
test.WithTempFS(files, func(root string) {
params := newBuildParams()
params.outputFile = path.Join(root, "bundle.tar.gz")
err := dobuild(params, []string{root})
if err != nil {
t.Fatal(err)
}
_, err = loader.NewFileLoader().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)
for {
f, err := tr.Next()
if err == io.EOF {
break
} else if err != nil {
t.Fatal(err)
}
if f.Name == "/.manifest" || f.Name == "/data.json" || strings.HasSuffix(f.Name, "/test.rego") {
continue
}
t.Fatal("unexpected file:", f.Name)
}
})
}
func TestBuildRespectsCapabilities(t *testing.T) {
//nolint:prealloc // test slice is extended dynamically, initial values are clearer as slice literal
tests := []struct {
note string
caps string
policy string
err string
bundleMode bool // build with "-b" flag
}{
{
note: "builtin defined in caps",
caps: `{
"builtins": [
{
"name": "is_foo",
"decl": {
"args": [
{
"type": "string"
}
],
"result": {
"type": "boolean"
},
"type": "function"
}
}
]
}`,
policy: `package test
p { is_foo("bar") }`,
},
{
note: "future kw NOT defined in caps",
caps: func() string {
c := ast.CapabilitiesForThisVersion()
c.FutureKeywords = []string{"in"}
c.Features = []string{}
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`,
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 {
c := ast.CapabilitiesForThisVersion()
c.FutureKeywords = []string{"in", "if"}
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: "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:
for i := range tests {
tc := tests[i]
tc.bundleMode = true
tc.note += " (as bundle)"
tests = append(tests, tc)
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
files := map[string]string{
"capabilities.json": tc.caps,
"test.rego": tc.policy,
}
test.WithTempFS(files, func(root string) {
caps := newCapabilitiesFlag()
if err := caps.Set(path.Join(root, "capabilities.json")); err != nil {
t.Fatal(err)
}
params := newBuildParams()
params.outputFile = path.Join(root, "bundle.tar.gz")
params.capabilities = caps
params.bundleMode = tc.bundleMode
// Test capabilities are all pre-v1
params.v0Compatible = true
err := dobuild(params, []string{root})
switch {
case err != nil && tc.err != "":
if !strings.Contains(err.Error(), tc.err) {
t.Fatalf("expected err %v, got %v", tc.err, err)
}
return // don't read back bundle below
case err != nil && tc.err == "":
t.Fatalf("unexpected error: %v", err)
case err == nil && tc.err != "":
t.Fatalf("expected error %v, got nil", tc.err)
}
// check that the resulting bundle is readable
_, err = loader.NewFileLoader().AsBundle(params.outputFile)
if err != nil {
t.Fatal(err)
}
})
})
}
}
func TestBuildFilesystemModeIgnoresTarGz(t *testing.T) {
files := map[string]string{
"test.rego": `
package test
p = 1
`,
}
test.WithTempFS(files, func(root string) {
params := newBuildParams()
params.outputFile = path.Join(root, "bundle.tar.gz")
err := dobuild(params, []string{root})
if err != nil {
t.Fatal(err)
}
_, err = loader.NewFileLoader().AsBundle(params.outputFile)
if err != nil {
t.Fatal(err)
}
// Just run the build again to simulate the user doing back-to-back builds.
err = dobuild(params, []string{root})
if err != nil {
t.Fatal(err)
}
})
}
func TestBuildErrorDoesNotWriteFile(t *testing.T) {
files := map[string]string{
"test.rego": `
package test
p if { p }
`,
}
test.WithTempFS(files, func(root string) {
params := newBuildParams()
params.outputFile = path.Join(root, "bundle.tar.gz")
err := dobuild(params, []string{root})
exp := fmt.Sprintf("1 error occurred: %s/test.rego:4: rego_recursion_error: rule data.test.p is recursive: data.test.p -> data.test.p",
root)
if err == nil || err.Error() != exp {
t.Fatalf("expected recursion error %q but got: %q", exp, err)
}
if _, err := os.Stat(params.outputFile); !os.IsNotExist(err) {
t.Fatalf("expected stat \"not found\" error, got %v", err)
}
})
}
func TestBuildErrorVerifyNonBundle(t *testing.T) {
files := map[string]string{
"test.rego": `
package test
p if { p }
`,
}
test.WithTempFS(files, func(root string) {
params := newBuildParams()
params.outputFile = path.Join(root, "bundle.tar.gz")
params.pubKey = "secret"
err := dobuild(params, []string{root})
if err == nil {
t.Fatal("expected error but got nil")
}
exp := "enable bundle mode (ie. --bundle) to verify or sign bundle files or directories"
if err.Error() != exp {
t.Fatalf("expected error message %v but got %v", exp, err.Error())
}
})
}
func TestBuildVerificationConfigError(t *testing.T) {
if os.Getuid() == 0 {
t.Skip("cannot be run as root")
}
files := map[string]string{
"public.pem": "foo",
}
test.WithTempFS(files, func(rootDir string) {
// simulate error while reading file
err := os.Chmod(filepath.Join(rootDir, "public.pem"), 0111)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
_, err = buildVerificationConfig(filepath.Join(rootDir, "public.pem"), "default", "", "", nil)
if err == nil {
t.Fatal("Expected error but got nil")
}
})
}
func TestBuildSigningConfigError(t *testing.T) {
tests := []struct {
note string
key, plugin, claimsFile string
expErr bool
}{
{
note: "key+plugin+claimsFile unset",
},
{
note: "key+claimsFile unset",
plugin: "plugin",
expErr: true,
},
{
note: "key+plugin unset",
claimsFile: "claims",
expErr: true,
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
_, err := buildSigningConfig(tc.key, defaultTokenSigningAlg, tc.claimsFile, tc.plugin)
switch {
case tc.expErr && err == nil:
t.Fatal("Expected error but got nil")
case !tc.expErr && err != nil:
t.Fatalf("Expected no error but got %v", err)
}
})
}
}
func TestBuildPlanWithPruneUnused(t *testing.T) {
files := map[string]string{
"test.rego": `
package test
p contains 1
f(x) if { p[x] }
`,
}
test.WithTempFS(files, func(root string) {
params := newBuildParams()
if err := params.target.Set("plan"); err != nil {
t.Fatal(err)
}
params.pruneUnused = true
params.entrypoints.v = []string{"test"}
params.outputFile = path.Join(root, "bundle.tar.gz")
err := dobuild(params, []string{root})
if err != nil {
t.Fatal(err)
}
_, err = loader.NewFileLoader().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)
found := false // for plan.json
for {
f, err := tr.Next()
if err == io.EOF {
break
} else if err != nil {
t.Fatal(err)
}
switch {
case f.Name == "/plan.json":
found = true
case f.Name == "/.manifest" || f.Name == "/data.json" || strings.HasSuffix(f.Name, "/test.rego"): // expected
default:
t.Errorf("unexpected file: %s", f.Name)
}
}
if !found {
t.Error("plan.json not found")
}
})
}
func TestBuildPlanProtoFormat(t *testing.T) {
files := map[string]string{
"test.rego": `
package test
default p := false
p if input.user == "alice"
`,
}
test.WithTempFS(files, func(root string) {
params := newBuildParams()
if err := params.target.Set("plan"); err != nil {
t.Fatal(err)
}
if err := params.planFormat.Set("proto"); err != nil {
t.Fatal(err)
}
params.entrypoints.v = []string{"test"}
params.outputFile = path.Join(root, "bundle.tar.gz")
if err := dobuild(params, []string{root}); err != nil {
t.Fatal(err)
}
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)
var foundPlan, foundManifest bool
var sawJSONPlan, sawJSONManifest bool
for {
h, err := tr.Next()
if err == io.EOF {
break
} else if err != nil {
t.Fatal(err)
}
switch h.Name {
case "/plan.pb":
foundPlan = true
case "/.manifest.pb":
foundManifest = true
case "/plan.json":
sawJSONPlan = true
case "/.manifest":
sawJSONManifest = true
}
}
if !foundPlan {
t.Error("plan.pb not found in bundle")
}
if !foundManifest {
t.Error("/.manifest.pb not found in bundle")
}
if sawJSONPlan {
t.Error("plan.json should not be present when --format=proto")
}
if sawJSONManifest {
t.Error("/.manifest should not be present when --format=proto")
}
loaded, err := loader.NewFileLoader().AsBundle(params.outputFile)
if err != nil {
t.Fatalf("load proto bundle: %v", err)
}
if len(loaded.PlanModules) != 1 {
t.Fatalf("expected 1 plan module, got %d", len(loaded.PlanModules))
}
if filepath.Base(loaded.PlanModules[0].Path) != "plan.pb" {
t.Errorf("plan module path should end in plan.pb, got %q", loaded.PlanModules[0].Path)
}
if loaded.Manifest.Empty() {
t.Error("manifest should not be empty after loading proto bundle")
}
})
}
func TestBuildFormatRequiresPlanTarget(t *testing.T) {
files := map[string]string{
"test.rego": `package test`,
}
test.WithTempFS(files, func(root string) {
params := newBuildParams()
if err := params.planFormat.Set("proto"); err != nil {
t.Fatal(err)
}
params.outputFile = path.Join(root, "bundle.tar.gz")
err := dobuild(params, []string{root})
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "plan") {
t.Fatalf("error should mention plan target, got: %v", err)
}
})
}
func TestBuildPlanWithPrintStatements(t *testing.T) {
files := map[string]string{
"test.rego": `
package test
p if { print("hello") }
`,
}
test.WithTempFS(files, func(root string) {
params := newBuildParams()
if err := params.target.Set("plan"); err != nil {
t.Fatal(err)
}
params.entrypoints.v = []string{"test"}
params.outputFile = path.Join(root, "bundle.tar.gz")
err := dobuild(params, []string{root})
if err != nil {
t.Fatal(err)
}
_, err = loader.NewFileLoader().AsBundle(params.outputFile)
if err != nil {
t.Fatal(err)
}
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)
var found bool
for {
f, err := tr.Next()
if err == io.EOF {
break
} else if err != nil {
t.Fatal(err)
}
if f.Name == "/plan.json" {
found = true
plan, err := io.ReadAll(tr)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(plan), "internal.print") {
t.Error("expected plan.json to contain reference to internal.print built-in function")
}
}
}
if !found {
t.Error("plan.json not found")
}
})
}
func TestBuildPlanWithRegoEntrypointAnnotations(t *testing.T) {
tests := []struct {
note string
files map[string]string
err error
v0Compatible bool
}{
{
note: "annotated entrypoint",
files: map[string]string{
"test.rego": `
# METADATA
# entrypoint: true
package test
p contains 1
f(x) if { p[x] }
`,
},
err: nil,
},
{
note: "set generation with annotated entrypoint (v0)",
v0Compatible: true,
files: map[string]string{
"test.rego": `
package test
# METADATA
# entrypoint: true
p[x] {
{"a", "b"}[x]
}
`,
},
err: nil,
},
{
note: "set generation with annotated entrypoint (contains if)",
files: map[string]string{
"test.rego": `
package test
# METADATA
# entrypoint: true
p contains x if {
{"a", "b"}[x]
}
`,
},
err: nil,
},
{
note: "object generation with annotated entrypoint (v0)",
v0Compatible: true,
files: map[string]string{
"test.rego": `
package test
# METADATA
# entrypoint: true
p[i] := x {
x := ["a", "b"][i]
}
`,
},
err: nil,
},
{
note: "object generation with annotated entrypoint (if)",
files: map[string]string{
"test.rego": `
package test
# METADATA
# entrypoint: true
p[i] if {
{"a", "b"}[i]
}
`,
},
err: nil,
},
{
note: "dots in head with annotated entrypoint",
files: map[string]string{
"test.rego": `
package test
# METADATA
# entrypoint: true
p.a.b if {
true
}
`,
},
err: nil,
},
{
note: "dots in head object generation with annotated entrypoint",
files: map[string]string{
"test.rego": `
package test
# METADATA
# entrypoint: true
p.a.b[i] := x if {
x := ["a", "b"][i]
}
`,
},
err: nil,
},
{
note: "no annotated entrypoint",
files: map[string]string{
"test.rego": `
package test
p contains 1
f(x) if { p[x] }
`,
},
err: errors.New("plan compilation requires at least one entrypoint"),
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
test.WithTempFS(tc.files, func(root string) {
params := newBuildParams()
if err := params.target.Set("plan"); err != nil {
t.Fatal(err)
}
params.pruneUnused = true
params.outputFile = path.Join(root, "bundle.tar.gz")
params.v0Compatible = tc.v0Compatible
// Build should fail if entrypoint is not discovered from annotations.
err := dobuild(params, []string{root})
if err != nil {
if tc.err == nil || tc.err.Error() != err.Error() {
t.Fatal(err)
}
return // Bail out if this was an expected test failure.
}
// Attempt to load up the built bundle.
_, err = loader.NewFileLoader().AsBundle(params.outputFile)
if err != nil {
t.Fatal(err)
}
})
})
}
}
func TestBuildWasmWithAnnotations(t *testing.T) {
tests := []struct {
note string
files map[string]string
entrypoints []string
manifest string
}{
{
note: "last rule is annotated entrypoint",
files: map[string]string{
"test.rego": `
package test
# METADATA
# title: P1
p1 := 1
# METADATA
# title: P2
# entrypoint: true
p2 := 2
`,
},
manifest: `
{
"revision":"",
"rego_version": %REGO_VERSION%,
"roots":[""],
"wasm":[{
"entrypoint":"test/p2",
"module":"/policy.wasm",
"annotations":[{
"scope":"document",
"title":"P2",
"entrypoint":true
}]
}]
}
`,
},
{
note: "last rule is (not annotated) entrypoint",
files: map[string]string{
"test.rego": `
package test
# METADATA
# title: P1
p1 := 1
# METADATA
# title: P2
p2 := 2
`,
},
entrypoints: []string{"test/p2"},
manifest: `
{
"revision":"",
"rego_version": %REGO_VERSION%,
"roots":[""],
"wasm":[{
"entrypoint":"test/p2",
"module":"/policy.wasm",
"annotations":[{
"scope":"rule",
"title":"P2"
}]
}]
}
`,
},
{
note: "rules in multiple files are entrypoints",
files: map[string]string{
"test1.rego": `
package test
# METADATA
# title: P1
p1 := 1
# METADATA
# title: P2
# entrypoint: true
p2 := 2
`,
"test2.rego": `
package test
# METADATA
# title: P3
p3 := 3
# METADATA
# title: P4
p4 := 4
`,
"test3.rego": `
package test.foo
# METADATA
# title: BAR
# entrypoint: true
bar := "baz"
`,
},
entrypoints: []string{"test/p3"},
manifest: `
{
"revision":"",
"rego_version": %REGO_VERSION%,
"roots":[""],
"wasm":[{
"entrypoint":"test/p3",
"annotations":[{"scope":"rule","title":"P3"}],
"module":"/policy.wasm"
},{
"entrypoint":"test/foo/bar",
"module":"/policy.wasm",
"annotations":[{
"scope":"document",
"title":"BAR",
"entrypoint":true
}]
},{
"entrypoint":"test/p2",
"module":"/policy.wasm",
"annotations":[{
"scope":"document",
"title":"P2",
"entrypoint":true
}]
}]
}
`,
},
{
note: "rule with multiple metadata blocks",
files: map[string]string{
"test.rego": `
package test
# METADATA
# title: P doc
# scope: document
# entrypoint: true
# METADATA
# title: P
p := 1
`,
},
manifest: `
{
"revision":"",
"rego_version": %REGO_VERSION%,
"roots":[""],
"wasm":[{
"entrypoint":"test/p",
"module":"/policy.wasm",
"annotations":[{
"scope":"document",
"title":"P doc",
"entrypoint":true
},{
"scope":"rule",
"title":"P"
}]
}]
}
`,
},
// Package annotations are not injected into manifest, as package definition is always retained in Rego source.
{
note: "package is annotated entrypoint",
files: map[string]string{
"test.rego": `
# METADATA
# title: PKG
# entrypoint: true
package test
# METADATA
# title: P1
p1 := 1
# METADATA
# title: P2
p2 := 2
`,
},
manifest: `
{
"revision":"",
"rego_version": %REGO_VERSION%,
"roots":[""],
"wasm":[{
"entrypoint":"test",
"module":"/policy.wasm"
}]
}
`,
},
{
note: "package is (not annotated) entrypoint",
files: map[string]string{
"test.rego": `
package test
# METADATA
# title: P1
p1 := 1
# METADATA
# title: P2
p2 := 2
`,
},
entrypoints: []string{"test"},
manifest: `
{
"revision":"",
"rego_version": %REGO_VERSION%,
"roots":[""],
"wasm":[{
"entrypoint":"test",
"module":"/policy.wasm"
}]
}
`,
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
test.WithTempFS(tc.files, func(root string) {
params := newBuildParams()
if err := params.target.Set("wasm"); err != nil {
t.Fatal(err)
}
params.pruneUnused = true
params.outputFile = path.Join(root, "bundle.tar.gz")
params.entrypoints.v = tc.entrypoints
// Build should fail if entrypoint is not discovered from annotations.
err := dobuild(params, []string{root})
if err != nil {
t.Fatal(err)
}
_, err = loader.NewFileLoader().AsBundle(params.outputFile)
if err != nil {
t.Fatal(err)
}
// Check that manifest has expected content
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)
expManifest := strings.ReplaceAll(tc.manifest, "%REGO_VERSION%",
strconv.Itoa(ast.DefaultRegoVersion.Int()))
found := false
for {
f, err := tr.Next()
if err == io.EOF {
break
} else if err != nil {
t.Fatal(err)
}
if f.Name == "/.manifest" {
found = true
data, err := io.ReadAll(tr)
if err != nil {
t.Fatal(err)
}
manifest := util.MustUnmarshalJSON(data)
if !reflect.DeepEqual(manifest, util.MustUnmarshalJSON([]byte(expManifest))) {
t.Fatalf("expected manifest\n\n%v\n\nbut got\n\n%v", expManifest, string(util.MustMarshalJSON(manifest)))
}
break
}
}
if !found {
t.Fatal("no manifest found in bundle")
}
})
})
}
}
func TestBuildBundleModeIgnoreFlag(t *testing.T) {
files := map[string]string{
"/a/b/d/data.json": `{"e": "f"}`,
"/policy.rego": "package foo\n p = 1",
"/policy_test.rego": "package foo\n test_p { p }",
"/roles/policy.rego": "package bar\n p = 1",
"/roles/policy_test.rego": "package bar\n test_p { p }",
"/deeper/dir/path/than/others/policy.rego": "package baz\n p = 1",
"/deeper/dir/path/than/others/policy_test.rego": "package baz\n test_p { p }",
}
test.WithTempFS(files, func(root string) {
params := newBuildParams()
params.outputFile = path.Join(root, "bundle.tar.gz")
params.bundleMode = true
params.ignore = []string{"*_test.rego"}
err := dobuild(params, []string{root})
if err != nil {
t.Fatal(err)
}
_, err = loader.NewFileLoader().AsBundle(params.outputFile)
if err != nil {
t.Fatal(err)
}
// Check that test files are not included in the output bundle
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)
files := []string{}
for {
f, err := tr.Next()
if err == io.EOF {
break
} else if err != nil {
t.Fatal(err)
}
files = append(files, filepath.Base(f.Name))
}
// We additionally expect a manifest file
expected := 5
if len(files) != expected {
t.Fatalf("expected %v files but got %v", expected, len(files))
}
})
}
func TestBuildBundleModeWithManifestRegoVersion(t *testing.T) {
tests := []struct {
note string
roots []string
files map[string]string
expManifest string
expErrs []string
v0Compatible bool
v1Compatible bool
capabilities *ast.Capabilities
}{
{
note: "v0 bundle rego-version",
files: map[string]string{
".manifest": `{"rego_version": 0}`,
"test.rego": `package test
p[42] {
input.x == 1
}`,
},
expManifest: `{"revision":"","roots":[""],"rego_version":0}`,
},
{
note: "v1 bundle rego-version",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"test.rego": `package test
p contains 42 if {
input.x == 1
}`,
},
expManifest: `{"revision":"","roots":[""],"rego_version":1}`,
},
{
note: "v0 bundle rego-version, v1 per-file override",
files: map[string]string{
".manifest": `{
"rego_version": 0,
"file_rego_versions": {
"*/test2.rego": 1
}
}`,
"test1.rego": `package test
p[1] {
input.x == 1
}`,
"test2.rego": `package test
p contains 2 if {
input.x == 1
}`,
},
expManifest: `{"revision":"","roots":[""],"rego_version":0,"file_rego_versions":{"%ROOT%/test2.rego":1}}`,
},
{
note: "v0 bundle rego-version, v1 per-file override, missing v1 keywords in v1 file",
files: map[string]string{
".manifest": `{
"rego_version": 0,
"file_rego_versions": {
"*/test2.rego": 1
}
}`,
"test1.rego": `package test
p[1] {
input.x == 1
}`,
"test2.rego": `package test
p[2] {
input.x == 1
}`,
},
expErrs: []string{
"rego_parse_error: `if` keyword is required before rule body",
"rego_parse_error: `contains` keyword is required for partial set rules",
},
},
{
note: "v0 bundle rego-version, v1 per-file override, v1 keywords but no v1 imports in v0 file",
files: map[string]string{
".manifest": `{
"rego_version": 0,
"file_rego_versions": {
"*/test2.rego": 1
}
}`,
"test1.rego": `package test
p contains 1 if {
input.x == 1
}`,
"test2.rego": `package test
p contains 2 if {
input.x == 1
}`,
},
expErrs: []string{
"rego_parse_error: var cannot be used for rule name",
"rego_parse_error: number cannot be used for rule name",
},
},
{
note: "multiple bundles with different rego-versions, v0-compatible",
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
}`,
},
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, v0-compatible, no 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: capsWithoutFeat(ast.RegoV0, ast.FeatureRegoV1),
expErrs: []string{
// capabilities 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",
v1Compatible: 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
}`,
},
expManifest: `{"revision":"","roots":["test1","test2"],"rego_version":1,"file_rego_versions":{"%ROOT%/bundle1/test1.rego":0,"%ROOT%/bundle2/test4.rego":0}}`,
},
}
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.bundleMode = true
params.v0Compatible = tc.v0Compatible
params.v1Compatible = tc.v1Compatible
if tc.capabilities != nil {
params.capabilities = newCapabilitiesFlag()
params.capabilities.C = tc.capabilities
}
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}
} else {
for _, r := range tc.roots {
roots = append(roots, path.Join(root, r))
}
}
err := dobuild(params, roots)
if tc.expErrs != nil {
if err == nil {
t.Fatal("expected error but got none")
}
for _, expErr := range tc.expErrs {
if !strings.Contains(err.Error(), expErr) {
t.Fatalf("expected error:\n\n%q\n\nbut got:\n\n%v", expErr, err)
}
}
} else {
if err != nil {
t.Fatal(err)
}
_, err = loader.NewFileLoader().AsBundle(params.outputFile)
if err != nil {
t.Fatal(err)
}
f, err := os.Open(params.outputFile)
if err != nil {
t.Fatal(err)
}
defer func() {
_ = f.Close()
}()
gr, err := gzip.NewReader(f)
if err != nil {
t.Fatal(err)
}
tr := tar.NewReader(gr)
for {
f, err := tr.Next()
if err == io.EOF {
break
} else if err != nil {
t.Fatal(err)
}
if f.Name == "/.manifest" {
b, err := io.ReadAll(tr)
if err != nil {
t.Fatal(err)
}
expManifest := strings.ReplaceAll(tc.expManifest, "%ROOT%", root)
if !strings.Contains(string(b), expManifest) {
t.Fatalf("expected manifest:\n\n%v\n\nbut got:\n\n%v", expManifest, string(b))
}
}
}
}
})
})
}
}
func capsWithoutFeat(regoVersion ast.RegoVersion, feat ...string) *ast.Capabilities {
caps := ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(regoVersion))
feats := make([]string, 0, len(caps.Features))
for _, f := range caps.Features {
skip := slices.Contains(feat, f)
if !skip {
feats = append(feats, f)
}
}
caps.Features = feats
return caps
}
func TestBuildBundleFromOtherBundles(t *testing.T) {
type bundleInfo map[string]string
tests := []struct {
note string
v0Compatible bool
v1Compatible bool
capabilities *ast.Capabilities
bundles map[string]bundleInfo
expBundle bundleInfo
expErrs []string
}{
{
note: "single bundle",
bundles: map[string]bundleInfo{
"bundle.tar.gz": {
"policy.rego": `package test
p := input.x == 1
`,
},
},
expBundle: bundleInfo{
"/data.json": `{}
`,
"/.manifest": `{"revision":"","roots":[""],"rego_version":%DEFAULT_REGO_VERSION%}
`,
"%ROOT%/bundle.tar.gz/policy.rego": `package test
p := input.x == 1
`,
},
},
{
note: "single bundle, --v1-compatible",
v1Compatible: true,
bundles: map[string]bundleInfo{
"bundle.tar.gz": {
"policy.rego": `package test
p if {
input.x == 1
}`,
},
},
expBundle: bundleInfo{
"/data.json": `{}
`,
"/.manifest": `{"revision":"","roots":[""],"rego_version":1}
`,
"%ROOT%/bundle.tar.gz/policy.rego": `package test
p if {
input.x == 1
}
`,
},
},
{
note: "single v0 bundle",
bundles: map[string]bundleInfo{
"bundle.tar.gz": {
".manifest": `{"rego_version": 0}`,
"policy.rego": `package test
p {
input.x == 1
}`,
},
},
expBundle: bundleInfo{
"/data.json": `{}
`,
"/.manifest": `{"revision":"","roots":[""],"rego_version":0}
`,
"%ROOT%/bundle.tar.gz/policy.rego": `package test
p {
input.x == 1
}
`,
},
},
{
note: "single v0 bundle, --v1-compatible",
v1Compatible: true,
bundles: map[string]bundleInfo{
"bundle.tar.gz": {
".manifest": `{"rego_version": 0}`,
"policy.rego": `package test
p {
input.x == 1
}`,
},
},
// We don't expect parse/compile errors, as the bundle rego-version is 0, which overrides the --v1-compatible flag.
expBundle: bundleInfo{
"/data.json": `{}
`,
"/.manifest": `{"revision":"","roots":[""],"rego_version":0}
`,
"%ROOT%/bundle.tar.gz/policy.rego": `package test
p {
input.x == 1
}
`,
},
},
{
note: "single v1 bundle, --v0-compatible",
v1Compatible: true,
bundles: map[string]bundleInfo{
"bundle.tar.gz": {
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
p if {
input.x == 1
}`,
},
},
// We don't expect parse/compile errors, as the bundle rego-version is 0, which overrides the --v1-compatible flag.
expBundle: bundleInfo{
"/data.json": `{}
`,
"/.manifest": `{"revision":"","roots":[""],"rego_version":1}
`,
"%ROOT%/bundle.tar.gz/policy.rego": `package test
p if {
input.x == 1
}
`,
},
},
{
note: "single v0 bundle, v1 per-file override",
bundles: map[string]bundleInfo{
"bundle.tar.gz": {
".manifest": `{
"rego_version": 0,
"file_rego_versions": {
"/policy_1.rego": 1
}
}`,
"policy_0.rego": `package test
p {
input.x == 1
}`,
"policy_1.rego": `package test
q contains 1 if {
input.x == 1
}`,
},
},
expBundle: bundleInfo{
"/data.json": `{}
`,
"/.manifest": `{"revision":"","roots":[""],"rego_version":0,"file_rego_versions":{"%ROOT%/bundle.tar.gz/policy_1.rego":1}}
`,
"%ROOT%/bundle.tar.gz/policy_0.rego": `package test
p {
input.x == 1
}
`,
"%ROOT%/bundle.tar.gz/policy_1.rego": `package test
q contains 1 if {
input.x == 1
}
`,
},
},
{
note: "single v0 bundle, v1 per-file override, --v1-compatible",
v1Compatible: true,
bundles: map[string]bundleInfo{
"bundle.tar.gz": {
".manifest": `{
"rego_version": 0,
"file_rego_versions": {
"/policy_1.rego": 1
}
}`,
"policy_0.rego": `package test
p {
input.x == 1
}`,
"policy_1.rego": `package test
q contains 1 if {
input.x == 1
}`,
},
},
expBundle: bundleInfo{
"/data.json": `{}
`,
"/.manifest": `{"revision":"","roots":[""],"rego_version":0,"file_rego_versions":{"%ROOT%/bundle.tar.gz/policy_1.rego":1}}
`,
"%ROOT%/bundle.tar.gz/policy_0.rego": `package test
p {
input.x == 1
}
`,
"%ROOT%/bundle.tar.gz/policy_1.rego": `package test
q contains 1 if {
input.x == 1
}
`,
},
},
{
note: "single v1 bundle, v0 per-file override",
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
}`,
},
},
expBundle: bundleInfo{
"/data.json": `{}
`,
"/.manifest": `{"revision":"","roots":[""],"rego_version":1,"file_rego_versions":{"%ROOT%/bundle.tar.gz/policy_0.rego":0}}
`,
"%ROOT%/bundle.tar.gz/policy_0.rego": `package test
p {
input.x == 1
}
`,
"%ROOT%/bundle.tar.gz/policy_1.rego": `package test
q contains 1 if {
input.x == 1
}
`,
},
},
{
note: "single v1 bundle, v0 per-file override, --v0-compatible",
v0Compatible: true,
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
}`,
},
},
expBundle: bundleInfo{
"/data.json": `{}
`,
"/.manifest": `{"revision":"","roots":[""],"rego_version":1,"file_rego_versions":{"%ROOT%/bundle.tar.gz/policy_0.rego":0}}
`,
"%ROOT%/bundle.tar.gz/policy_0.rego": `package test
p {
input.x == 1
}
`,
"%ROOT%/bundle.tar.gz/policy_1.rego": `package test
q contains 1 if {
input.x == 1
}
`,
},
},
{
note: "single v1 bundle, v0 per-file override, --v0-compatible, no rego_v1 capabilities feature",
v0Compatible: true,
capabilities: capsWithoutFeat(ast.RegoV0, ast.FeatureRegoV1),
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
}`,
},
},
expErrs: []string{
// capabilities 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",
v0Compatible: true,
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
}`,
},
},
expBundle: bundleInfo{
"/data.json": `{}
`,
"/.manifest": `{"revision":"","roots":["test1","test2"],"rego_version":0,"file_rego_versions":{"%ROOT%/bundle_v1.tar.gz/policy.rego":1}}
`,
"%ROOT%/bundle_v0.tar.gz/policy.rego": `package test1
p {
input.x == 1
}
`,
"%ROOT%/bundle_v1.tar.gz/policy.rego": `package test2
q contains 1 if {
input.x == 1
}
`,
},
},
{
note: "v0 bundle + v1 bundle, --v0-compatible, no rego_v1 capabilities feature",
v0Compatible: true,
capabilities: capsWithoutFeat(ast.RegoV0, ast.FeatureRegoV1),
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
}`,
},
},
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, --v1-compatible",
v1Compatible: true,
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
}`,
},
},
expBundle: bundleInfo{
"/data.json": `{}
`,
// We get a v1 bundle with a v0 per-file override
"/.manifest": `{"revision":"","roots":["test1","test2"],"rego_version":1,"file_rego_versions":{"%ROOT%/bundle_v0.tar.gz/policy.rego":0}}
`,
"%ROOT%/bundle_v0.tar.gz/policy.rego": `package test1
p {
input.x == 1
}
`,
"%ROOT%/bundle_v1.tar.gz/policy.rego": `package test2
q contains 1 if {
input.x == 1
}
`,
},
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
root := t.TempDir()
var roots []string
for name, files := range tc.bundles {
p := filepath.Join(root, name)
roots = append(roots, p)
filePairs := make([][2]string, 0, len(files))
for k, v := range files {
filePairs = append(filePairs, [2]string{k, v})
}
buf := archive.MustWriteTarGz(filePairs)
bf, err := os.Create(p)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
_, err = bf.Write(buf.Bytes())
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
}
params := newBuildParams()
params.outputFile = path.Join(root, "bundle.tar.gz")
params.bundleMode = true
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 {
t.Fatal("expected error but got none")
}
for _, expErr := range tc.expErrs {
if !strings.Contains(err.Error(), expErr) {
t.Fatalf("expected error:\n\n%q\n\nbut got:\n\n%v", expErr, err)
}
}
} else {
if err != nil {
t.Fatal(err)
}
_, err = loader.NewFileLoader().AsBundle(params.outputFile)
if err != nil {
t.Fatal(err)
}
f, err := os.Open(params.outputFile)
if err != nil {
t.Fatal(err)
}
defer func() {
_ = f.Close()
}()
gr, err := gzip.NewReader(f)
if err != nil {
t.Fatal(err)
}
tr := tar.NewReader(gr)
for {
f, err := tr.Next()
if err == io.EOF {
break
} else if err != nil {
t.Fatal(err)
}
found := false
for expName, expVal := range tc.expBundle {
expName = strings.ReplaceAll(expName, "%ROOT%", root)
if f.Name == expName {
found = true
b, err := io.ReadAll(tr)
if err != nil {
t.Fatal(err)
}
expVal = strings.ReplaceAll(expVal, "%ROOT%", root)
expVal = strings.ReplaceAll(expVal, "%DEFAULT_REGO_VERSION%",
strconv.Itoa(ast.DefaultRegoVersion.Int()))
if string(b) != expVal {
t.Fatalf("expected %v:\n\n%v\n\nbut got:\n\n%v", expName, expVal, string(b))
}
break
}
}
if !found {
t.Fatalf("unexpected file in bundle: %v", f.Name)
}
}
}
})
}
}
func TestBuild_DefaultRegoVersion(t *testing.T) {
tests := []struct {
note string
files map[string]string
expFiles map[string]string
expErrs []string
}{
{
note: "v0 module",
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",
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")
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 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{
"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 without rego_v1 feature",
capabilities: capsWithoutFeat(ast.RegoV0, ast.FeatureRegoV1),
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
}`,
},
},
{
note: "v1 module, not v0-compatible, v0 capabilities without rego_v1 feature",
capabilities: capsWithoutFeat(ast.RegoV0, ast.FeatureRegoV1),
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
v0Compatible bool
v1Compatible bool
files map[string]string
expectedFiles map[string]string
expectedErr string
}{
{
note: "v0 compatibility: policy with no rego.v1 or future.keywords imports",
v0Compatible: true,
files: map[string]string{
"test.rego": `package test
allow if {
1 < 2
}`,
},
expectedErr: "rego_parse_error",
},
{
note: "v0 compatibility: policy with rego.v1 imports",
v0Compatible: true,
files: map[string]string{
"test.rego": `package test
import rego.v1
allow if {
1 < 2
}`,
},
// Imports are preserved
expectedFiles: map[string]string{
".manifest": `{"revision":"","roots":[""],"rego_version":0}
`,
"test.rego": `package test
import rego.v1
allow if {
1 < 2
}
`,
},
},
{
note: "v0 compatibility: policy with future.keywords imports",
v0Compatible: true,
files: map[string]string{
"test.rego": `package test
import future.keywords.if
allow if {
1 < 2
}`,
},
// Imports are preserved
expectedFiles: map[string]string{
".manifest": `{"revision":"","roots":[""],"rego_version":0}
`,
"test.rego": `package test
import future.keywords.if
allow if {
1 < 2
}
`,
},
},
{
note: "v1 compatibility: policy with no rego.v1 or future.keywords imports",
v1Compatible: true,
files: map[string]string{
"test.rego": `package test
allow if {
1 < 2
}`,
},
// Imports are not added in
expectedFiles: map[string]string{
".manifest": `{"revision":"","roots":[""],"rego_version":1}
`,
"test.rego": `package test
allow if {
1 < 2
}
`,
},
},
{
note: "v1 compatibility: policy with rego.v1 import",
v1Compatible: true,
files: map[string]string{
"test.rego": `package test
import rego.v1
allow if {
1 < 2
}`,
},
// the rego.v1 import is kept to maximize compatibility surface
expectedFiles: map[string]string{
".manifest": `{"revision":"","roots":[""],"rego_version":1}
`,
"test.rego": `package test
import rego.v1
allow if {
1 < 2
}
`,
},
},
{
note: "v1 compatibility: policy with future.keywords import",
v1Compatible: true,
files: map[string]string{
"test.rego": `package test
import future.keywords.if
allow if {
1 < 2
}`,
},
// future.keywords imports are kept to maximize compatibility surface
expectedFiles: map[string]string{
".manifest": `{"revision":"","roots":[""],"rego_version":1}
`,
"test.rego": `package test
import future.keywords.if
allow if {
1 < 2
}
`,
},
},
{
note: "v1 compatibility: policy with rego.v1 and future.keywords imports",
v1Compatible: true,
files: map[string]string{
"test.rego": `package test
import rego.v1
import future.keywords.if
allow if {
1 < 2
}`,
},
// future.keywords are dropped as these are covered by rego.v1
expectedFiles: map[string]string{
".manifest": `{"revision":"","roots":[""],"rego_version":1}
`,
"test.rego": `package test
import rego.v1
allow if {
1 < 2
}
`,
},
},
{
note: "v1 compatibility: missing keywords",
v1Compatible: true,
files: map[string]string{
"test.rego": `package test
allow[1] {
1 < 2
}`,
},
expectedErr: "rego_parse_error",
},
// v0 takes precedence over v1
{
note: "v0+v1 compatibility: policy with no rego.v1 or future.keywords imports",
v0Compatible: true,
v1Compatible: true,
files: map[string]string{
"test.rego": `package test
allow if {
1 < 2
}`,
},
expectedErr: "rego_parse_error",
},
{
note: "v0+v1 compatibility: policy with rego.v1 imports",
v0Compatible: true,
v1Compatible: true,
files: map[string]string{
"test.rego": `package test
import rego.v1
allow if {
1 < 2
}`,
},
// Imports are preserved
expectedFiles: map[string]string{
".manifest": `{"revision":"","roots":[""],"rego_version":0}
`,
"test.rego": `package test
import rego.v1
allow if {
1 < 2
}
`,
},
},
{
note: "v0+v1 compatibility: policy with future.keywords imports",
v0Compatible: true,
v1Compatible: true,
files: map[string]string{
"test.rego": `package test
import future.keywords.if
allow if {
1 < 2
}`,
},
// Imports are preserved
expectedFiles: map[string]string{
".manifest": `{"revision":"","roots":[""],"rego_version":0}
`,
"test.rego": `package test
import future.keywords.if
allow if {
1 < 2
}
`,
},
},
}
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.v1Compatible = tc.v1Compatible
err := dobuild(params, []string{root})
if tc.expectedErr != "" {
if err == nil {
t.Fatal("expected error but got nil")
}
if !strings.Contains(err.Error(), tc.expectedErr) {
t.Fatalf("expected error %v, got %v", tc.expectedErr, err)
}
} else {
if err != nil {
t.Fatal(err)
}
fl := loader.NewFileLoader()
if tc.v1Compatible {
fl = fl.WithRegoVersion(ast.RegoV1)
}
_, 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.expectedFiles[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.expectedFiles {
if _, ok := foundFiles[expectedFile]; !ok {
t.Fatalf("expected file %s not found in bundle, got: %v", expectedFile, foundFiles)
}
}
}
})
})
}
}
func TestBuildOptimizedLevel2(t *testing.T) {
tests := []struct {
note string
files map[string]string
expectedFiles map[string]string
}{
{
note: "Parse package path correctly",
files: map[string]string{
"test.rego": `package demo["test-policy"]
# METADATA
# entrypoint: true
default allow := false
allow if {
some value in input
}
`,
},
expectedFiles: map[string]string{
"/optimized/demo/test-policy.rego": `package demo["test-policy"]
default allow := false
allow if __local2__1 = input[__local1__1]
`,
},
},
}
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.optimizationLevel = 2
err := dobuild(params, []string{root})
if err != nil {
t.Fatal(err)
}
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[f.Name] = struct{}{}
expectedFile := tc.expectedFiles[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.expectedFiles {
if _, ok := foundFiles[expectedFile]; !ok {
t.Fatalf("expected file %s not found in bundle, got: %v", expectedFile, foundFiles)
}
}
})
})
}
}
func TestBuildOptimizedWithRegoVersion(t *testing.T) {
tests := []struct {
note string
v1Compatible bool
regoV1ImportCapable bool
files map[string]string
expectedFiles map[string]string
}{
{
note: "v0, no future keywords",
v1Compatible: false,
regoV1ImportCapable: true,
files: map[string]string{
"test.rego": `package test
# METADATA
# entrypoint: true
p[v] {
v := input.v
}
`,
},
expectedFiles: map[string]string{
"/.manifest": `{"revision":"","roots":[""],"rego_version":0}
`,
// rego.v1 import added to optimized support module
"/optimized/test.rego": `package test
import rego.v1
p contains __local0__1 if {
__local0__1 = input.v
}
`,
},
},
{
note: "v0, No future keywords, not rego.v1 import capable",
v1Compatible: false,
regoV1ImportCapable: false,
files: map[string]string{
"test.rego": `package test
# METADATA
# entrypoint: true
p[v] {
v := input.v
}
`,
},
expectedFiles: map[string]string{
"/.manifest": `{"revision":"","roots":[""],"rego_version":0}
`,
// rego.v1 import NOT added to optimized support module
"/optimized/test.rego": `package test
p[__local0__1] {
__local0__1 = input.v
}
`,
},
},
{
note: "v1, No imports",
v1Compatible: true,
regoV1ImportCapable: true,
files: map[string]string{
"test.rego": `package test
# METADATA
# entrypoint: true
p[k] contains v if {
k := "foo"
v := input.v
}
`,
},
expectedFiles: map[string]string{
"/.manifest": `{"revision":"","roots":[""],"rego_version":1}
`,
"/optimized/test/p.rego": `package test.p
foo contains __local1__1 if {
__local1__1 = input.v
}
`,
},
},
{
note: "v1, rego.v1 imported",
v1Compatible: true,
regoV1ImportCapable: true,
files: map[string]string{
"test.rego": `package test
import rego.v1
# METADATA
# entrypoint: true
p[k] contains v if {
k := "foo"
v := input.v
}
`,
},
// Note: the rego.v1 import isn't added to the optimized module.
// This is ok, as the bundle was built with the --v1-compatible flag,
// and is tagged with a rego-version to inform the consumer.
expectedFiles: map[string]string{
"/.manifest": `{"revision":"","roots":[""],"rego_version":1}
`,
"/optimized/test/p.rego": `package test.p
foo contains __local1__1 if {
__local1__1 = input.v
}
`,
},
},
{
note: "v1, future.keywords imported",
v1Compatible: true,
regoV1ImportCapable: true,
files: map[string]string{
"test.rego": `package test
import future.keywords
# METADATA
# entrypoint: true
p[k] contains v if {
k := "foo"
v := input.v
}
`,
},
expectedFiles: map[string]string{
"/.manifest": `{"revision":"","roots":[""],"rego_version":1}
`,
"/optimized/test/p.rego": `package test.p
foo contains __local1__1 if {
__local1__1 = input.v
}
`,
},
},
}
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.v1Compatible
params.v1Compatible = tc.v1Compatible
params.optimizationLevel = 1
if !tc.regoV1ImportCapable {
caps := newCapabilitiesFlag()
caps.C = ast.CapabilitiesForThisVersion()
caps.C.Features = []string{
ast.FeatureRefHeadStringPrefixes,
ast.FeatureRefHeads,
}
params.capabilities = caps
}
err := dobuild(params, []string{root})
if err != nil {
t.Fatal(err)
}
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[f.Name] = struct{}{}
expectedFile := tc.expectedFiles[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.expectedFiles {
if _, ok := foundFiles[expectedFile]; !ok {
t.Fatalf("expected file %s not found in bundle, got: %v", expectedFile, foundFiles)
}
}
})
})
}
}
// TestBuildWithFollowSymlinks tests that the build command follows symlinks when building a bundle.
// This test uses a local tmp filesystem to create a directory with a symlink to a file in it's root
// and a local file in the bundle directory, and verifies that the built bundle contains both the symlink
// and the regular file.
// There's probably some common utilities that could be extracted at some point but for now this code is
// local to the test until we need to reuse it elsewhere.
func TestBuildWithFollowSymlinks(t *testing.T) {
rootDir := t.TempDir()
bundleDir := path.Join(rootDir, "bundle")
err := os.Mkdir(bundleDir, 0777)
if err != nil {
t.Fatal(err)
}
// create a regular file in our temp bundle directory
err = os.WriteFile(filepath.Join(bundleDir, "foo.rego"), []byte("package foo\none = 1"), 0777)
if err != nil {
t.Fatal(err)
}
// create a regular file in the root directory of our tmp directory that we will symlink into the bundle directory later
err = os.WriteFile(filepath.Join(rootDir, "bar.rego"), []byte("package foo\ntwo = 2"), 0777)
if err != nil {
t.Fatal(err)
}
// create a symlink in the bundle directory to the file in the root directory
err = os.Symlink(filepath.Join(rootDir, "bar.rego"), filepath.Join(bundleDir, "bar.rego"))
if err != nil {
t.Fatal(err)
}
params := newBuildParams()
params.outputFile = path.Join(rootDir, "test.tar.gz")
params.bundleMode = true
params.followSymlinks = true
params.v1Compatible = true
err = dobuild(params, []string{bundleDir})
if err != nil {
t.Fatal(err)
}
// verify that the bundle is a loadable bundle
_, err = loader.NewFileLoader().AsBundle(params.outputFile)
if err != nil {
t.Fatal(err)
}
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)
// map of file name -> file content
expectedFiles := map[string]string{
bundleDir + "/foo.rego": "package foo\n\none := 1",
bundleDir + "/bar.rego": "package foo\n\ntwo := 2",
"/.manifest": `{"revision":"","roots":[""],"rego_version":1}`,
"/data.json": "{}",
}
foundFiles := make(map[string]string, 4)
for f, err := tr.Next(); err != io.EOF; f, err = tr.Next() {
if err != nil {
t.Fatal(err)
}
// ensure that all the files are regular files in the bundle
// and that no symlinks were copied
if mode := f.FileInfo().Mode(); !mode.IsRegular() {
t.Fatalf("expected regular file for file %s but got %s", f.FileInfo().Name(), mode.String())
}
// read the file content
data, err := io.ReadAll(tr)
if err != nil {
t.Fatalf("failed to read file %s: %v", f.FileInfo().Name(), err)
}
foundFiles[f.Name] = string(data)
}
if len(foundFiles) != 4 {
t.Fatalf("expected four files in bundle but got %d", len(foundFiles))
}
for name, contents := range foundFiles {
// trim added whitespace because it's annoying and makes the test less readable
contents := strings.Trim(contents, "\n")
// check that the file content matches the expected content
expectedContent, ok := expectedFiles[name]
if !ok {
t.Fatalf("unexpected file %s in bundle", name)
}
if contents != expectedContent {
t.Fatalf("expected file %s to contain:\n\n%v\n\ngot:\n\n%v", name, expectedContent, contents)
}
}
}
// TestBuildWithFollowSymlinksEntireDir tests that the build command can build a bundle from a symlinked directory.
// This test uses a local tmp filesystem to create a directory with a local file in the bundle directory, and
// verifies that the built bundle contains the files from the symlinked directory.
func TestBuildWithFollowSymlinksEntireDir(t *testing.T) {
rootDir := t.TempDir()
defer func() {
if err := os.RemoveAll(rootDir); err != nil {
t.Fatal(err)
}
}()
bundleDir := path.Join(rootDir, "src")
err := os.Mkdir(bundleDir, 0777)
if err != nil {
t.Fatal(err)
}
// create a regular file in our temp bundle directory
err = os.WriteFile(filepath.Join(bundleDir, "foo.rego"), []byte("package foo\none = 1"), 0777)
if err != nil {
t.Fatal(err)
}
symlinkDir := path.Join(rootDir, "symlink")
err = os.Mkdir(symlinkDir, 0777)
if err != nil {
t.Fatal(err)
}
// create a symlink in the symlink directory to the src directory
err = os.Symlink(bundleDir, filepath.Join(symlinkDir, "linked"))
if err != nil {
t.Fatal(err)
}
params := newBuildParams()
params.outputFile = path.Join(rootDir, "test.tar.gz")
params.bundleMode = true
params.followSymlinks = true
params.v1Compatible = true
err = dobuild(params, []string{symlinkDir + "/linked/"})
if err != nil {
t.Fatal(err)
}
// verify that the bundle is a loadable bundle
_, err = loader.NewFileLoader().AsBundle(params.outputFile)
if err != nil {
t.Fatal(err)
}
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)
// map of file name -> file content
expectedFiles := map[string]string{
path.Join(symlinkDir, "linked", "foo.rego"): "package foo\n\none := 1",
"/.manifest": `{"revision":"","roots":[""],"rego_version":1}`,
"/data.json": "{}",
}
foundFiles := make(map[string]string, 3)
for f, err := tr.Next(); err != io.EOF; f, err = tr.Next() {
if err != nil {
t.Fatal(err)
}
// ensure that all the files are regular files in the bundle
// and that no symlinks were copied
if mode := f.FileInfo().Mode(); !mode.IsRegular() {
t.Fatalf("expected regular file for file %s but got %s", f.FileInfo().Name(), mode.String())
}
// read the file content
data, err := io.ReadAll(tr)
if err != nil {
t.Fatalf("failed to read file %s: %v", f.FileInfo().Name(), err)
}
foundFiles[f.Name] = string(data)
}
if len(foundFiles) != 3 {
t.Fatalf("expected three files in bundle but got %d", len(foundFiles))
}
for name, contents := range foundFiles {
// trim added whitespace because it's annoying and makes the test less readable
contents := strings.Trim(contents, "\n")
// check that the file content matches the expected content
expectedContent, ok := expectedFiles[name]
if !ok {
t.Fatalf("unexpected file %s in bundle", name)
}
if contents != expectedContent {
t.Fatalf("expected file %s to contain:\n\n%v\n\ngot:\n\n%v", name, expectedContent, contents)
}
}
}
func TestBuildManifestWarning(t *testing.T) {
testCases := map[string]struct {
files map[string]string
bundleMode bool
buildArgs []string
expectedStderr func(root string) string
}{
"warns when manifest ignored": {
files: map[string]string{
"bundle/.manifest": `{"revision":"1.0.0","roots":["foo"]}`,
"bundle/data.json": `{"data": "value"}`,
},
bundleMode: false,
buildArgs: []string{"bundle"},
expectedStderr: func(root string) string {
return fmt.Sprintf("Warning: .manifest file found in %q but -b flag not specified. Manifest will be ignored.\n", path.Join(root, "bundle"))
},
},
"no warning when bundle mode enabled": {
files: map[string]string{
"bundle/.manifest": `{"revision":"1.0.0","roots":["foo"]}`,
"bundle/foo/data.json": `{"data": "value"}`,
},
bundleMode: true,
buildArgs: []string{"bundle"},
expectedStderr: func(root string) string {
return ""
},
},
"no warning when no manifest exists": {
files: map[string]string{
"bundle/data.json": `{"data": "value"}`,
},
bundleMode: false,
buildArgs: []string{"bundle"},
expectedStderr: func(root string) string {
return ""
},
},
"warns for multiple bundles with manifests": {
files: map[string]string{
"bundle1/.manifest": `{"revision":"1.0.0","roots":["foo"]}`,
"bundle1/foo/data.json": `{"data1": "value1"}`,
"bundle2/.manifest": `{"revision":"2.0.0","roots":["bar"]}`,
"bundle2/bar/data.json": `{"data2": "value2"}`,
"bundle3/baz/data.json": `{"data3": "value3"}`,
},
bundleMode: false,
buildArgs: []string{"bundle1", "bundle2", "bundle3"},
expectedStderr: func(root string) string {
return fmt.Sprintf(`Warning: .manifest file found in %q but -b flag not specified. Manifest will be ignored.
Warning: .manifest file found in %q but -b flag not specified. Manifest will be ignored.
`,
path.Join(root, "bundle1"), path.Join(root, "bundle2"))
},
},
"warns when proto manifest ignored": {
files: map[string]string{
"bundle/.manifest.pb": "ignored-content",
"bundle/data.json": `{"data": "value"}`,
},
bundleMode: false,
buildArgs: []string{"bundle"},
expectedStderr: func(root string) string {
return fmt.Sprintf("Warning: .manifest.pb file found in %q but -b flag not specified. Manifest will be ignored.\n", path.Join(root, "bundle"))
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
var stderr bytes.Buffer
test.WithTempFS(tc.files, func(root string) {
params := newBuildParams()
params.outputFile = path.Join(root, "output.tar.gz")
params.bundleMode = tc.bundleMode
params.stderr = &stderr
args := make([]string, 0, len(tc.buildArgs))
for _, arg := range tc.buildArgs {
args = append(args, path.Join(root, arg))
}
err := dobuild(params, args)
if err != nil {
t.Fatal(err)
}
stderrOutput := stderr.String()
expectedStderr := tc.expectedStderr(root)
if stderrOutput != expectedStderr {
t.Fatalf("Expected stderr:\n%q\nGot:\n%q", expectedStderr, stderrOutput)
}
})
})
}
}
func TestBuildPlanJSONOutputBytes(t *testing.T) {
files := map[string]string{
"test.rego": `
package test
p = 1
`,
}
test.WithTempFS(files, func(root string) {
params := newBuildParams()
if err := params.target.Set("plan"); err != nil {
t.Fatal(err)
}
params.entrypoints.v = []string{"test"}
params.outputFile = path.Join(root, "bundle.tar.gz")
if err := dobuild(params, []string{root}); err != nil {
t.Fatal(err)
}
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)
var planBytes []byte
var found bool
for {
h, err := tr.Next()
if err == io.EOF {
break
} else if err != nil {
t.Fatal(err)
}
if h.Name == "/plan.json" {
found = true
if planBytes, err = io.ReadAll(tr); err != nil {
t.Fatal(err)
}
}
}
if !found {
t.Fatal("plan.json not found")
}
got := strings.ReplaceAll(string(planBytes), root, "TEMPDIR")
expected := `{"static":{"strings":[{"value":"result"},{"value":"p"},{"value":"1"},{"value":"test"}],"files":[{"value":"TEMPDIR/test.rego"}]},"plans":{"plans":[{"name":"test","blocks":[{"stmts":[{"type":"MakeObjectStmt","stmt":{"target":2,"file":0,"col":0,"row":0}},{"type":"BlockStmt","stmt":{"blocks":[{"stmts":[{"type":"CallStmt","stmt":{"func":"g0.data.test.p","args":[{"type":"local","value":0},{"type":"local","value":1}],"result":3,"file":0,"col":0,"row":0}},{"type":"ObjectInsertStmt","stmt":{"key":{"type":"string_index","value":1},"value":{"type":"local","value":3},"object":2,"file":0,"col":0,"row":0}}]}],"file":0,"col":0,"row":0}},{"type":"BlockStmt","stmt":{"blocks":[{"stmts":[{"type":"BlockStmt","stmt":{"blocks":[{"stmts":[{"type":"DotStmt","stmt":{"source":{"type":"local","value":1},"key":{"type":"string_index","value":3},"target":5,"file":0,"col":0,"row":0}},{"type":"ObjectMergeStmt","stmt":{"a":5,"b":2,"target":4,"file":0,"col":0,"row":0}},{"type":"BreakStmt","stmt":{"index":1,"file":0,"col":0,"row":0}}]}],"file":0,"col":0,"row":0}},{"type":"AssignVarStmt","stmt":{"source":{"type":"local","value":2},"target":4,"file":0,"col":0,"row":0}}]}],"file":0,"col":0,"row":0}},{"type":"AssignVarStmt","stmt":{"source":{"type":"local","value":4},"target":6,"file":0,"col":0,"row":0}},{"type":"MakeObjectStmt","stmt":{"target":7,"file":0,"col":0,"row":0}},{"type":"ObjectInsertStmt","stmt":{"key":{"type":"string_index","value":0},"value":{"type":"local","value":6},"object":7,"file":0,"col":0,"row":0}},{"type":"ResultSetAddStmt","stmt":{"value":7,"file":0,"col":0,"row":0}}]}]}]},"funcs":{"funcs":[{"name":"g0.data.test.p","params":[0,1],"return":2,"blocks":[{"stmts":[{"type":"ResetLocalStmt","stmt":{"target":3,"file":0,"col":4,"row":3}},{"type":"MakeNumberRefStmt","stmt":{"file":0,"col":4,"row":3,"index":2,"Index":2,"target":4}},{"type":"AssignVarOnceStmt","stmt":{"source":{"type":"local","value":4},"target":3,"file":0,"col":4,"row":3}}]},{"stmts":[{"type":"IsDefinedStmt","stmt":{"source":3,"file":0,"col":4,"row":3}},{"type":"AssignVarOnceStmt","stmt":{"source":{"type":"local","value":3},"target":2,"file":0,"col":4,"row":3}}]},{"stmts":[{"type":"ReturnLocalStmt","stmt":{"source":2,"file":0,"col":4,"row":3}}]}],"path":["g0","test","p"]}]}}`
if diff := cmp.Diff(expected, got); diff != "" {
t.Errorf("unexpected result (-want, +got):\n%s", diff)
}
})
}