mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
add --format flag for proto/JSON plan bundles (#8825)
This change adds a new flag for emitting plan bundles in the new protobuf wire format. `opa build --format=json|proto`. With `--format=proto`, the bundle contains `/plan.pb` and `/.manifest.pb` in place of `/plan.json`and `/.manifest`. Bundle Reader auto-detects both forms; mixed-format bundles are rejected at read, merge, and write time. Signed-off-by: Sebastian Spaink <sebastianspaink@gmail.com>
This commit is contained in:
+4
-1
@@ -40,4 +40,7 @@ docs/website/.hugo_build.lock
|
||||
docs/website/data/versions
|
||||
|
||||
# generated when building windows binary to add icon and product version
|
||||
resource.syso
|
||||
resource.syso
|
||||
|
||||
# generated when building proto codegen plugin (see Makefile generate-proto)
|
||||
build/tools/bin/
|
||||
|
||||
@@ -91,6 +91,15 @@ ifeq ($(GOOS),windows)
|
||||
endif
|
||||
$(GO) generate
|
||||
|
||||
.PHONY: generate-proto
|
||||
generate-proto:
|
||||
cd build/tools && $(GO) build -o $(CURDIR)/build/tools/bin/protoc-gen-go google.golang.org/protobuf/cmd/protoc-gen-go
|
||||
PATH="$(CURDIR)/build/tools/bin:$$PATH" protoc \
|
||||
--go_out=. \
|
||||
--go_opt=module=github.com/open-policy-agent/opa \
|
||||
v1/ir/plan.proto \
|
||||
v1/bundle/manifest.proto
|
||||
|
||||
.PHONY: build
|
||||
build: go-build
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ tool (
|
||||
github.com/rogpeppe/go-internal/cmd/testscript
|
||||
golang.org/x/perf/cmd/benchstat
|
||||
golang.org/x/vuln/cmd/govulncheck
|
||||
google.golang.org/protobuf/cmd/protoc-gen-go
|
||||
rsc.io/cmd/benchlab
|
||||
)
|
||||
|
||||
@@ -27,6 +28,7 @@ require (
|
||||
golang.org/x/telemetry v0.0.0-20260421165255-392afab6f40e // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
golang.org/x/vuln v1.3.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
rsc.io/cmd/benchlab v0.0.0-20260520161042-9fc40f0f0431 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -12,10 +12,13 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU=
|
||||
github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/josephspurrier/goversioninfo v1.7.0 h1:LQzXOlVm/CtbwJ9/UHl5a2HT0BjcLAwid5gqGd7ZUJ8=
|
||||
@@ -48,5 +51,11 @@ golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTF
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
|
||||
golang.org/x/vuln v1.3.0 h1:hZYzR8uRhYhDSX88d+40TWbKAVw7BIvRWm26rtEn8jw=
|
||||
golang.org/x/vuln v1.3.0/go.mod h1:MIY2PaR1y52stzZM3uHBboUAdVJvSVMl5nP3OQrwQaE=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
+9
-4
@@ -29,6 +29,7 @@ const defaultPublicKeyID = "default"
|
||||
type buildParams struct {
|
||||
capabilities *capabilitiesFlag
|
||||
target *util.EnumFlag
|
||||
planFormat *util.EnumFlag
|
||||
bundleMode bool
|
||||
pruneUnused bool
|
||||
optimizationLevel int
|
||||
@@ -57,6 +58,7 @@ func newBuildParams() buildParams {
|
||||
return buildParams{
|
||||
capabilities: newCapabilitiesFlag(),
|
||||
target: util.NewEnumFlag(compile.TargetRego, compile.Targets),
|
||||
planFormat: util.NewEnumFlag(compile.PlanFormatJSON, compile.PlanFormats),
|
||||
stderr: os.Stderr,
|
||||
}
|
||||
}
|
||||
@@ -254,6 +256,7 @@ against ` + brand + ` v0.22.0:
|
||||
}
|
||||
|
||||
buildCommand.Flags().VarP(buildParams.target, "target", "t", "set the output bundle target type")
|
||||
buildCommand.Flags().Var(buildParams.planFormat, "format", "set the plan output format (only applies when --target=plan)")
|
||||
buildCommand.Flags().BoolVar(&buildParams.pruneUnused, "prune-unused", false, "exclude dependents of entrypoints")
|
||||
buildCommand.Flags().BoolVar(&buildParams.debug, "debug", false, "enable debug output")
|
||||
buildCommand.Flags().IntVarP(&buildParams.optimizationLevel, "optimize", "O", 0, "set optimization level")
|
||||
@@ -312,11 +315,12 @@ func dobuild(params buildParams, args []string) error {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(arg, ".manifest")); err != nil {
|
||||
continue
|
||||
for _, name := range []string{bundle.ManifestExt, bundle.ManifestProtoExt} {
|
||||
if _, err := os.Stat(filepath.Join(arg, name)); err == nil {
|
||||
fmt.Fprintf(params.stderr, "Warning: %s file found in %q but -b flag not specified. Manifest will be ignored.\n", name, arg)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(params.stderr, "Warning: .manifest file found in %q but -b flag not specified. Manifest will be ignored.\n", arg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,6 +333,7 @@ func dobuild(params buildParams, args []string) error {
|
||||
compiler := compile.New().
|
||||
WithCapabilities(capabilities).
|
||||
WithTarget(params.target.String()).
|
||||
WithPlanFormat(params.planFormat.String()).
|
||||
WithAsBundle(params.bundleMode).
|
||||
WithPruneUnused(params.pruneUnused).
|
||||
WithOptimizationLevel(params.optimizationLevel).
|
||||
|
||||
@@ -449,6 +449,114 @@ func TestBuildPlanWithPruneUnused(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
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{
|
||||
@@ -3266,6 +3374,17 @@ Warning: .manifest file found in %q but -b flag not specified. Manifest will be
|
||||
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 {
|
||||
|
||||
@@ -29,6 +29,11 @@ func TestManifestProtoConsistency(t *testing.T) {
|
||||
{
|
||||
Name: "Manifest",
|
||||
GoType: reflect.TypeOf(bundle.Manifest{}),
|
||||
// roots_set is wire-form bookkeeping: it preserves the
|
||||
// nil-vs-explicit-empty distinction that Manifest.Roots
|
||||
// (*[]string) carries on the Go side. No Go counterpart by
|
||||
// design.
|
||||
SkipProtoFields: []string{"roots_set"},
|
||||
},
|
||||
{
|
||||
Name: "WasmResolver",
|
||||
|
||||
@@ -73,6 +73,11 @@ type MessageSpec struct {
|
||||
// is intentionally absent from the proto because consumers consult
|
||||
// their own registry for builtin signatures.
|
||||
SkipGoFields []string
|
||||
// SkipProtoFields names proto fields with no Go counterpart by design —
|
||||
// wire-form bookkeeping that the Go side carries differently. For
|
||||
// example, bundle.Manifest.roots_set distinguishes nil from explicit-
|
||||
// empty on the wire; the Go side already carries that as Roots *[]string.
|
||||
SkipProtoFields []string
|
||||
}
|
||||
|
||||
// OneofSpec asserts that the named oneof on MessageName has exactly the
|
||||
@@ -289,6 +294,14 @@ func checkMessage(t *testing.T, declared map[string]protoreflect.MessageDescript
|
||||
}
|
||||
}
|
||||
|
||||
skipProto := map[string]bool{}
|
||||
for _, n := range m.SkipProtoFields {
|
||||
skipProto[n] = true
|
||||
if msg.Fields().ByName(protoreflect.Name(n)) == nil {
|
||||
t.Errorf("%s: SkipProtoFields entry %q does not match any proto field on this message", m.Name, n)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate FieldNameOverride entries point at real Go fields.
|
||||
for goJSONName := range override {
|
||||
if !goJSONNameExists(m.GoType, goJSONName) {
|
||||
@@ -333,6 +346,9 @@ func checkMessage(t *testing.T, declared map[string]protoreflect.MessageDescript
|
||||
if opaque[name] {
|
||||
continue
|
||||
}
|
||||
if skipProto[name] {
|
||||
continue
|
||||
}
|
||||
// Fields belonging to a oneof handled by an OneofSpec are
|
||||
// validated there, not here. Skip them so the orphan check
|
||||
// doesn't double-fire.
|
||||
|
||||
+6
-2
@@ -3172,7 +3172,7 @@ func (b *metadataParser) Parse() (result *Annotations, err error) {
|
||||
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
a.Schema, err = parseSchemaRef(v)
|
||||
a.Schema, err = ParseSchemaRef(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -3275,10 +3275,14 @@ func unwrapPair(pair map[string]any) (string, any) {
|
||||
|
||||
var errInvalidSchemaRef = errors.New("invalid schema reference")
|
||||
|
||||
// ParseSchemaRef parses a schema reference string into a Ref. Unlike
|
||||
// ParseRef, it accepts the bare `schema` Var and Refs prefixed with the
|
||||
// schema root document.
|
||||
//
|
||||
// NOTE(tsandall): 'schema' is not registered as a root because it's not
|
||||
// supported by the compiler or evaluator today. Once we fix that, we can remove
|
||||
// this function.
|
||||
func parseSchemaRef(s string) (Ref, error) {
|
||||
func ParseSchemaRef(s string) (Ref, error) {
|
||||
|
||||
term, err := ParseTerm(s)
|
||||
if err == nil {
|
||||
|
||||
+134
-21
@@ -23,11 +23,13 @@ import (
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/file/archive"
|
||||
"github.com/open-policy-agent/opa/internal/merge"
|
||||
"github.com/open-policy-agent/opa/v1/ast"
|
||||
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
|
||||
pb "github.com/open-policy-agent/opa/v1/bundle/v1pb"
|
||||
"github.com/open-policy-agent/opa/v1/format"
|
||||
"github.com/open-policy-agent/opa/v1/metrics"
|
||||
"github.com/open-policy-agent/opa/v1/storage"
|
||||
@@ -39,7 +41,9 @@ const (
|
||||
RegoExt = ".rego"
|
||||
WasmFile = "policy.wasm"
|
||||
PlanFile = "plan.json"
|
||||
PlanProtoFile = "plan.pb"
|
||||
ManifestExt = ".manifest"
|
||||
ManifestProtoExt = ".manifest.pb"
|
||||
SignaturesFile = "signatures.json"
|
||||
patchFile = "patch.json"
|
||||
dataFile = "data.json"
|
||||
@@ -70,6 +74,13 @@ type Bundle struct {
|
||||
|
||||
lazyLoadingMode bool
|
||||
sizeLimitBytes int64
|
||||
manifestProto bool
|
||||
}
|
||||
|
||||
// SetManifestProto configures the bundle to serialize its manifest as
|
||||
// protobuf at /.manifest.pb instead of JSON at /.manifest.
|
||||
func (b *Bundle) SetManifestProto(yes bool) {
|
||||
b.manifestProto = yes
|
||||
}
|
||||
|
||||
// Raw contains raw bytes representing the bundle's content
|
||||
@@ -641,6 +652,7 @@ func (r *Reader) Read() (Bundle, error) {
|
||||
}
|
||||
|
||||
var modules []ModuleFile
|
||||
var manifestPath string
|
||||
for _, f := range descriptors {
|
||||
buf, err := readFile(f, r.sizeLimitBytes)
|
||||
if err != nil {
|
||||
@@ -695,7 +707,7 @@ func (r *Reader) Read() (Bundle, error) {
|
||||
Path: r.fullPath(path),
|
||||
Raw: buf.Bytes(),
|
||||
})
|
||||
} else if filepath.Base(path) == PlanFile {
|
||||
} else if filepath.Base(path) == PlanFile || filepath.Base(path) == PlanProtoFile {
|
||||
bundle.PlanModules = append(bundle.PlanModules, PlanModuleFile{
|
||||
URL: f.URL(),
|
||||
Path: r.fullPath(path),
|
||||
@@ -741,7 +753,26 @@ func (r *Reader) Read() (Bundle, error) {
|
||||
return empty, err
|
||||
}
|
||||
|
||||
} else if strings.HasSuffix(path, ManifestProtoExt) {
|
||||
if manifestPath != "" {
|
||||
return empty, fmt.Errorf("bundle contains multiple manifest files: %q and %q", manifestPath, path)
|
||||
}
|
||||
manifestPath = path
|
||||
pbManifest := &pb.Manifest{}
|
||||
if err := proto.Unmarshal(buf.Bytes(), pbManifest); err != nil {
|
||||
return empty, fmt.Errorf("bundle load failed on manifest decode: %w", err)
|
||||
}
|
||||
m, err := ManifestFromProto(pbManifest)
|
||||
if err != nil {
|
||||
return empty, fmt.Errorf("bundle load failed on manifest decode: %w", err)
|
||||
}
|
||||
bundle.Manifest = *m
|
||||
bundle.manifestProto = true
|
||||
} else if strings.HasSuffix(path, ManifestExt) {
|
||||
if manifestPath != "" {
|
||||
return empty, fmt.Errorf("bundle contains multiple manifest files: %q and %q", manifestPath, path)
|
||||
}
|
||||
manifestPath = path
|
||||
if err := util.NewJSONDecoder(&buf).Decode(&bundle.Manifest); err != nil {
|
||||
return empty, fmt.Errorf("bundle load failed on manifest decode: %w", err)
|
||||
}
|
||||
@@ -935,6 +966,9 @@ func (w *Writer) DisableFormat(yes bool) *Writer {
|
||||
|
||||
// Write writes the bundle to the writer's output stream.
|
||||
func (w *Writer) Write(bundle Bundle) error {
|
||||
if err := validateBundleFormat(&bundle); err != nil {
|
||||
return err
|
||||
}
|
||||
tw := archive.NewTarGzWriter(w.w)
|
||||
|
||||
if bundle.Type() == SnapshotBundleType {
|
||||
@@ -971,8 +1005,18 @@ func (w *Writer) Write(bundle Bundle) error {
|
||||
}
|
||||
|
||||
if !bundle.Manifest.Empty() {
|
||||
if err := tw.WriteJSONFile("/.manifest", bundle.Manifest); err != nil {
|
||||
return err
|
||||
if bundle.manifestProto {
|
||||
bs, err := marshalManifestProto(&bundle.Manifest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tw.WriteFile(util.WithPrefix(ManifestProtoExt, "/"), bs); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := tw.WriteJSONFile("/.manifest", bundle.Manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1060,32 +1104,74 @@ func hashBundleFiles(hash SignatureHasher, b *Bundle) ([]FileInfo, error) {
|
||||
files = append(files, NewFile(strings.TrimPrefix(planmodule.Path, "/"), hex.EncodeToString(bs), defaultHashingAlg))
|
||||
}
|
||||
|
||||
// If the manifest is essentially empty, don't add it to the signatures since it
|
||||
// won't be written to the bundle. Otherwise:
|
||||
// parse the manifest into a JSON structure;
|
||||
// then recursively order the fields of all objects alphabetically and then apply
|
||||
// the hash function to result to compute the hash.
|
||||
// Skip empty manifest — Writer.Write skips it too, so no entry to hash.
|
||||
// Proto manifest is hashed as raw deterministic-marshal bytes (matches
|
||||
// what VerifyBundleFile sees, since IsStructuredDoc is false for /.manifest.pb).
|
||||
if !b.Manifest.Empty() {
|
||||
mbs, err := json.Marshal(b.Manifest)
|
||||
if err != nil {
|
||||
return files, err
|
||||
}
|
||||
if b.manifestProto {
|
||||
pbBytes, err := marshalManifestProto(&b.Manifest)
|
||||
if err != nil {
|
||||
return files, err
|
||||
}
|
||||
if bs, err = hash.HashFile(pbBytes); err != nil {
|
||||
return files, err
|
||||
}
|
||||
files = append(files, NewFile(strings.TrimPrefix(ManifestProtoExt, "/"), hex.EncodeToString(bs), defaultHashingAlg))
|
||||
} else {
|
||||
mbs, err := json.Marshal(b.Manifest)
|
||||
if err != nil {
|
||||
return files, err
|
||||
}
|
||||
|
||||
var result map[string]any
|
||||
if err := util.Unmarshal(mbs, &result); err != nil {
|
||||
return files, err
|
||||
}
|
||||
var result map[string]any
|
||||
if err := util.Unmarshal(mbs, &result); err != nil {
|
||||
return files, err
|
||||
}
|
||||
|
||||
if bs, err = hash.HashFile(result); err != nil {
|
||||
return files, err
|
||||
}
|
||||
if bs, err = hash.HashFile(result); err != nil {
|
||||
return files, err
|
||||
}
|
||||
|
||||
files = append(files, NewFile(strings.TrimPrefix(ManifestExt, "/"), hex.EncodeToString(bs), defaultHashingAlg))
|
||||
files = append(files, NewFile(strings.TrimPrefix(ManifestExt, "/"), hex.EncodeToString(bs), defaultHashingAlg))
|
||||
}
|
||||
}
|
||||
|
||||
return files, err
|
||||
}
|
||||
|
||||
// marshalManifestProto returns the deterministic protobuf wire form so
|
||||
// signer and writer produce byte-identical output (sign/verify on
|
||||
// /.manifest.pb depends on it).
|
||||
func marshalManifestProto(m *Manifest) ([]byte, error) {
|
||||
pbManifest, err := ManifestToProto(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return proto.MarshalOptions{Deterministic: true}.Marshal(pbManifest)
|
||||
}
|
||||
|
||||
// validateBundleFormat rejects bundles whose plan format disagrees with
|
||||
// the manifest format (e.g. /plan.pb + /.manifest).
|
||||
func validateBundleFormat(b *Bundle) error {
|
||||
if b.Manifest.Empty() {
|
||||
return nil
|
||||
}
|
||||
for _, pm := range b.PlanModules {
|
||||
base := filepath.Base(pm.Path)
|
||||
switch base {
|
||||
case PlanFile:
|
||||
if b.manifestProto {
|
||||
return fmt.Errorf("bundle has proto manifest but JSON plan %q; SetManifestProto must agree with plan format", pm.Path)
|
||||
}
|
||||
case PlanProtoFile:
|
||||
if !b.manifestProto {
|
||||
return fmt.Errorf("bundle has JSON manifest but proto plan %q; SetManifestProto(true) required", pm.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormatModules formats Rego modules
|
||||
// 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 {
|
||||
@@ -1473,11 +1559,34 @@ func MergeWithRegoVersion(bundles []*Bundle, regoVersion ast.RegoVersion, usePat
|
||||
var roots []string
|
||||
var result Bundle
|
||||
|
||||
var planFile string
|
||||
var manifestProto bool
|
||||
var manifestProtoSet bool
|
||||
|
||||
for _, b := range bundles {
|
||||
if b.Manifest.Roots == nil {
|
||||
return nil, errors.New("bundle manifest not initialized")
|
||||
}
|
||||
|
||||
for _, pm := range b.PlanModules {
|
||||
base := filepath.Base(pm.Path)
|
||||
if base != PlanFile && base != PlanProtoFile {
|
||||
continue
|
||||
}
|
||||
if planFile == "" {
|
||||
planFile = base
|
||||
} else if planFile != base {
|
||||
return nil, fmt.Errorf("cannot merge bundles with mixed plan formats (%s and %s)", planFile, base)
|
||||
}
|
||||
}
|
||||
|
||||
if !manifestProtoSet {
|
||||
manifestProto = b.manifestProto
|
||||
manifestProtoSet = true
|
||||
} else if manifestProto != b.manifestProto {
|
||||
return nil, errors.New("cannot merge bundles with mixed manifest formats")
|
||||
}
|
||||
|
||||
roots = append(roots, *b.Manifest.Roots...)
|
||||
|
||||
result.Modules = append(result.Modules, b.Modules...)
|
||||
@@ -1508,6 +1617,8 @@ func MergeWithRegoVersion(bundles []*Bundle, regoVersion ast.RegoVersion, usePat
|
||||
}
|
||||
}
|
||||
|
||||
result.manifestProto = manifestProto
|
||||
|
||||
// We respect the bundle rego-version, defaulting to the provided rego version if not set.
|
||||
result.SetRegoVersion(result.RegoVersion(regoVersion))
|
||||
|
||||
@@ -1666,7 +1777,9 @@ func modulePathWithPrefix(bundleName string, modulePath string) string {
|
||||
return path.Join(bundleName, modulePath)
|
||||
}
|
||||
|
||||
// IsStructuredDoc checks if the file name equals a structured file extension ex. ".json"
|
||||
// IsStructuredDoc checks if the file name equals a structured file extension ex. ".json".
|
||||
// Note: ManifestProtoExt (".manifest.pb") is intentionally absent — proto manifests are
|
||||
// hashed as raw wire bytes on both the sign and verify paths.
|
||||
func IsStructuredDoc(name string) bool {
|
||||
base := filepath.Base(name)
|
||||
return base == dataFile || base == yamlDataFile || base == SignaturesFile || base == ManifestExt
|
||||
|
||||
@@ -492,6 +492,46 @@ func TestReadWithManifest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadRejectsDuplicateManifests(t *testing.T) {
|
||||
pbBytes, err := marshalManifestProto(&Manifest{Revision: "from-proto"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
note string
|
||||
files [][2]string
|
||||
}{
|
||||
{
|
||||
note: "json + proto manifest",
|
||||
files: [][2]string{
|
||||
{"/.manifest", `{"revision": "from-json"}`},
|
||||
{"/.manifest.pb", string(pbBytes)},
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "two json manifests at different paths (HasSuffix shadowing)",
|
||||
files: [][2]string{
|
||||
{"/.manifest", `{"revision": "from-root"}`},
|
||||
{"/sub/release.manifest", `{"revision": "from-sub"}`},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
buf := archive.MustWriteTarGz(tc.files)
|
||||
_, err := NewReader(buf).Read()
|
||||
if err == nil {
|
||||
t.Fatal("expected error for duplicate manifests, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "multiple manifest files") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManifestMetadata(t *testing.T) {
|
||||
files := [][2]string{
|
||||
{"/.manifest", `{
|
||||
@@ -1297,6 +1337,168 @@ func TestRoundtrip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriterRejectsMixedPlanManifestFormats(t *testing.T) {
|
||||
manifest := Manifest{Roots: &[]string{""}, Revision: "r"}
|
||||
|
||||
t.Run("proto manifest with JSON plan", func(t *testing.T) {
|
||||
b := Bundle{
|
||||
Manifest: manifest,
|
||||
PlanModules: []PlanModuleFile{{
|
||||
Path: PlanFile, URL: PlanFile, Raw: []byte("{}"),
|
||||
}},
|
||||
}
|
||||
b.SetManifestProto(true)
|
||||
var buf bytes.Buffer
|
||||
err := NewWriter(&buf).Write(b)
|
||||
if err == nil || !strings.Contains(err.Error(), "proto manifest but JSON plan") {
|
||||
t.Fatalf("expected mixed-format error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("JSON manifest with proto plan", func(t *testing.T) {
|
||||
b := Bundle{
|
||||
Manifest: manifest,
|
||||
PlanModules: []PlanModuleFile{{
|
||||
Path: PlanProtoFile, URL: PlanProtoFile, Raw: []byte{0x00},
|
||||
}},
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err := NewWriter(&buf).Write(b)
|
||||
if err == nil || !strings.Contains(err.Error(), "JSON manifest but proto plan") {
|
||||
t.Fatalf("expected mixed-format error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty manifest exempts proto plan", func(t *testing.T) {
|
||||
b := Bundle{
|
||||
Manifest: Manifest{},
|
||||
PlanModules: []PlanModuleFile{{
|
||||
Path: PlanProtoFile, URL: PlanProtoFile, Raw: []byte{0x00},
|
||||
}},
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := NewWriter(&buf).Write(b); err != nil {
|
||||
t.Fatalf("expected empty-manifest write to succeed, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("uniform proto succeeds", func(t *testing.T) {
|
||||
b := Bundle{
|
||||
Manifest: manifest,
|
||||
PlanModules: []PlanModuleFile{{
|
||||
Path: PlanProtoFile, URL: PlanProtoFile, Raw: []byte{0x00},
|
||||
}},
|
||||
}
|
||||
b.SetManifestProto(true)
|
||||
var buf bytes.Buffer
|
||||
if err := NewWriter(&buf).Write(b); err != nil {
|
||||
t.Fatalf("expected uniform-proto write to succeed, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoundtripWithProtoManifestSigned(t *testing.T) {
|
||||
|
||||
bundle := Bundle{
|
||||
Data: map[string]any{
|
||||
"foo": map[string]any{
|
||||
"bar": []any{json.Number("1"), json.Number("2"), json.Number("3")},
|
||||
},
|
||||
},
|
||||
PlanModules: []PlanModuleFile{
|
||||
{
|
||||
Path: PlanProtoFile,
|
||||
URL: PlanProtoFile,
|
||||
Raw: []byte("plan-proto-bytes"),
|
||||
},
|
||||
},
|
||||
Manifest: Manifest{
|
||||
Roots: &[]string{""},
|
||||
Revision: "quickbrownfaux",
|
||||
Metadata: map[string]any{"version": "v1", "hello": "world"},
|
||||
},
|
||||
}
|
||||
bundle.SetManifestProto(true)
|
||||
|
||||
if err := bundle.GenerateSignature(NewSigningConfig("secret", "HS256", ""), "foo", false); err != nil {
|
||||
t.Fatal("Unexpected error:", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := NewWriter(&buf).Write(bundle); err != nil {
|
||||
t.Fatal("Unexpected error:", err)
|
||||
}
|
||||
|
||||
vc := NewVerificationConfig(map[string]*KeyConfig{"foo": {Key: "secret", Algorithm: "HS256"}}, "foo", "", nil)
|
||||
bundle2, err := NewReader(&buf).WithBundleVerificationConfig(vc).Read()
|
||||
if err != nil {
|
||||
t.Fatalf("verify failed for proto-manifest bundle: %v", err)
|
||||
}
|
||||
|
||||
if bundle2.Manifest.Revision != bundle.Manifest.Revision {
|
||||
t.Fatalf("manifest not preserved through proto round-trip: got revision %q", bundle2.Manifest.Revision)
|
||||
}
|
||||
if !reflect.DeepEqual(bundle2.Signatures, bundle.Signatures) {
|
||||
t.Fatal("expected signatures to be same after read")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadProtoManifestPreservesFormatIntent(t *testing.T) {
|
||||
src := Bundle{
|
||||
Data: map[string]any{},
|
||||
Manifest: Manifest{
|
||||
Roots: &[]string{""},
|
||||
Revision: "abc",
|
||||
},
|
||||
}
|
||||
src.SetManifestProto(true)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := NewWriter(&buf).Write(src); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
loaded, err := NewReader(&buf).Read()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !loaded.manifestProto {
|
||||
t.Fatal("Reader did not propagate manifestProto onto the loaded bundle")
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
if err := NewWriter(&out).Write(loaded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gr, err := gzip.NewReader(&out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tr := tar.NewReader(gr)
|
||||
var sawProto, sawJSON bool
|
||||
for {
|
||||
h, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
switch h.Name {
|
||||
case "/.manifest.pb":
|
||||
sawProto = true
|
||||
case "/.manifest":
|
||||
sawJSON = true
|
||||
}
|
||||
}
|
||||
if !sawProto {
|
||||
t.Error("re-written bundle missing /.manifest.pb")
|
||||
}
|
||||
if sawJSON {
|
||||
t.Error("re-written bundle silently downgraded to /.manifest")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundtripWithPlanModules(t *testing.T) {
|
||||
|
||||
b := Bundle{
|
||||
@@ -1804,6 +2006,75 @@ func TestMergeCorruptManifest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeRejectsMixedFormats(t *testing.T) {
|
||||
rootEmpty := &[]string{""}
|
||||
|
||||
t.Run("mixed plan formats", func(t *testing.T) {
|
||||
jsonBundle := &Bundle{
|
||||
Manifest: Manifest{Roots: rootEmpty},
|
||||
PlanModules: []PlanModuleFile{{
|
||||
Path: PlanFile, URL: PlanFile, Raw: []byte("{}"),
|
||||
}},
|
||||
}
|
||||
protoBundle := &Bundle{
|
||||
Manifest: Manifest{Roots: rootEmpty},
|
||||
PlanModules: []PlanModuleFile{{
|
||||
Path: PlanProtoFile, URL: PlanProtoFile, Raw: []byte{0x00},
|
||||
}},
|
||||
}
|
||||
_, err := Merge([]*Bundle{jsonBundle, protoBundle})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for mixed plan formats, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "mixed plan formats") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("mixed manifest formats", func(t *testing.T) {
|
||||
jsonManifestBundle := &Bundle{
|
||||
Manifest: Manifest{Roots: rootEmpty, Revision: "j"},
|
||||
}
|
||||
protoManifestBundle := &Bundle{
|
||||
Manifest: Manifest{Roots: rootEmpty, Revision: "p"},
|
||||
}
|
||||
protoManifestBundle.SetManifestProto(true)
|
||||
|
||||
_, err := Merge([]*Bundle{jsonManifestBundle, protoManifestBundle})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for mixed manifest formats, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "mixed manifest formats") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("uniform proto inputs propagate flag", func(t *testing.T) {
|
||||
a := &Bundle{
|
||||
Manifest: Manifest{Roots: &[]string{"a"}, Revision: "a"},
|
||||
PlanModules: []PlanModuleFile{{
|
||||
Path: PlanProtoFile, URL: PlanProtoFile, Raw: []byte{0x00},
|
||||
}},
|
||||
}
|
||||
a.SetManifestProto(true)
|
||||
b := &Bundle{
|
||||
Manifest: Manifest{Roots: &[]string{"b"}, Revision: "b"},
|
||||
PlanModules: []PlanModuleFile{{
|
||||
Path: PlanProtoFile, URL: PlanProtoFile, Raw: []byte{0x01},
|
||||
}},
|
||||
}
|
||||
b.SetManifestProto(true)
|
||||
|
||||
merged, err := Merge([]*Bundle{a, b})
|
||||
if err != nil {
|
||||
t.Fatalf("expected uniform-format merge to succeed, got %v", err)
|
||||
}
|
||||
if !merged.manifestProto {
|
||||
t.Fatal("merged bundle should carry manifestProto=true forward from inputs")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMerge(t *testing.T) {
|
||||
|
||||
expRegoVersion := ast.DefaultRegoVersion.Int()
|
||||
|
||||
+4
-1
@@ -200,7 +200,10 @@ func formatPath(fileName string, root string, pathFormat PathFormat) string {
|
||||
case Chrooted:
|
||||
// Trim off the root directory and return path as if chrooted
|
||||
result := strings.TrimPrefix(fileName, filepath.FromSlash(root))
|
||||
if root == "." && filepath.Base(fileName) == ManifestExt {
|
||||
// TrimPrefix at root="." strips the leading dot from dotfile manifests
|
||||
// (".manifest" → "manifest"), which then misses the Reader's HasSuffix
|
||||
// check. Restore the original name for both manifest forms.
|
||||
if root == "." && (filepath.Base(fileName) == ManifestExt || filepath.Base(fileName) == ManifestProtoExt) {
|
||||
result = fileName
|
||||
}
|
||||
if !strings.HasPrefix(result, string(filepath.Separator)) {
|
||||
|
||||
@@ -544,3 +544,47 @@ func getFilter(pattern string, minDepth int) filter.LoaderFilter {
|
||||
return match && depth >= minDepth
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatPathChrootedManifests(t *testing.T) {
|
||||
sep := string(filepath.Separator)
|
||||
cases := []struct {
|
||||
note string
|
||||
fileName string
|
||||
root string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
note: "json manifest at chroot .",
|
||||
fileName: ".manifest",
|
||||
root: ".",
|
||||
want: sep + ".manifest",
|
||||
},
|
||||
{
|
||||
note: "proto manifest at chroot .",
|
||||
fileName: ".manifest.pb",
|
||||
root: ".",
|
||||
want: sep + ".manifest.pb",
|
||||
},
|
||||
{
|
||||
note: "json manifest at named chroot",
|
||||
fileName: filepath.Join("a", "b", ".manifest"),
|
||||
root: filepath.Join("a", "b"),
|
||||
want: sep + ".manifest",
|
||||
},
|
||||
{
|
||||
note: "proto manifest at named chroot",
|
||||
fileName: filepath.Join("a", "b", ".manifest.pb"),
|
||||
root: filepath.Join("a", "b"),
|
||||
want: sep + ".manifest.pb",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
got := formatPath(tc.fileName, tc.root, Chrooted)
|
||||
if got != tc.want {
|
||||
t.Fatalf("formatPath(%q, %q, Chrooted) = %q, want %q", tc.fileName, tc.root, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ message Manifest {
|
||||
// Bundle revision string.
|
||||
string revision = 1;
|
||||
|
||||
// Root paths the bundle owns.
|
||||
// Root paths the bundle owns. See `roots_set` for nil-vs-empty.
|
||||
repeated string roots = 2;
|
||||
|
||||
// Wasm resolvers attached to the bundle. JSON key is `wasm`.
|
||||
@@ -31,6 +31,10 @@ message Manifest {
|
||||
// Free-form metadata object. Modeled as `Struct` because the Go field
|
||||
// is `map[string]any`.
|
||||
google.protobuf.Struct metadata = 6;
|
||||
|
||||
// True if `bundle.Manifest.Roots` was non-nil. `repeated string` can't
|
||||
// distinguish nil (default to [""]) from explicit-empty (owns no paths).
|
||||
bool roots_set = 7;
|
||||
}
|
||||
|
||||
// WasmResolver mirrors `bundle.WasmResolver` in v1/bundle/bundle.go.
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
// Copyright 2026 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package bundle
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
"github.com/open-policy-agent/opa/v1/ast"
|
||||
"github.com/open-policy-agent/opa/v1/ast/location"
|
||||
pb "github.com/open-policy-agent/opa/v1/bundle/v1pb"
|
||||
)
|
||||
|
||||
// ManifestToProto converts a bundle Manifest to its protobuf wire-form,
|
||||
// defined in v1/bundle/manifest.proto. The compiled-only fileRegoVersions
|
||||
// cache is intentionally not modeled. Roots presence (nil vs explicit-empty)
|
||||
// is preserved via the `roots_set` wire field.
|
||||
func ManifestToProto(m *Manifest) (*pb.Manifest, error) {
|
||||
if m == nil {
|
||||
return nil, nil
|
||||
}
|
||||
out := &pb.Manifest{
|
||||
Revision: proto.String(m.Revision),
|
||||
}
|
||||
if m.Roots != nil {
|
||||
out.Roots = append([]string(nil), (*m.Roots)...)
|
||||
out.RootsSet = proto.Bool(true)
|
||||
}
|
||||
if len(m.WasmResolvers) > 0 {
|
||||
out.Wasm = make([]*pb.WasmResolver, len(m.WasmResolvers))
|
||||
for i := range m.WasmResolvers {
|
||||
wr, err := wasmResolverToProto(&m.WasmResolvers[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("manifest wasm[%d]: %w", i, err)
|
||||
}
|
||||
out.Wasm[i] = wr
|
||||
}
|
||||
}
|
||||
if m.RegoVersion != nil {
|
||||
out.RegoVersion = proto.Int32(int32(*m.RegoVersion))
|
||||
}
|
||||
if len(m.FileRegoVersions) > 0 {
|
||||
out.FileRegoVersions = make(map[string]int32, len(m.FileRegoVersions))
|
||||
for k, v := range m.FileRegoVersions {
|
||||
out.FileRegoVersions[k] = int32(v)
|
||||
}
|
||||
}
|
||||
if len(m.Metadata) > 0 {
|
||||
s, err := jsonNormalizeStruct(m.Metadata)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("manifest metadata: %w", err)
|
||||
}
|
||||
out.Metadata = s
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func wasmResolverToProto(w *WasmResolver) (*pb.WasmResolver, error) {
|
||||
if w == nil {
|
||||
return nil, nil
|
||||
}
|
||||
out := &pb.WasmResolver{
|
||||
Entrypoint: proto.String(w.Entrypoint),
|
||||
Module: proto.String(w.Module),
|
||||
}
|
||||
if len(w.Annotations) > 0 {
|
||||
out.Annotations = make([]*pb.Annotations, len(w.Annotations))
|
||||
for i, a := range w.Annotations {
|
||||
ap, err := annotationsToProto(a)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("annotations[%d]: %w", i, err)
|
||||
}
|
||||
out.Annotations[i] = ap
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func annotationsToProto(a *ast.Annotations) (*pb.Annotations, error) {
|
||||
if a == nil {
|
||||
return nil, nil
|
||||
}
|
||||
out := &pb.Annotations{
|
||||
Scope: proto.String(a.Scope),
|
||||
Title: proto.String(a.Title),
|
||||
Entrypoint: proto.Bool(a.Entrypoint),
|
||||
Description: proto.String(a.Description),
|
||||
Organizations: append([]string(nil), a.Organizations...),
|
||||
}
|
||||
if len(a.RelatedResources) > 0 {
|
||||
out.RelatedResources = make([]*pb.RelatedResourceAnnotation, len(a.RelatedResources))
|
||||
for i, r := range a.RelatedResources {
|
||||
out.RelatedResources[i] = relatedResourceToProto(r)
|
||||
}
|
||||
}
|
||||
if len(a.Authors) > 0 {
|
||||
out.Authors = make([]*pb.AuthorAnnotation, len(a.Authors))
|
||||
for i, au := range a.Authors {
|
||||
out.Authors[i] = authorToProto(au)
|
||||
}
|
||||
}
|
||||
if len(a.Schemas) > 0 {
|
||||
out.Schemas = make([]*pb.SchemaAnnotation, len(a.Schemas))
|
||||
for i, s := range a.Schemas {
|
||||
sa, err := schemaToProto(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("schemas[%d]: %w", i, err)
|
||||
}
|
||||
out.Schemas[i] = sa
|
||||
}
|
||||
}
|
||||
if a.Compile != nil {
|
||||
out.Compile = compileToProto(a.Compile)
|
||||
}
|
||||
if len(a.Custom) > 0 {
|
||||
s, err := jsonNormalizeStruct(a.Custom)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("custom: %w", err)
|
||||
}
|
||||
out.Custom = s
|
||||
}
|
||||
if len(a.Labels) > 0 {
|
||||
s, err := jsonNormalizeStruct(a.Labels)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("labels: %w", err)
|
||||
}
|
||||
out.Labels = s
|
||||
}
|
||||
if a.Location != nil {
|
||||
out.Location = locationToProto(a.Location)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func relatedResourceToProto(r *ast.RelatedResourceAnnotation) *pb.RelatedResourceAnnotation {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return &pb.RelatedResourceAnnotation{
|
||||
Ref: proto.String(r.Ref.String()),
|
||||
Description: proto.String(r.Description),
|
||||
}
|
||||
}
|
||||
|
||||
func authorToProto(a *ast.AuthorAnnotation) *pb.AuthorAnnotation {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
return &pb.AuthorAnnotation{
|
||||
Name: proto.String(a.Name),
|
||||
Email: proto.String(a.Email),
|
||||
}
|
||||
}
|
||||
|
||||
func schemaToProto(s *ast.SchemaAnnotation) (*pb.SchemaAnnotation, error) {
|
||||
if s == nil {
|
||||
return nil, nil
|
||||
}
|
||||
out := &pb.SchemaAnnotation{
|
||||
Path: proto.String(s.Path.String()),
|
||||
Schema: proto.String(s.Schema.String()),
|
||||
}
|
||||
if s.Definition != nil {
|
||||
v, err := jsonNormalizeValue(*s.Definition)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("definition: %w", err)
|
||||
}
|
||||
out.Definition = v
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func compileToProto(c *ast.CompileAnnotation) *pb.CompileAnnotation {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
out := &pb.CompileAnnotation{
|
||||
MaskRule: proto.String(c.MaskRule.String()),
|
||||
}
|
||||
if len(c.Unknowns) > 0 {
|
||||
out.Unknowns = make([]string, len(c.Unknowns))
|
||||
for i, u := range c.Unknowns {
|
||||
out.Unknowns[i] = u.String()
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func locationToProto(l *location.Location) *pb.Location {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return &pb.Location{
|
||||
File: proto.String(l.File),
|
||||
Row: proto.Int32(int32(l.Row)),
|
||||
Col: proto.Int32(int32(l.Col)),
|
||||
}
|
||||
}
|
||||
|
||||
// ManifestFromProto is the inverse of ManifestToProto.
|
||||
func ManifestFromProto(m *pb.Manifest) (*Manifest, error) {
|
||||
if m == nil {
|
||||
return nil, nil
|
||||
}
|
||||
out := &Manifest{
|
||||
Revision: m.GetRevision(),
|
||||
}
|
||||
if m.GetRootsSet() {
|
||||
roots := make([]string, len(m.Roots))
|
||||
copy(roots, m.Roots)
|
||||
out.Roots = &roots
|
||||
}
|
||||
if len(m.Wasm) > 0 {
|
||||
out.WasmResolvers = make([]WasmResolver, len(m.Wasm))
|
||||
for i, wr := range m.Wasm {
|
||||
converted, err := wasmResolverFromProto(wr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("manifest wasm[%d]: %w", i, err)
|
||||
}
|
||||
out.WasmResolvers[i] = converted
|
||||
}
|
||||
}
|
||||
if m.RegoVersion != nil {
|
||||
v := int(*m.RegoVersion)
|
||||
out.RegoVersion = &v
|
||||
}
|
||||
if len(m.FileRegoVersions) > 0 {
|
||||
out.FileRegoVersions = make(map[string]int, len(m.FileRegoVersions))
|
||||
for k, v := range m.FileRegoVersions {
|
||||
out.FileRegoVersions[k] = int(v)
|
||||
}
|
||||
}
|
||||
if m.Metadata != nil {
|
||||
out.Metadata = m.Metadata.AsMap()
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func wasmResolverFromProto(w *pb.WasmResolver) (WasmResolver, error) {
|
||||
out := WasmResolver{
|
||||
Entrypoint: w.GetEntrypoint(),
|
||||
Module: w.GetModule(),
|
||||
}
|
||||
if len(w.Annotations) > 0 {
|
||||
out.Annotations = make([]*ast.Annotations, len(w.Annotations))
|
||||
for i, a := range w.Annotations {
|
||||
converted, err := annotationsFromProto(a)
|
||||
if err != nil {
|
||||
return WasmResolver{}, fmt.Errorf("annotations[%d]: %w", i, err)
|
||||
}
|
||||
out.Annotations[i] = converted
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func annotationsFromProto(a *pb.Annotations) (*ast.Annotations, error) {
|
||||
if a == nil {
|
||||
return nil, nil
|
||||
}
|
||||
out := &ast.Annotations{
|
||||
Scope: a.GetScope(),
|
||||
Title: a.GetTitle(),
|
||||
Entrypoint: a.GetEntrypoint(),
|
||||
Description: a.GetDescription(),
|
||||
}
|
||||
if len(a.Organizations) > 0 {
|
||||
out.Organizations = append([]string(nil), a.Organizations...)
|
||||
}
|
||||
if len(a.RelatedResources) > 0 {
|
||||
out.RelatedResources = make([]*ast.RelatedResourceAnnotation, len(a.RelatedResources))
|
||||
for i, r := range a.RelatedResources {
|
||||
converted, err := relatedResourceFromProto(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("related_resources[%d]: %w", i, err)
|
||||
}
|
||||
out.RelatedResources[i] = converted
|
||||
}
|
||||
}
|
||||
if len(a.Authors) > 0 {
|
||||
out.Authors = make([]*ast.AuthorAnnotation, len(a.Authors))
|
||||
for i, au := range a.Authors {
|
||||
out.Authors[i] = authorFromProto(au)
|
||||
}
|
||||
}
|
||||
if len(a.Schemas) > 0 {
|
||||
out.Schemas = make([]*ast.SchemaAnnotation, len(a.Schemas))
|
||||
for i, s := range a.Schemas {
|
||||
converted, err := schemaFromProto(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("schemas[%d]: %w", i, err)
|
||||
}
|
||||
out.Schemas[i] = converted
|
||||
}
|
||||
}
|
||||
if a.Compile != nil {
|
||||
converted, err := compileFromProto(a.Compile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("compile: %w", err)
|
||||
}
|
||||
out.Compile = converted
|
||||
}
|
||||
if a.Custom != nil {
|
||||
out.Custom = a.Custom.AsMap()
|
||||
}
|
||||
if a.Labels != nil {
|
||||
out.Labels = a.Labels.AsMap()
|
||||
}
|
||||
if a.Location != nil {
|
||||
out.Location = locationFromProto(a.Location)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func relatedResourceFromProto(r *pb.RelatedResourceAnnotation) (*ast.RelatedResourceAnnotation, error) {
|
||||
if r == nil {
|
||||
return nil, nil
|
||||
}
|
||||
out := &ast.RelatedResourceAnnotation{
|
||||
Description: r.GetDescription(),
|
||||
}
|
||||
if ref := r.GetRef(); ref != "" {
|
||||
u, err := url.Parse(ref)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ref %q: %w", ref, err)
|
||||
}
|
||||
out.Ref = *u
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func authorFromProto(a *pb.AuthorAnnotation) *ast.AuthorAnnotation {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
return &ast.AuthorAnnotation{
|
||||
Name: a.GetName(),
|
||||
Email: a.GetEmail(),
|
||||
}
|
||||
}
|
||||
|
||||
func schemaFromProto(s *pb.SchemaAnnotation) (*ast.SchemaAnnotation, error) {
|
||||
if s == nil {
|
||||
return nil, nil
|
||||
}
|
||||
out := &ast.SchemaAnnotation{}
|
||||
if p := s.GetPath(); p != "" {
|
||||
ref, err := ast.ParseRef(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("path %q: %w", p, err)
|
||||
}
|
||||
out.Path = ref
|
||||
}
|
||||
if sc := s.GetSchema(); sc != "" {
|
||||
ref, err := ast.ParseSchemaRef(sc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("schema %q: %w", sc, err)
|
||||
}
|
||||
out.Schema = ref
|
||||
}
|
||||
if s.Definition != nil {
|
||||
def := s.Definition.AsInterface()
|
||||
out.Definition = &def
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func compileFromProto(c *pb.CompileAnnotation) (*ast.CompileAnnotation, error) {
|
||||
if c == nil {
|
||||
return nil, nil
|
||||
}
|
||||
out := &ast.CompileAnnotation{}
|
||||
if mr := c.GetMaskRule(); mr != "" {
|
||||
ref, err := ast.ParseRef(mr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mask_rule %q: %w", mr, err)
|
||||
}
|
||||
out.MaskRule = ref
|
||||
}
|
||||
if len(c.Unknowns) > 0 {
|
||||
out.Unknowns = make([]ast.Ref, len(c.Unknowns))
|
||||
for i, u := range c.Unknowns {
|
||||
ref, err := ast.ParseRef(u)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unknowns[%d] %q: %w", i, u, err)
|
||||
}
|
||||
out.Unknowns[i] = ref
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func locationFromProto(l *pb.Location) *location.Location {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return &location.Location{
|
||||
File: l.GetFile(),
|
||||
Row: int(l.GetRow()),
|
||||
Col: int(l.GetCol()),
|
||||
}
|
||||
}
|
||||
|
||||
// jsonNormalizeStruct routes a map through JSON before structpb.NewStruct
|
||||
// so the proto path accepts the same value types the JSON path does.
|
||||
func jsonNormalizeStruct(m map[string]any) (*structpb.Struct, error) {
|
||||
bs, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var normalized map[string]any
|
||||
if err := json.Unmarshal(bs, &normalized); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return structpb.NewStruct(normalized)
|
||||
}
|
||||
|
||||
func jsonNormalizeValue(v any) (*structpb.Value, error) {
|
||||
bs, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var normalized any
|
||||
if err := json.Unmarshal(bs, &normalized); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return structpb.NewValue(normalized)
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
// Copyright 2026 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package bundle
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/open-policy-agent/opa/v1/ast"
|
||||
"github.com/open-policy-agent/opa/v1/ast/location"
|
||||
pb "github.com/open-policy-agent/opa/v1/bundle/v1pb"
|
||||
)
|
||||
|
||||
func TestManifestProtoRoundTrip(t *testing.T) {
|
||||
regoV1 := 1
|
||||
roots := []string{"", "a/b"}
|
||||
relatedURL, err := url.Parse("https://example.com/policy")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
def := any(map[string]any{"type": "string"})
|
||||
|
||||
m := &Manifest{
|
||||
Revision: "rev-1",
|
||||
Roots: &roots,
|
||||
WasmResolvers: []WasmResolver{{
|
||||
Entrypoint: "data.example.allow",
|
||||
Module: "/policy.wasm",
|
||||
Annotations: []*ast.Annotations{{
|
||||
Scope: "rule",
|
||||
Title: "t",
|
||||
Entrypoint: true,
|
||||
Description: "d",
|
||||
Organizations: []string{"o1", "o2"},
|
||||
RelatedResources: []*ast.RelatedResourceAnnotation{{
|
||||
Ref: *relatedURL,
|
||||
Description: "rr",
|
||||
}},
|
||||
Authors: []*ast.AuthorAnnotation{{Name: "Ada", Email: "ada@example.com"}},
|
||||
Schemas: []*ast.SchemaAnnotation{{
|
||||
Path: ast.MustParseRef("input.x"),
|
||||
Schema: ast.MustParseRef("schema.foo"),
|
||||
Definition: &def,
|
||||
}},
|
||||
Compile: &ast.CompileAnnotation{
|
||||
MaskRule: ast.MustParseRef("data.mask.rule"),
|
||||
Unknowns: []ast.Ref{ast.MustParseRef("input.unknown")},
|
||||
},
|
||||
Custom: map[string]any{"k": "v"},
|
||||
Labels: map[string]any{"team": "core"},
|
||||
Location: &location.Location{File: "f.rego", Row: 1, Col: 2},
|
||||
}},
|
||||
}},
|
||||
RegoVersion: ®oV1,
|
||||
FileRegoVersions: map[string]int{"a.rego": 1},
|
||||
Metadata: map[string]any{"k": "v", "n": float64(7)},
|
||||
}
|
||||
|
||||
pbManifest, err := ManifestToProto(m)
|
||||
if err != nil {
|
||||
t.Fatalf("to proto: %v", err)
|
||||
}
|
||||
bs, err := proto.Marshal(pbManifest)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
decoded := &pb.Manifest{}
|
||||
if err := proto.Unmarshal(bs, decoded); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
got, err := ManifestFromProto(decoded)
|
||||
if err != nil {
|
||||
t.Fatalf("from proto: %v", err)
|
||||
}
|
||||
|
||||
if !m.Equal(*got) {
|
||||
t.Fatal("manifest semantic equality failed after round trip")
|
||||
}
|
||||
if !reflect.DeepEqual(m.FileRegoVersions, got.FileRegoVersions) {
|
||||
t.Fatalf("file rego versions: want %v, got %v", m.FileRegoVersions, got.FileRegoVersions)
|
||||
}
|
||||
if got.WasmResolvers[0].Annotations[0].Compile.MaskRule.String() != "data.mask.rule" {
|
||||
t.Fatalf("compile mask rule round-trip lost: %v", got.WasmResolvers[0].Annotations[0].Compile.MaskRule)
|
||||
}
|
||||
if got.WasmResolvers[0].Annotations[0].RelatedResources[0].Ref.String() != "https://example.com/policy" {
|
||||
t.Fatalf("related resource ref round-trip lost: %v", got.WasmResolvers[0].Annotations[0].RelatedResources[0].Ref)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManifestToProtoNil(t *testing.T) {
|
||||
got, err := ManifestToProto(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Fatalf("expected nil, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManifestFromProtoNil(t *testing.T) {
|
||||
got, err := ManifestFromProto(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Fatalf("expected nil, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestManifestMetadataAcceptsJSONTypes pins fidelity parity with the
|
||||
// JSON path. structpb.NewStruct rejects ints, json.RawMessage,
|
||||
// time-like values, etc.; the encoder routes Metadata through JSON so
|
||||
// the proto path accepts whatever the JSON path does.
|
||||
func TestManifestMetadataAcceptsJSONTypes(t *testing.T) {
|
||||
m := &Manifest{
|
||||
Revision: "r",
|
||||
Metadata: map[string]any{
|
||||
"count": int(7),
|
||||
"raw": json.RawMessage(`{"k":"v"}`),
|
||||
"nested": map[string]any{
|
||||
"items": []any{int64(1), int64(2)},
|
||||
},
|
||||
},
|
||||
}
|
||||
pm, err := ManifestToProto(m)
|
||||
if err != nil {
|
||||
t.Fatalf("ManifestToProto rejected JSON-friendly types: %v", err)
|
||||
}
|
||||
out, err := ManifestFromProto(pm)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := out.Metadata
|
||||
if got["count"].(float64) != 7 {
|
||||
t.Fatalf("count not preserved: %v", got["count"])
|
||||
}
|
||||
raw, ok := got["raw"].(map[string]any)
|
||||
if !ok || raw["k"].(string) != "v" {
|
||||
t.Fatalf("json.RawMessage not preserved: %v", got["raw"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestManifestFromProtoMalformedRef(t *testing.T) {
|
||||
pm := &pb.Manifest{
|
||||
Wasm: []*pb.WasmResolver{{
|
||||
Annotations: []*pb.Annotations{{
|
||||
Compile: &pb.CompileAnnotation{MaskRule: proto.String("not a ref!!!")},
|
||||
}},
|
||||
}},
|
||||
}
|
||||
if _, err := ManifestFromProto(pm); err == nil {
|
||||
t.Fatal("expected error for malformed mask_rule, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestManifestRootsPresenceRoundTrip pins the nil-vs-explicit-empty
|
||||
// distinction across the proto round-trip. `repeated string` cannot
|
||||
// carry that bit on its own; the `roots_set` wire field does.
|
||||
func TestManifestRootsPresenceRoundTrip(t *testing.T) {
|
||||
cases := []struct {
|
||||
note string
|
||||
in *[]string
|
||||
}{
|
||||
{note: "nil (default to [\"\"])", in: nil},
|
||||
{note: "explicit empty (owns nothing)", in: &[]string{}},
|
||||
{note: "populated", in: &[]string{"a", "b"}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
pm, err := ManifestToProto(&Manifest{Revision: "r", Roots: tc.in})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := ManifestFromProto(pm)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if (tc.in == nil) != (out.Roots == nil) {
|
||||
t.Fatalf("roots nil-ness lost: in=%v out=%v", tc.in, out.Roots)
|
||||
}
|
||||
if tc.in != nil && !reflect.DeepEqual(*tc.in, *out.Roots) {
|
||||
t.Fatalf("roots values lost: want %v, got %v", *tc.in, *out.Roots)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSchemaAnnotationBareVarRoundTrip pins the schema-ref decoder.
|
||||
// Annotations using `schemas: [{input: schema}]` produce a bare Var Ref
|
||||
// from the canonical parser (parseSchemaRef in v1/ast). The encoder
|
||||
// serializes that as the literal "schema". ast.ParseRef cannot decode a
|
||||
// bare Var, so the proto round-trip used to fail with `expected ref but
|
||||
// got schema` — load-blocking for any annotation using this idiom.
|
||||
func TestSchemaAnnotationBareVarRoundTrip(t *testing.T) {
|
||||
cases := []struct {
|
||||
note string
|
||||
ref ast.Ref
|
||||
}{
|
||||
{note: "bare schema var", ref: ast.SchemaRootRef.Copy()},
|
||||
{note: "schema with subpath", ref: ast.MustParseRef("schema.foo")},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
m := &Manifest{
|
||||
Revision: "r1",
|
||||
WasmResolvers: []WasmResolver{{
|
||||
Annotations: []*ast.Annotations{{
|
||||
Schemas: []*ast.SchemaAnnotation{{
|
||||
Path: ast.MustParseRef("input.x"),
|
||||
Schema: tc.ref,
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
}
|
||||
|
||||
pm, err := ManifestToProto(m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := ManifestFromProto(pm)
|
||||
if err != nil {
|
||||
t.Fatalf("decode failed for %v: %v", tc.ref, err)
|
||||
}
|
||||
gotRef := out.WasmResolvers[0].Annotations[0].Schemas[0].Schema
|
||||
if !gotRef.Equal(tc.ref) {
|
||||
t.Fatalf("schema ref not preserved: want %v, got %v", tc.ref, gotRef)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,751 @@
|
||||
// Copyright 2026 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.1
|
||||
// source: v1/bundle/manifest.proto
|
||||
|
||||
package v1pb
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
structpb "google.golang.org/protobuf/types/known/structpb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// Manifest mirrors `bundle.Manifest` in v1/bundle/bundle.go.
|
||||
type Manifest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Bundle revision string.
|
||||
Revision *string `protobuf:"bytes,1,opt,name=revision" json:"revision,omitempty"`
|
||||
// Root paths the bundle owns. See `roots_set` for nil-vs-empty.
|
||||
Roots []string `protobuf:"bytes,2,rep,name=roots" json:"roots,omitempty"`
|
||||
// Wasm resolvers attached to the bundle. JSON key is `wasm`.
|
||||
Wasm []*WasmResolver `protobuf:"bytes,3,rep,name=wasm" json:"wasm,omitempty"`
|
||||
// Global Rego version for the bundle. Currently 0 (RegoV0) or 1 (RegoV1).
|
||||
RegoVersion *int32 `protobuf:"varint,4,opt,name=rego_version,json=regoVersion" json:"rego_version,omitempty"`
|
||||
// Per-file Rego version overrides keyed by file path.
|
||||
FileRegoVersions map[string]int32 `protobuf:"bytes,5,rep,name=file_rego_versions,json=fileRegoVersions" json:"file_rego_versions,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"`
|
||||
// Free-form metadata object. Modeled as `Struct` because the Go field
|
||||
// is `map[string]any`.
|
||||
Metadata *structpb.Struct `protobuf:"bytes,6,opt,name=metadata" json:"metadata,omitempty"`
|
||||
// True iff `bundle.Manifest.Roots` was non-nil. `repeated string` can't
|
||||
// distinguish nil (default to [""]) from explicit-empty (owns no paths).
|
||||
RootsSet *bool `protobuf:"varint,7,opt,name=roots_set,json=rootsSet" json:"roots_set,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Manifest) Reset() {
|
||||
*x = Manifest{}
|
||||
mi := &file_v1_bundle_manifest_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Manifest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Manifest) ProtoMessage() {}
|
||||
|
||||
func (x *Manifest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_bundle_manifest_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Manifest.ProtoReflect.Descriptor instead.
|
||||
func (*Manifest) Descriptor() ([]byte, []int) {
|
||||
return file_v1_bundle_manifest_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Manifest) GetRevision() string {
|
||||
if x != nil && x.Revision != nil {
|
||||
return *x.Revision
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Manifest) GetRoots() []string {
|
||||
if x != nil {
|
||||
return x.Roots
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Manifest) GetWasm() []*WasmResolver {
|
||||
if x != nil {
|
||||
return x.Wasm
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Manifest) GetRegoVersion() int32 {
|
||||
if x != nil && x.RegoVersion != nil {
|
||||
return *x.RegoVersion
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Manifest) GetFileRegoVersions() map[string]int32 {
|
||||
if x != nil {
|
||||
return x.FileRegoVersions
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Manifest) GetMetadata() *structpb.Struct {
|
||||
if x != nil {
|
||||
return x.Metadata
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Manifest) GetRootsSet() bool {
|
||||
if x != nil && x.RootsSet != nil {
|
||||
return *x.RootsSet
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// WasmResolver mirrors `bundle.WasmResolver` in v1/bundle/bundle.go.
|
||||
type WasmResolver struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Entrypoint policy ref this resolver targets.
|
||||
Entrypoint *string `protobuf:"bytes,1,opt,name=entrypoint" json:"entrypoint,omitempty"`
|
||||
// Path to the wasm module within the bundle.
|
||||
Module *string `protobuf:"bytes,2,opt,name=module" json:"module,omitempty"`
|
||||
// Rego annotations attached to the entrypoint.
|
||||
Annotations []*Annotations `protobuf:"bytes,3,rep,name=annotations" json:"annotations,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *WasmResolver) Reset() {
|
||||
*x = WasmResolver{}
|
||||
mi := &file_v1_bundle_manifest_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *WasmResolver) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*WasmResolver) ProtoMessage() {}
|
||||
|
||||
func (x *WasmResolver) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_bundle_manifest_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use WasmResolver.ProtoReflect.Descriptor instead.
|
||||
func (*WasmResolver) Descriptor() ([]byte, []int) {
|
||||
return file_v1_bundle_manifest_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *WasmResolver) GetEntrypoint() string {
|
||||
if x != nil && x.Entrypoint != nil {
|
||||
return *x.Entrypoint
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *WasmResolver) GetModule() string {
|
||||
if x != nil && x.Module != nil {
|
||||
return *x.Module
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *WasmResolver) GetAnnotations() []*Annotations {
|
||||
if x != nil {
|
||||
return x.Annotations
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Annotations mirrors `ast.Annotations` in v1/ast/annotations.go.
|
||||
type Annotations struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Scope *string `protobuf:"bytes,1,opt,name=scope" json:"scope,omitempty"`
|
||||
Title *string `protobuf:"bytes,2,opt,name=title" json:"title,omitempty"`
|
||||
Entrypoint *bool `protobuf:"varint,3,opt,name=entrypoint" json:"entrypoint,omitempty"`
|
||||
Description *string `protobuf:"bytes,4,opt,name=description" json:"description,omitempty"`
|
||||
Organizations []string `protobuf:"bytes,5,rep,name=organizations" json:"organizations,omitempty"`
|
||||
RelatedResources []*RelatedResourceAnnotation `protobuf:"bytes,6,rep,name=related_resources,json=relatedResources" json:"related_resources,omitempty"`
|
||||
Authors []*AuthorAnnotation `protobuf:"bytes,7,rep,name=authors" json:"authors,omitempty"`
|
||||
Schemas []*SchemaAnnotation `protobuf:"bytes,8,rep,name=schemas" json:"schemas,omitempty"`
|
||||
Compile *CompileAnnotation `protobuf:"bytes,9,opt,name=compile" json:"compile,omitempty"`
|
||||
// `custom` and `labels` are `map[string]any` in Go — genuinely
|
||||
// free-form, so `Struct` is the right model.
|
||||
Custom *structpb.Struct `protobuf:"bytes,10,opt,name=custom" json:"custom,omitempty"`
|
||||
Labels *structpb.Struct `protobuf:"bytes,11,opt,name=labels" json:"labels,omitempty"`
|
||||
Location *Location `protobuf:"bytes,12,opt,name=location" json:"location,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Annotations) Reset() {
|
||||
*x = Annotations{}
|
||||
mi := &file_v1_bundle_manifest_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Annotations) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Annotations) ProtoMessage() {}
|
||||
|
||||
func (x *Annotations) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_bundle_manifest_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Annotations.ProtoReflect.Descriptor instead.
|
||||
func (*Annotations) Descriptor() ([]byte, []int) {
|
||||
return file_v1_bundle_manifest_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *Annotations) GetScope() string {
|
||||
if x != nil && x.Scope != nil {
|
||||
return *x.Scope
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Annotations) GetTitle() string {
|
||||
if x != nil && x.Title != nil {
|
||||
return *x.Title
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Annotations) GetEntrypoint() bool {
|
||||
if x != nil && x.Entrypoint != nil {
|
||||
return *x.Entrypoint
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Annotations) GetDescription() string {
|
||||
if x != nil && x.Description != nil {
|
||||
return *x.Description
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Annotations) GetOrganizations() []string {
|
||||
if x != nil {
|
||||
return x.Organizations
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Annotations) GetRelatedResources() []*RelatedResourceAnnotation {
|
||||
if x != nil {
|
||||
return x.RelatedResources
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Annotations) GetAuthors() []*AuthorAnnotation {
|
||||
if x != nil {
|
||||
return x.Authors
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Annotations) GetSchemas() []*SchemaAnnotation {
|
||||
if x != nil {
|
||||
return x.Schemas
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Annotations) GetCompile() *CompileAnnotation {
|
||||
if x != nil {
|
||||
return x.Compile
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Annotations) GetCustom() *structpb.Struct {
|
||||
if x != nil {
|
||||
return x.Custom
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Annotations) GetLabels() *structpb.Struct {
|
||||
if x != nil {
|
||||
return x.Labels
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Annotations) GetLocation() *Location {
|
||||
if x != nil {
|
||||
return x.Location
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SchemaAnnotation mirrors `ast.SchemaAnnotation`. Path/Schema are
|
||||
// `ast.Ref` in Go (a list of terms); the wire form is the canonical
|
||||
// dotted ref string (e.g. `data.foo.bar`). Modeling the term tree
|
||||
// faithfully would pull most of the AST into this schema and isn't
|
||||
// worth the cost for annotations.
|
||||
type SchemaAnnotation struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Path *string `protobuf:"bytes,1,opt,name=path" json:"path,omitempty"`
|
||||
Schema *string `protobuf:"bytes,2,opt,name=schema" json:"schema,omitempty"`
|
||||
// `*any` on the Go side — a parsed JSON Schema document or any
|
||||
// JSON value. `Value` (not `Struct`) because the top level may be
|
||||
// a scalar, list, or null, not just an object.
|
||||
Definition *structpb.Value `protobuf:"bytes,3,opt,name=definition" json:"definition,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SchemaAnnotation) Reset() {
|
||||
*x = SchemaAnnotation{}
|
||||
mi := &file_v1_bundle_manifest_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SchemaAnnotation) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SchemaAnnotation) ProtoMessage() {}
|
||||
|
||||
func (x *SchemaAnnotation) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_bundle_manifest_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SchemaAnnotation.ProtoReflect.Descriptor instead.
|
||||
func (*SchemaAnnotation) Descriptor() ([]byte, []int) {
|
||||
return file_v1_bundle_manifest_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *SchemaAnnotation) GetPath() string {
|
||||
if x != nil && x.Path != nil {
|
||||
return *x.Path
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SchemaAnnotation) GetSchema() string {
|
||||
if x != nil && x.Schema != nil {
|
||||
return *x.Schema
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SchemaAnnotation) GetDefinition() *structpb.Value {
|
||||
if x != nil {
|
||||
return x.Definition
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompileAnnotation mirrors `ast.CompileAnnotation`. Refs are the
|
||||
// canonical dotted form; see SchemaAnnotation for the trade-off.
|
||||
type CompileAnnotation struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Unknowns []string `protobuf:"bytes,1,rep,name=unknowns" json:"unknowns,omitempty"`
|
||||
MaskRule *string `protobuf:"bytes,2,opt,name=mask_rule,json=maskRule" json:"mask_rule,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CompileAnnotation) Reset() {
|
||||
*x = CompileAnnotation{}
|
||||
mi := &file_v1_bundle_manifest_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CompileAnnotation) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CompileAnnotation) ProtoMessage() {}
|
||||
|
||||
func (x *CompileAnnotation) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_bundle_manifest_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CompileAnnotation.ProtoReflect.Descriptor instead.
|
||||
func (*CompileAnnotation) Descriptor() ([]byte, []int) {
|
||||
return file_v1_bundle_manifest_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *CompileAnnotation) GetUnknowns() []string {
|
||||
if x != nil {
|
||||
return x.Unknowns
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CompileAnnotation) GetMaskRule() string {
|
||||
if x != nil && x.MaskRule != nil {
|
||||
return *x.MaskRule
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// AuthorAnnotation mirrors `ast.AuthorAnnotation`.
|
||||
type AuthorAnnotation struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Name *string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
|
||||
Email *string `protobuf:"bytes,2,opt,name=email" json:"email,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AuthorAnnotation) Reset() {
|
||||
*x = AuthorAnnotation{}
|
||||
mi := &file_v1_bundle_manifest_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AuthorAnnotation) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AuthorAnnotation) ProtoMessage() {}
|
||||
|
||||
func (x *AuthorAnnotation) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_bundle_manifest_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AuthorAnnotation.ProtoReflect.Descriptor instead.
|
||||
func (*AuthorAnnotation) Descriptor() ([]byte, []int) {
|
||||
return file_v1_bundle_manifest_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *AuthorAnnotation) GetName() string {
|
||||
if x != nil && x.Name != nil {
|
||||
return *x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AuthorAnnotation) GetEmail() string {
|
||||
if x != nil && x.Email != nil {
|
||||
return *x.Email
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// RelatedResourceAnnotation mirrors `ast.RelatedResourceAnnotation`.
|
||||
// `Ref` is a `url.URL` in Go, serialized to its `String()` form.
|
||||
type RelatedResourceAnnotation struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Ref *string `protobuf:"bytes,1,opt,name=ref" json:"ref,omitempty"`
|
||||
Description *string `protobuf:"bytes,2,opt,name=description" json:"description,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RelatedResourceAnnotation) Reset() {
|
||||
*x = RelatedResourceAnnotation{}
|
||||
mi := &file_v1_bundle_manifest_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RelatedResourceAnnotation) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RelatedResourceAnnotation) ProtoMessage() {}
|
||||
|
||||
func (x *RelatedResourceAnnotation) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_bundle_manifest_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RelatedResourceAnnotation.ProtoReflect.Descriptor instead.
|
||||
func (*RelatedResourceAnnotation) Descriptor() ([]byte, []int) {
|
||||
return file_v1_bundle_manifest_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *RelatedResourceAnnotation) GetRef() string {
|
||||
if x != nil && x.Ref != nil {
|
||||
return *x.Ref
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RelatedResourceAnnotation) GetDescription() string {
|
||||
if x != nil && x.Description != nil {
|
||||
return *x.Description
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Location mirrors `ast.Location` (= `location.Location`). Only the
|
||||
// File/Row/Col triple is wire-relevant; `Text`, `Offset`, and `Tabs`
|
||||
// are tagged `json:"-"` and intentionally absent.
|
||||
//
|
||||
// Distinct from `ir.Location`, which is promoted onto the `Stmt`
|
||||
// envelope in plan.proto. The two are independent Go types.
|
||||
type Location struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
File *string `protobuf:"bytes,1,opt,name=file" json:"file,omitempty"`
|
||||
Row *int32 `protobuf:"varint,2,opt,name=row" json:"row,omitempty"`
|
||||
Col *int32 `protobuf:"varint,3,opt,name=col" json:"col,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Location) Reset() {
|
||||
*x = Location{}
|
||||
mi := &file_v1_bundle_manifest_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Location) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Location) ProtoMessage() {}
|
||||
|
||||
func (x *Location) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_bundle_manifest_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Location.ProtoReflect.Descriptor instead.
|
||||
func (*Location) Descriptor() ([]byte, []int) {
|
||||
return file_v1_bundle_manifest_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *Location) GetFile() string {
|
||||
if x != nil && x.File != nil {
|
||||
return *x.File
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Location) GetRow() int32 {
|
||||
if x != nil && x.Row != nil {
|
||||
return *x.Row
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Location) GetCol() int32 {
|
||||
if x != nil && x.Col != nil {
|
||||
return *x.Col
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var File_v1_bundle_manifest_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_v1_bundle_manifest_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x18v1/bundle/manifest.proto\x12\ropa.bundle.v1\x1a\x1cgoogle/protobuf/struct.proto\"\x84\x03\n" +
|
||||
"\bManifest\x12\x1a\n" +
|
||||
"\brevision\x18\x01 \x01(\tR\brevision\x12\x14\n" +
|
||||
"\x05roots\x18\x02 \x03(\tR\x05roots\x12/\n" +
|
||||
"\x04wasm\x18\x03 \x03(\v2\x1b.opa.bundle.v1.WasmResolverR\x04wasm\x12!\n" +
|
||||
"\frego_version\x18\x04 \x01(\x05R\vregoVersion\x12[\n" +
|
||||
"\x12file_rego_versions\x18\x05 \x03(\v2-.opa.bundle.v1.Manifest.FileRegoVersionsEntryR\x10fileRegoVersions\x123\n" +
|
||||
"\bmetadata\x18\x06 \x01(\v2\x17.google.protobuf.StructR\bmetadata\x12\x1b\n" +
|
||||
"\troots_set\x18\a \x01(\bR\brootsSet\x1aC\n" +
|
||||
"\x15FileRegoVersionsEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\x05R\x05value:\x028\x01\"\x84\x01\n" +
|
||||
"\fWasmResolver\x12\x1e\n" +
|
||||
"\n" +
|
||||
"entrypoint\x18\x01 \x01(\tR\n" +
|
||||
"entrypoint\x12\x16\n" +
|
||||
"\x06module\x18\x02 \x01(\tR\x06module\x12<\n" +
|
||||
"\vannotations\x18\x03 \x03(\v2\x1a.opa.bundle.v1.AnnotationsR\vannotations\"\xc1\x04\n" +
|
||||
"\vAnnotations\x12\x14\n" +
|
||||
"\x05scope\x18\x01 \x01(\tR\x05scope\x12\x14\n" +
|
||||
"\x05title\x18\x02 \x01(\tR\x05title\x12\x1e\n" +
|
||||
"\n" +
|
||||
"entrypoint\x18\x03 \x01(\bR\n" +
|
||||
"entrypoint\x12 \n" +
|
||||
"\vdescription\x18\x04 \x01(\tR\vdescription\x12$\n" +
|
||||
"\rorganizations\x18\x05 \x03(\tR\rorganizations\x12U\n" +
|
||||
"\x11related_resources\x18\x06 \x03(\v2(.opa.bundle.v1.RelatedResourceAnnotationR\x10relatedResources\x129\n" +
|
||||
"\aauthors\x18\a \x03(\v2\x1f.opa.bundle.v1.AuthorAnnotationR\aauthors\x129\n" +
|
||||
"\aschemas\x18\b \x03(\v2\x1f.opa.bundle.v1.SchemaAnnotationR\aschemas\x12:\n" +
|
||||
"\acompile\x18\t \x01(\v2 .opa.bundle.v1.CompileAnnotationR\acompile\x12/\n" +
|
||||
"\x06custom\x18\n" +
|
||||
" \x01(\v2\x17.google.protobuf.StructR\x06custom\x12/\n" +
|
||||
"\x06labels\x18\v \x01(\v2\x17.google.protobuf.StructR\x06labels\x123\n" +
|
||||
"\blocation\x18\f \x01(\v2\x17.opa.bundle.v1.LocationR\blocation\"v\n" +
|
||||
"\x10SchemaAnnotation\x12\x12\n" +
|
||||
"\x04path\x18\x01 \x01(\tR\x04path\x12\x16\n" +
|
||||
"\x06schema\x18\x02 \x01(\tR\x06schema\x126\n" +
|
||||
"\n" +
|
||||
"definition\x18\x03 \x01(\v2\x16.google.protobuf.ValueR\n" +
|
||||
"definition\"L\n" +
|
||||
"\x11CompileAnnotation\x12\x1a\n" +
|
||||
"\bunknowns\x18\x01 \x03(\tR\bunknowns\x12\x1b\n" +
|
||||
"\tmask_rule\x18\x02 \x01(\tR\bmaskRule\"<\n" +
|
||||
"\x10AuthorAnnotation\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" +
|
||||
"\x05email\x18\x02 \x01(\tR\x05email\"O\n" +
|
||||
"\x19RelatedResourceAnnotation\x12\x10\n" +
|
||||
"\x03ref\x18\x01 \x01(\tR\x03ref\x12 \n" +
|
||||
"\vdescription\x18\x02 \x01(\tR\vdescription\"B\n" +
|
||||
"\bLocation\x12\x12\n" +
|
||||
"\x04file\x18\x01 \x01(\tR\x04file\x12\x10\n" +
|
||||
"\x03row\x18\x02 \x01(\x05R\x03row\x12\x10\n" +
|
||||
"\x03col\x18\x03 \x01(\x05R\x03colB3P\x01Z/github.com/open-policy-agent/opa/v1/bundle/v1pbb\beditionsp\xe8\a"
|
||||
|
||||
var (
|
||||
file_v1_bundle_manifest_proto_rawDescOnce sync.Once
|
||||
file_v1_bundle_manifest_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_v1_bundle_manifest_proto_rawDescGZIP() []byte {
|
||||
file_v1_bundle_manifest_proto_rawDescOnce.Do(func() {
|
||||
file_v1_bundle_manifest_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_v1_bundle_manifest_proto_rawDesc), len(file_v1_bundle_manifest_proto_rawDesc)))
|
||||
})
|
||||
return file_v1_bundle_manifest_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_v1_bundle_manifest_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
||||
var file_v1_bundle_manifest_proto_goTypes = []any{
|
||||
(*Manifest)(nil), // 0: opa.bundle.v1.Manifest
|
||||
(*WasmResolver)(nil), // 1: opa.bundle.v1.WasmResolver
|
||||
(*Annotations)(nil), // 2: opa.bundle.v1.Annotations
|
||||
(*SchemaAnnotation)(nil), // 3: opa.bundle.v1.SchemaAnnotation
|
||||
(*CompileAnnotation)(nil), // 4: opa.bundle.v1.CompileAnnotation
|
||||
(*AuthorAnnotation)(nil), // 5: opa.bundle.v1.AuthorAnnotation
|
||||
(*RelatedResourceAnnotation)(nil), // 6: opa.bundle.v1.RelatedResourceAnnotation
|
||||
(*Location)(nil), // 7: opa.bundle.v1.Location
|
||||
nil, // 8: opa.bundle.v1.Manifest.FileRegoVersionsEntry
|
||||
(*structpb.Struct)(nil), // 9: google.protobuf.Struct
|
||||
(*structpb.Value)(nil), // 10: google.protobuf.Value
|
||||
}
|
||||
var file_v1_bundle_manifest_proto_depIdxs = []int32{
|
||||
1, // 0: opa.bundle.v1.Manifest.wasm:type_name -> opa.bundle.v1.WasmResolver
|
||||
8, // 1: opa.bundle.v1.Manifest.file_rego_versions:type_name -> opa.bundle.v1.Manifest.FileRegoVersionsEntry
|
||||
9, // 2: opa.bundle.v1.Manifest.metadata:type_name -> google.protobuf.Struct
|
||||
2, // 3: opa.bundle.v1.WasmResolver.annotations:type_name -> opa.bundle.v1.Annotations
|
||||
6, // 4: opa.bundle.v1.Annotations.related_resources:type_name -> opa.bundle.v1.RelatedResourceAnnotation
|
||||
5, // 5: opa.bundle.v1.Annotations.authors:type_name -> opa.bundle.v1.AuthorAnnotation
|
||||
3, // 6: opa.bundle.v1.Annotations.schemas:type_name -> opa.bundle.v1.SchemaAnnotation
|
||||
4, // 7: opa.bundle.v1.Annotations.compile:type_name -> opa.bundle.v1.CompileAnnotation
|
||||
9, // 8: opa.bundle.v1.Annotations.custom:type_name -> google.protobuf.Struct
|
||||
9, // 9: opa.bundle.v1.Annotations.labels:type_name -> google.protobuf.Struct
|
||||
7, // 10: opa.bundle.v1.Annotations.location:type_name -> opa.bundle.v1.Location
|
||||
10, // 11: opa.bundle.v1.SchemaAnnotation.definition:type_name -> google.protobuf.Value
|
||||
12, // [12:12] is the sub-list for method output_type
|
||||
12, // [12:12] is the sub-list for method input_type
|
||||
12, // [12:12] is the sub-list for extension type_name
|
||||
12, // [12:12] is the sub-list for extension extendee
|
||||
0, // [0:12] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_v1_bundle_manifest_proto_init() }
|
||||
func file_v1_bundle_manifest_proto_init() {
|
||||
if File_v1_bundle_manifest_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_bundle_manifest_proto_rawDesc), len(file_v1_bundle_manifest_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 9,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_v1_bundle_manifest_proto_goTypes,
|
||||
DependencyIndexes: file_v1_bundle_manifest_proto_depIdxs,
|
||||
MessageInfos: file_v1_bundle_manifest_proto_msgTypes,
|
||||
}.Build()
|
||||
File_v1_bundle_manifest_proto = out.File
|
||||
file_v1_bundle_manifest_proto_goTypes = nil
|
||||
file_v1_bundle_manifest_proto_depIdxs = nil
|
||||
}
|
||||
+48
-3
@@ -19,6 +19,8 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/compiler/wasm"
|
||||
"github.com/open-policy-agent/opa/internal/debug"
|
||||
"github.com/open-policy-agent/opa/internal/planner"
|
||||
@@ -50,6 +52,15 @@ const (
|
||||
TargetPlan = "plan"
|
||||
)
|
||||
|
||||
// Plan output formats. Only meaningful when Target == TargetPlan.
|
||||
const (
|
||||
PlanFormatJSON = "json"
|
||||
PlanFormatProto = "proto"
|
||||
)
|
||||
|
||||
// PlanFormats contains the list of plan output formats supported by the compiler.
|
||||
var PlanFormats = []string{PlanFormatJSON, PlanFormatProto}
|
||||
|
||||
// Targets contains the list of targets supported by the compiler.
|
||||
var Targets = []string{
|
||||
TargetRego,
|
||||
@@ -73,6 +84,7 @@ type Compiler struct {
|
||||
useRegoAnnotationEntrypoints bool // allow compiler to late-bind entrypoints from annotated rules in policies.
|
||||
optimizationLevel int // how aggressive should optimization be
|
||||
target string // target type (wasm, rego, etc.)
|
||||
planFormat string // plan output format (json or proto)
|
||||
output *io.Writer // output stream to write bundle to
|
||||
entrypointrefs []*ast.Term // validated entrypoints computed from default decision or manually supplied entrypoints
|
||||
compiler *ast.Compiler // rego ast compiler used for semantic checks and rewriting
|
||||
@@ -97,6 +109,7 @@ func New() *Compiler {
|
||||
asBundle: false,
|
||||
optimizationLevel: 0,
|
||||
target: TargetRego,
|
||||
planFormat: PlanFormatJSON,
|
||||
debug: debug.Discard(),
|
||||
regoVersion: ast.DefaultRegoVersion,
|
||||
}
|
||||
@@ -158,6 +171,12 @@ func (c *Compiler) WithTarget(t string) *Compiler {
|
||||
return c
|
||||
}
|
||||
|
||||
// WithPlanFormat sets the wire-format (json or proto) for plan-target builds.
|
||||
func (c *Compiler) WithPlanFormat(f string) *Compiler {
|
||||
c.planFormat = f
|
||||
return c
|
||||
}
|
||||
|
||||
// WithOutput sets the output stream to write the bundle to.
|
||||
func (c *Compiler) WithOutput(w io.Writer) *Compiler {
|
||||
c.output = &w
|
||||
@@ -315,6 +334,13 @@ func (c *Compiler) Build(ctx context.Context) error {
|
||||
return errors.New("rego-version not set")
|
||||
}
|
||||
|
||||
if !slices.Contains(PlanFormats, c.planFormat) {
|
||||
return fmt.Errorf("unsupported plan format %q (want one of %v)", c.planFormat, PlanFormats)
|
||||
}
|
||||
if c.planFormat != PlanFormatJSON && c.target != TargetPlan {
|
||||
return fmt.Errorf("plan format %q is only valid with target %q", c.planFormat, TargetPlan)
|
||||
}
|
||||
|
||||
if err := c.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -374,14 +400,31 @@ func (c *Compiler) Build(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
bs, err := json.Marshal(c.policy)
|
||||
var (
|
||||
bs []byte
|
||||
planPath string
|
||||
err error
|
||||
)
|
||||
switch c.planFormat {
|
||||
case PlanFormatProto:
|
||||
// Deterministic for byte-identical plan.pb across builds.
|
||||
pbPolicy, perr := ir.PolicyToProto(c.policy)
|
||||
if perr != nil {
|
||||
return perr
|
||||
}
|
||||
bs, err = proto.MarshalOptions{Deterministic: true}.Marshal(pbPolicy)
|
||||
planPath = bundle.PlanProtoFile
|
||||
case PlanFormatJSON:
|
||||
bs, err = json.Marshal(c.policy)
|
||||
planPath = bundle.PlanFile
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.bundle.PlanModules = append(c.bundle.PlanModules, bundle.PlanModuleFile{
|
||||
Path: bundle.PlanFile,
|
||||
URL: bundle.PlanFile,
|
||||
Path: planPath,
|
||||
URL: planPath,
|
||||
Raw: bs,
|
||||
})
|
||||
case TargetRego:
|
||||
@@ -396,6 +439,8 @@ func (c *Compiler) Build(ctx context.Context) error {
|
||||
c.bundle.Manifest.Metadata = *c.metadata
|
||||
}
|
||||
|
||||
c.bundle.SetManifestProto(c.target == TargetPlan && c.planFormat == PlanFormatProto)
|
||||
|
||||
if err := c.bundle.FormatModulesWithOptions(bundle.BundleFormatOptions{
|
||||
RegoVersion: c.regoVersion,
|
||||
Capabilities: c.capabilities,
|
||||
|
||||
@@ -104,6 +104,16 @@ func TestCompilerInitErrors(t *testing.T) {
|
||||
c: New().WithTarget("plan"),
|
||||
want: errors.New("plan compilation requires at least one entrypoint"),
|
||||
},
|
||||
{
|
||||
note: "unsupported plan format",
|
||||
c: New().WithPlanFormat("xml"),
|
||||
want: fmt.Errorf("unsupported plan format %q (want one of %v)", "xml", PlanFormats),
|
||||
},
|
||||
{
|
||||
note: "plan format requires plan target",
|
||||
c: New().WithPlanFormat(PlanFormatProto),
|
||||
want: fmt.Errorf("plan format %q is only valid with target %q", PlanFormatProto, TargetPlan),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
@@ -1012,6 +1022,39 @@ func TestCompilerLoadFilesystemWithEnablePrintStatementsTrue(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompilerProtoPlanIsDeterministic(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"test.rego": `
|
||||
package test
|
||||
import rego.v1
|
||||
|
||||
allow if { input.user == "alice" }
|
||||
deny if { input.user == "mallory" }
|
||||
`,
|
||||
}
|
||||
|
||||
// Single root keeps Static.Files (source paths) stable across builds.
|
||||
test.WithTempFS(files, func(root string) {
|
||||
build := func() []byte {
|
||||
compiler := New().
|
||||
WithPaths(root).
|
||||
WithTarget(TargetPlan).
|
||||
WithPlanFormat(PlanFormatProto).
|
||||
WithEntrypoints("test/allow", "test/deny")
|
||||
if err := compiler.Build(t.Context()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return append([]byte(nil), compiler.Bundle().PlanModules[0].Raw...)
|
||||
}
|
||||
|
||||
a := build()
|
||||
b := build()
|
||||
if !bytes.Equal(a, b) {
|
||||
t.Fatalf("plan.pb bytes differ across builds (len %d vs %d)", len(a), len(b))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompilerLoadHonorsFilter(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"test.rego": `
|
||||
|
||||
+340
@@ -0,0 +1,340 @@
|
||||
// Copyright 2026 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ir
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
pb "github.com/open-policy-agent/opa/v1/ir/v1pb"
|
||||
)
|
||||
|
||||
// PolicyToProto converts an IR Policy to its protobuf wire-form,
|
||||
// defined in v1/ir/plan.proto. Returns an error if the policy contains
|
||||
// a Stmt or Val kind not yet covered by the encoder switch.
|
||||
func PolicyToProto(p *Policy) (out *pb.Policy, err error) {
|
||||
if p == nil {
|
||||
return nil, nil
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
out = nil
|
||||
err = fmt.Errorf("ir: PolicyToProto: %v", r)
|
||||
}
|
||||
}()
|
||||
return &pb.Policy{
|
||||
Static: staticToProto(p.Static),
|
||||
Plans: plansToProto(p.Plans),
|
||||
Funcs: funcsToProto(p.Funcs),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func staticToProto(s *Static) *pb.Static {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
out := &pb.Static{
|
||||
Strings: make([]*pb.StringConst, len(s.Strings)),
|
||||
BuiltinFuncs: make([]*pb.BuiltinFunc, len(s.BuiltinFuncs)),
|
||||
Files: make([]*pb.StringConst, len(s.Files)),
|
||||
}
|
||||
for i, sc := range s.Strings {
|
||||
out.Strings[i] = stringConstToProto(sc)
|
||||
}
|
||||
for i, bf := range s.BuiltinFuncs {
|
||||
out.BuiltinFuncs[i] = builtinFuncToProto(bf)
|
||||
}
|
||||
for i, f := range s.Files {
|
||||
out.Files[i] = stringConstToProto(f)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func stringConstToProto(s *StringConst) *pb.StringConst {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return &pb.StringConst{Value: proto.String(s.Value)}
|
||||
}
|
||||
|
||||
func builtinFuncToProto(b *BuiltinFunc) *pb.BuiltinFunc {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return &pb.BuiltinFunc{Name: proto.String(b.Name)}
|
||||
}
|
||||
|
||||
func plansToProto(p *Plans) *pb.Plans {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
out := &pb.Plans{Plans: make([]*pb.Plan, len(p.Plans))}
|
||||
for i, pl := range p.Plans {
|
||||
out.Plans[i] = planToProto(pl)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func planToProto(p *Plan) *pb.Plan {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
out := &pb.Plan{Name: proto.String(p.Name), Blocks: make([]*pb.Block, len(p.Blocks))}
|
||||
for i, b := range p.Blocks {
|
||||
out.Blocks[i] = blockToProto(b)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func funcsToProto(f *Funcs) *pb.Funcs {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
out := &pb.Funcs{Funcs: make([]*pb.Func, len(f.Funcs))}
|
||||
for i, fn := range f.Funcs {
|
||||
out.Funcs[i] = funcToProto(fn)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func funcToProto(f *Func) *pb.Func {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
out := &pb.Func{
|
||||
Name: proto.String(f.Name),
|
||||
Params: localsToInt32s(f.Params),
|
||||
Result: proto.Int32(toInt32(f.Return)),
|
||||
Blocks: make([]*pb.Block, len(f.Blocks)),
|
||||
Path: f.Path,
|
||||
}
|
||||
for i, b := range f.Blocks {
|
||||
out.Blocks[i] = blockToProto(b)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func blockToProto(b *Block) *pb.Block {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
out := &pb.Block{Stmts: make([]*pb.Stmt, len(b.Stmts))}
|
||||
for i, s := range b.Stmts {
|
||||
out.Stmts[i] = stmtToProto(s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func operandToProto(o Operand) *pb.Operand {
|
||||
return &pb.Operand{Value: valToProto(o.Value)}
|
||||
}
|
||||
|
||||
func operandsToProto(os []Operand) []*pb.Operand {
|
||||
out := make([]*pb.Operand, len(os))
|
||||
for i, o := range os {
|
||||
out[i] = operandToProto(o)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func valToProto(v Val) *pb.Val {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
switch x := v.(type) {
|
||||
case Local:
|
||||
return &pb.Val{Kind: &pb.Val_Local{Local: toInt32(x)}}
|
||||
case StringIndex:
|
||||
return &pb.Val{Kind: &pb.Val_StringIndex{StringIndex: toInt32(x)}}
|
||||
case Bool:
|
||||
return &pb.Val{Kind: &pb.Val_Bool{Bool: bool(x)}}
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported Val type %T", v))
|
||||
}
|
||||
}
|
||||
|
||||
// toInt32 narrows an int-based value to int32, panicking if it would
|
||||
// overflow. PolicyToProto recovers from the panic and returns it as an
|
||||
// error, so callers don't need to check the bound themselves.
|
||||
func toInt32[T ~int](v T) int32 {
|
||||
if int64(v) > math.MaxInt32 || int64(v) < math.MinInt32 {
|
||||
panic(fmt.Sprintf("value %d overflows int32", int64(v)))
|
||||
}
|
||||
return int32(v)
|
||||
}
|
||||
|
||||
func localsToInt32s(ls []Local) []int32 {
|
||||
out := make([]int32, len(ls))
|
||||
for i, l := range ls {
|
||||
out[i] = toInt32(l)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func intsToInt32s(is []int) []int32 {
|
||||
out := make([]int32, len(is))
|
||||
for i, v := range is {
|
||||
out[i] = toInt32(v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func stmtToProto(s Stmt) *pb.Stmt {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
loc := s.GetLocation()
|
||||
out := &pb.Stmt{
|
||||
File: proto.Int32(toInt32(loc.File)),
|
||||
Col: proto.Int32(toInt32(loc.Col)),
|
||||
Row: proto.Int32(toInt32(loc.Row)),
|
||||
}
|
||||
switch x := s.(type) {
|
||||
case *ArrayAppendStmt:
|
||||
out.Kind = &pb.Stmt_ArrayAppendStmt{ArrayAppendStmt: &pb.ArrayAppendStmt{
|
||||
Value: operandToProto(x.Value),
|
||||
Array: proto.Int32(toInt32(x.Array)),
|
||||
}}
|
||||
case *AssignIntStmt:
|
||||
out.Kind = &pb.Stmt_AssignIntStmt{AssignIntStmt: &pb.AssignIntStmt{
|
||||
Value: proto.Int64(x.Value),
|
||||
Target: proto.Int32(toInt32(x.Target)),
|
||||
}}
|
||||
case *AssignVarOnceStmt:
|
||||
out.Kind = &pb.Stmt_AssignVarOnceStmt{AssignVarOnceStmt: &pb.AssignVarOnceStmt{
|
||||
Source: operandToProto(x.Source),
|
||||
Target: proto.Int32(toInt32(x.Target)),
|
||||
}}
|
||||
case *AssignVarStmt:
|
||||
out.Kind = &pb.Stmt_AssignVarStmt{AssignVarStmt: &pb.AssignVarStmt{
|
||||
Source: operandToProto(x.Source),
|
||||
Target: proto.Int32(toInt32(x.Target)),
|
||||
}}
|
||||
case *BlockStmt:
|
||||
body := &pb.BlockStmt{Blocks: make([]*pb.Block, len(x.Blocks))}
|
||||
for i, b := range x.Blocks {
|
||||
body.Blocks[i] = blockToProto(b)
|
||||
}
|
||||
out.Kind = &pb.Stmt_BlockStmt{BlockStmt: body}
|
||||
case *BreakStmt:
|
||||
out.Kind = &pb.Stmt_BreakStmt{BreakStmt: &pb.BreakStmt{Index: proto.Uint32(x.Index)}}
|
||||
case *CallDynamicStmt:
|
||||
out.Kind = &pb.Stmt_CallDynamicStmt{CallDynamicStmt: &pb.CallDynamicStmt{
|
||||
Args: localsToInt32s(x.Args),
|
||||
Result: proto.Int32(toInt32(x.Result)),
|
||||
Path: operandsToProto(x.Path),
|
||||
}}
|
||||
case *CallStmt:
|
||||
out.Kind = &pb.Stmt_CallStmt{CallStmt: &pb.CallStmt{
|
||||
Function: proto.String(x.Func),
|
||||
Args: operandsToProto(x.Args),
|
||||
Result: proto.Int32(toInt32(x.Result)),
|
||||
}}
|
||||
case *DotStmt:
|
||||
out.Kind = &pb.Stmt_DotStmt{DotStmt: &pb.DotStmt{
|
||||
Source: operandToProto(x.Source),
|
||||
Key: operandToProto(x.Key),
|
||||
Target: proto.Int32(toInt32(x.Target)),
|
||||
}}
|
||||
case *EqualStmt:
|
||||
out.Kind = &pb.Stmt_EqualStmt{EqualStmt: &pb.EqualStmt{
|
||||
A: operandToProto(x.A),
|
||||
B: operandToProto(x.B),
|
||||
}}
|
||||
case *IsArrayStmt:
|
||||
out.Kind = &pb.Stmt_IsArrayStmt{IsArrayStmt: &pb.IsArrayStmt{Source: operandToProto(x.Source)}}
|
||||
case *IsDefinedStmt:
|
||||
out.Kind = &pb.Stmt_IsDefinedStmt{IsDefinedStmt: &pb.IsDefinedStmt{Source: proto.Int32(toInt32(x.Source))}}
|
||||
case *IsObjectStmt:
|
||||
out.Kind = &pb.Stmt_IsObjectStmt{IsObjectStmt: &pb.IsObjectStmt{Source: operandToProto(x.Source)}}
|
||||
case *IsSetStmt:
|
||||
out.Kind = &pb.Stmt_IsSetStmt{IsSetStmt: &pb.IsSetStmt{Source: operandToProto(x.Source)}}
|
||||
case *IsUndefinedStmt:
|
||||
out.Kind = &pb.Stmt_IsUndefinedStmt{IsUndefinedStmt: &pb.IsUndefinedStmt{Source: proto.Int32(toInt32(x.Source))}}
|
||||
case *LenStmt:
|
||||
out.Kind = &pb.Stmt_LenStmt{LenStmt: &pb.LenStmt{
|
||||
Source: operandToProto(x.Source),
|
||||
Target: proto.Int32(toInt32(x.Target)),
|
||||
}}
|
||||
case *MakeArrayStmt:
|
||||
out.Kind = &pb.Stmt_MakeArrayStmt{MakeArrayStmt: &pb.MakeArrayStmt{
|
||||
Capacity: proto.Int32(x.Capacity),
|
||||
Target: proto.Int32(toInt32(x.Target)),
|
||||
}}
|
||||
case *MakeNullStmt:
|
||||
out.Kind = &pb.Stmt_MakeNullStmt{MakeNullStmt: &pb.MakeNullStmt{Target: proto.Int32(toInt32(x.Target))}}
|
||||
case *MakeNumberIntStmt:
|
||||
out.Kind = &pb.Stmt_MakeNumberIntStmt{MakeNumberIntStmt: &pb.MakeNumberIntStmt{
|
||||
Value: proto.Int64(x.Value),
|
||||
Target: proto.Int32(toInt32(x.Target)),
|
||||
}}
|
||||
case *MakeNumberRefStmt:
|
||||
out.Kind = &pb.Stmt_MakeNumberRefStmt{MakeNumberRefStmt: &pb.MakeNumberRefStmt{
|
||||
Index: proto.Int32(toInt32(x.Index)),
|
||||
Target: proto.Int32(toInt32(x.Target)),
|
||||
}}
|
||||
case *MakeObjectStmt:
|
||||
out.Kind = &pb.Stmt_MakeObjectStmt{MakeObjectStmt: &pb.MakeObjectStmt{Target: proto.Int32(toInt32(x.Target))}}
|
||||
case *MakeSetStmt:
|
||||
out.Kind = &pb.Stmt_MakeSetStmt{MakeSetStmt: &pb.MakeSetStmt{Target: proto.Int32(toInt32(x.Target))}}
|
||||
case *NopStmt:
|
||||
out.Kind = &pb.Stmt_NopStmt{NopStmt: &pb.NopStmt{}}
|
||||
case *NotEqualStmt:
|
||||
out.Kind = &pb.Stmt_NotEqualStmt{NotEqualStmt: &pb.NotEqualStmt{
|
||||
A: operandToProto(x.A),
|
||||
B: operandToProto(x.B),
|
||||
}}
|
||||
case *NotStmt:
|
||||
out.Kind = &pb.Stmt_NotStmt{NotStmt: &pb.NotStmt{Block: blockToProto(x.Block)}}
|
||||
case *ObjectInsertOnceStmt:
|
||||
out.Kind = &pb.Stmt_ObjectInsertOnceStmt{ObjectInsertOnceStmt: &pb.ObjectInsertOnceStmt{
|
||||
Key: operandToProto(x.Key),
|
||||
Value: operandToProto(x.Value),
|
||||
Object: proto.Int32(toInt32(x.Object)),
|
||||
}}
|
||||
case *ObjectInsertStmt:
|
||||
out.Kind = &pb.Stmt_ObjectInsertStmt{ObjectInsertStmt: &pb.ObjectInsertStmt{
|
||||
Key: operandToProto(x.Key),
|
||||
Value: operandToProto(x.Value),
|
||||
Object: proto.Int32(toInt32(x.Object)),
|
||||
}}
|
||||
case *ObjectMergeStmt:
|
||||
out.Kind = &pb.Stmt_ObjectMergeStmt{ObjectMergeStmt: &pb.ObjectMergeStmt{
|
||||
A: proto.Int32(toInt32(x.A)),
|
||||
B: proto.Int32(toInt32(x.B)),
|
||||
Target: proto.Int32(toInt32(x.Target)),
|
||||
}}
|
||||
case *ResetLocalStmt:
|
||||
out.Kind = &pb.Stmt_ResetLocalStmt{ResetLocalStmt: &pb.ResetLocalStmt{Target: proto.Int32(toInt32(x.Target))}}
|
||||
case *ResultSetAddStmt:
|
||||
out.Kind = &pb.Stmt_ResultSetAddStmt{ResultSetAddStmt: &pb.ResultSetAddStmt{Value: proto.Int32(toInt32(x.Value))}}
|
||||
case *ReturnLocalStmt:
|
||||
out.Kind = &pb.Stmt_ReturnLocalStmt{ReturnLocalStmt: &pb.ReturnLocalStmt{Source: proto.Int32(toInt32(x.Source))}}
|
||||
case *ScanStmt:
|
||||
out.Kind = &pb.Stmt_ScanStmt{ScanStmt: &pb.ScanStmt{
|
||||
Source: proto.Int32(toInt32(x.Source)),
|
||||
Key: proto.Int32(toInt32(x.Key)),
|
||||
Value: proto.Int32(toInt32(x.Value)),
|
||||
Block: blockToProto(x.Block),
|
||||
}}
|
||||
case *SetAddStmt:
|
||||
out.Kind = &pb.Stmt_SetAddStmt{SetAddStmt: &pb.SetAddStmt{
|
||||
Value: operandToProto(x.Value),
|
||||
Set: proto.Int32(toInt32(x.Set)),
|
||||
}}
|
||||
case *WithStmt:
|
||||
out.Kind = &pb.Stmt_WithStmt{WithStmt: &pb.WithStmt{
|
||||
Local: proto.Int32(toInt32(x.Local)),
|
||||
Path: intsToInt32s(x.Path),
|
||||
Value: operandToProto(x.Value),
|
||||
Block: blockToProto(x.Block),
|
||||
}}
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported Stmt type %T", s))
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// Copyright 2026 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ir_test
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/open-policy-agent/opa/v1/ir"
|
||||
pb "github.com/open-policy-agent/opa/v1/ir/v1pb"
|
||||
)
|
||||
|
||||
func TestPolicyToProtoRoundTrip(t *testing.T) {
|
||||
loc := ir.Location{File: 1, Row: 2, Col: 3}
|
||||
withLoc := func(s ir.Stmt) ir.Stmt {
|
||||
s.SetLocation(loc.File, loc.Row, loc.Col, "", "")
|
||||
return s
|
||||
}
|
||||
|
||||
p := &ir.Policy{
|
||||
Static: &ir.Static{
|
||||
Strings: []*ir.StringConst{{Value: "hello"}, {Value: "world"}},
|
||||
BuiltinFuncs: []*ir.BuiltinFunc{{Name: "eq"}},
|
||||
Files: []*ir.StringConst{{Value: "a.rego"}},
|
||||
},
|
||||
Plans: &ir.Plans{Plans: []*ir.Plan{{
|
||||
Name: "p",
|
||||
Blocks: []*ir.Block{{Stmts: []ir.Stmt{
|
||||
withLoc(&ir.ReturnLocalStmt{Source: ir.Local(1)}),
|
||||
withLoc(&ir.CallStmt{Func: "f", Args: []ir.Operand{{Value: ir.Local(2)}}, Result: ir.Local(3)}),
|
||||
withLoc(&ir.CallDynamicStmt{Args: []ir.Local{1, 2}, Result: 3, Path: []ir.Operand{{Value: ir.StringIndex(0)}}}),
|
||||
withLoc(&ir.BlockStmt{Blocks: []*ir.Block{{Stmts: []ir.Stmt{withLoc(&ir.NopStmt{})}}}}),
|
||||
withLoc(&ir.BreakStmt{Index: 1}),
|
||||
withLoc(&ir.DotStmt{Source: ir.Operand{Value: ir.Local(1)}, Key: ir.Operand{Value: ir.StringIndex(0)}, Target: ir.Local(2)}),
|
||||
withLoc(&ir.LenStmt{Source: ir.Operand{Value: ir.Local(1)}, Target: ir.Local(2)}),
|
||||
withLoc(&ir.ScanStmt{Source: 1, Key: 2, Value: 3, Block: &ir.Block{Stmts: []ir.Stmt{withLoc(&ir.NopStmt{})}}}),
|
||||
withLoc(&ir.NotStmt{Block: &ir.Block{Stmts: []ir.Stmt{withLoc(&ir.NopStmt{})}}}),
|
||||
withLoc(&ir.AssignIntStmt{Value: 42, Target: ir.Local(1)}),
|
||||
withLoc(&ir.AssignVarStmt{Source: ir.Operand{Value: ir.Local(1)}, Target: ir.Local(2)}),
|
||||
withLoc(&ir.AssignVarOnceStmt{Source: ir.Operand{Value: ir.Local(1)}, Target: ir.Local(2)}),
|
||||
withLoc(&ir.ResetLocalStmt{Target: ir.Local(1)}),
|
||||
withLoc(&ir.MakeNullStmt{Target: ir.Local(1)}),
|
||||
withLoc(&ir.MakeNumberIntStmt{Value: 7, Target: ir.Local(1)}),
|
||||
withLoc(&ir.MakeNumberRefStmt{Index: 0, Target: ir.Local(1)}),
|
||||
withLoc(&ir.MakeArrayStmt{Capacity: 4, Target: ir.Local(1)}),
|
||||
withLoc(&ir.MakeObjectStmt{Target: ir.Local(1)}),
|
||||
withLoc(&ir.MakeSetStmt{Target: ir.Local(1)}),
|
||||
withLoc(&ir.EqualStmt{A: ir.Operand{Value: ir.Local(1)}, B: ir.Operand{Value: ir.Bool(true)}}),
|
||||
withLoc(&ir.NotEqualStmt{A: ir.Operand{Value: ir.Local(1)}, B: ir.Operand{Value: ir.Local(2)}}),
|
||||
withLoc(&ir.IsArrayStmt{Source: ir.Operand{Value: ir.Local(1)}}),
|
||||
withLoc(&ir.IsObjectStmt{Source: ir.Operand{Value: ir.Local(1)}}),
|
||||
withLoc(&ir.IsSetStmt{Source: ir.Operand{Value: ir.Local(1)}}),
|
||||
withLoc(&ir.IsDefinedStmt{Source: ir.Local(1)}),
|
||||
withLoc(&ir.IsUndefinedStmt{Source: ir.Local(1)}),
|
||||
withLoc(&ir.ArrayAppendStmt{Value: ir.Operand{Value: ir.Local(1)}, Array: ir.Local(2)}),
|
||||
withLoc(&ir.ObjectInsertStmt{Key: ir.Operand{Value: ir.StringIndex(0)}, Value: ir.Operand{Value: ir.Local(1)}, Object: ir.Local(2)}),
|
||||
withLoc(&ir.ObjectInsertOnceStmt{Key: ir.Operand{Value: ir.StringIndex(0)}, Value: ir.Operand{Value: ir.Local(1)}, Object: ir.Local(2)}),
|
||||
withLoc(&ir.ObjectMergeStmt{A: 1, B: 2, Target: 3}),
|
||||
withLoc(&ir.SetAddStmt{Value: ir.Operand{Value: ir.Local(1)}, Set: ir.Local(2)}),
|
||||
withLoc(&ir.WithStmt{Local: 1, Path: []int{0, 1}, Value: ir.Operand{Value: ir.Local(2)}, Block: &ir.Block{Stmts: []ir.Stmt{withLoc(&ir.NopStmt{})}}}),
|
||||
withLoc(&ir.ResultSetAddStmt{Value: ir.Local(1)}),
|
||||
}}},
|
||||
}}},
|
||||
Funcs: &ir.Funcs{Funcs: []*ir.Func{{
|
||||
Name: "g",
|
||||
Params: []ir.Local{1, 2},
|
||||
Return: ir.Local(3),
|
||||
Path: []string{"a", "b"},
|
||||
Blocks: []*ir.Block{{Stmts: []ir.Stmt{withLoc(&ir.NopStmt{})}}},
|
||||
}}},
|
||||
}
|
||||
|
||||
pbPolicy, err := ir.PolicyToProto(p)
|
||||
if err != nil {
|
||||
t.Fatalf("PolicyToProto: %v", err)
|
||||
}
|
||||
bs, err := proto.Marshal(pbPolicy)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
|
||||
decoded := &pb.Policy{}
|
||||
if err := proto.Unmarshal(bs, decoded); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
|
||||
bs2, err := proto.Marshal(decoded)
|
||||
if err != nil {
|
||||
t.Fatalf("re-marshal: %v", err)
|
||||
}
|
||||
if !proto.Equal(pbPolicy, decoded) {
|
||||
t.Fatal("decoded proto differs from original")
|
||||
}
|
||||
if len(bs) != len(bs2) {
|
||||
t.Fatalf("re-encoded byte length differs: %d vs %d", len(bs), len(bs2))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyToProtoNil(t *testing.T) {
|
||||
got, err := ir.PolicyToProto(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Fatalf("expected nil for nil input, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyToProtoEmitsLocationTripleAlways(t *testing.T) {
|
||||
p := &ir.Policy{
|
||||
Plans: &ir.Plans{Plans: []*ir.Plan{{
|
||||
Name: "p",
|
||||
Blocks: []*ir.Block{{Stmts: []ir.Stmt{
|
||||
&ir.NopStmt{}, // no SetLocation call
|
||||
}}},
|
||||
}}},
|
||||
}
|
||||
pbPolicy, err := ir.PolicyToProto(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stmt := pbPolicy.Plans.Plans[0].Blocks[0].Stmts[0]
|
||||
if stmt.File == nil || *stmt.File != 0 {
|
||||
t.Errorf("File: want 0 (always present), got %v", stmt.File)
|
||||
}
|
||||
if stmt.Row == nil || *stmt.Row != 0 {
|
||||
t.Errorf("Row: want 0 (always present), got %v", stmt.Row)
|
||||
}
|
||||
if stmt.Col == nil || *stmt.Col != 0 {
|
||||
t.Errorf("Col: want 0 (always present), got %v", stmt.Col)
|
||||
}
|
||||
}
|
||||
|
||||
// unsupportedStmt is a Stmt kind the encoder switch doesn't handle.
|
||||
type unsupportedStmt struct{ ir.Location }
|
||||
|
||||
func TestPolicyToProtoUnknownStmtReturnsError(t *testing.T) {
|
||||
p := &ir.Policy{
|
||||
Plans: &ir.Plans{Plans: []*ir.Plan{{
|
||||
Name: "p",
|
||||
Blocks: []*ir.Block{{Stmts: []ir.Stmt{
|
||||
&unsupportedStmt{},
|
||||
}}},
|
||||
}}},
|
||||
}
|
||||
|
||||
got, err := ir.PolicyToProto(p)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unknown Stmt type, got nil")
|
||||
}
|
||||
if got != nil {
|
||||
t.Fatalf("expected nil result on error, got %v", got)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unsupportedStmt") {
|
||||
t.Fatalf("error should name the offending type: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyToProtoInt32OverflowReturnsError(t *testing.T) {
|
||||
p := &ir.Policy{
|
||||
Plans: &ir.Plans{Plans: []*ir.Plan{{
|
||||
Name: "p",
|
||||
Blocks: []*ir.Block{{Stmts: []ir.Stmt{
|
||||
&ir.ReturnLocalStmt{Source: ir.Local(math.MaxInt32 + 1)},
|
||||
}}},
|
||||
}}},
|
||||
}
|
||||
|
||||
got, err := ir.PolicyToProto(p)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for int32 overflow, got nil")
|
||||
}
|
||||
if got != nil {
|
||||
t.Fatalf("expected nil result on error, got %v", got)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "overflows int32") {
|
||||
t.Fatalf("error should mention the overflow: %v", err)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user