diff --git a/bundle/bundle.go b/bundle/bundle.go index 4b08501dd7..652eaaa24c 100644 --- a/bundle/bundle.go +++ b/bundle/bundle.go @@ -23,6 +23,7 @@ import ( "github.com/open-policy-agent/opa/format" "github.com/open-policy-agent/opa/internal/file/archive" "github.com/open-policy-agent/opa/internal/merge" + "github.com/open-policy-agent/opa/loader/extension" "github.com/open-policy-agent/opa/metrics" "github.com/open-policy-agent/opa/util" ) @@ -600,10 +601,15 @@ func (r *Reader) Read() (Bundle, error) { continue } + var err error var value interface{} r.metrics.Timer(metrics.RegoDataParse).Start() - err := util.NewJSONDecoder(&buf).Decode(&value) + if handler := extension.FindExtension(".json"); handler != nil { + value, err = handler(buf.Bytes()) + } else { + err = util.NewJSONDecoder(&buf).Decode(&value) + } r.metrics.Timer(metrics.RegoDataParse).Stop() if err != nil { diff --git a/loader/extension/extension.go b/loader/extension/extension.go new file mode 100644 index 0000000000..407ad96386 --- /dev/null +++ b/loader/extension/extension.go @@ -0,0 +1,46 @@ +// Copyright 2023 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 extension + +import ( + "sync" +) + +var pluginMtx sync.Mutex +var bundleExtensions map[string]Handler + +// Handler is used to unmarshal a byte slice of a registered extension +// EXPERIMENTAL: Please don't rely on this functionality, it may go +// away or change in the future. +type Handler func([]byte) (any, error) + +// RegisterExtension registers a Handler for a certain file extension, including +// the dot: ".json", not "json". +// EXPERIMENTAL: Please don't rely on this functionality, it may go +// away or change in the future. +func RegisterExtension(name string, handler Handler) { + pluginMtx.Lock() + defer pluginMtx.Unlock() + + if bundleExtensions == nil { + bundleExtensions = map[string]Handler{} + } + bundleExtensions[name] = handler +} + +// FindExtension ios used to look up a registered extension Handler +// EXPERIMENTAL: Please don't rely on this functionality, it may go +// away or change in the future. +func FindExtension(ext string) Handler { + pluginMtx.Lock() + defer pluginMtx.Unlock() + + for e, handler := range bundleExtensions { + if e == ext { + return handler + } + } + return nil +} diff --git a/loader/extension/extension_test.go b/loader/extension/extension_test.go new file mode 100644 index 0000000000..2f92232405 --- /dev/null +++ b/loader/extension/extension_test.go @@ -0,0 +1,55 @@ +// Copyright 2023 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 extension_test + +import ( + "crypto/rand" + "fmt" + "reflect" + "testing" + "testing/fstest" + + "github.com/open-policy-agent/opa/loader" + "github.com/open-policy-agent/opa/loader/extension" + "github.com/open-policy-agent/opa/util" +) + +func TestLoaderExtensionUnmarshal(t *testing.T) { + sentinelErr := fmt.Errorf("test handler called") + extension.RegisterExtension(".json", func([]byte) (any, error) { + return nil, sentinelErr + }) + defer extension.RegisterExtension(".json", nil) + + bs := make([]byte, 128) + _, err := rand.Read(bs) + if err != nil { + t.Fatal(err) + } + var v any + if err := util.Unmarshal(bs, &v); err != sentinelErr { + t.Error(err) + } +} + +func TestLoaderExtensionBundle(t *testing.T) { + data := map[string]any{"foo": "bar"} + extension.RegisterExtension(".json", func([]byte) (any, error) { + return data, nil + }) + defer extension.RegisterExtension(".json", nil) + + fs := fstest.MapFS{ + "data.json": {}, + } + ldr := loader.NewFileLoader().WithFS(fs) + res, err := ldr.All([]string{"."}) + if err != nil { + t.Error(err) + } + if exp, act := data, res.Documents; !reflect.DeepEqual(exp, act) { + t.Errorf("expected %v, got %v", exp, act) + } +} diff --git a/loader/loader.go b/loader/loader.go index d4d05943c2..c95a0df8a3 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -20,6 +20,7 @@ import ( "github.com/open-policy-agent/opa/bundle" fileurl "github.com/open-policy-agent/opa/internal/file/url" "github.com/open-policy-agent/opa/internal/merge" + "github.com/open-policy-agent/opa/loader/extension" "github.com/open-policy-agent/opa/loader/filter" "github.com/open-policy-agent/opa/metrics" "github.com/open-policy-agent/opa/storage" @@ -731,11 +732,16 @@ func loadRego(path string, bs []byte, m metrics.Metrics, opts ast.ParserOptions) func loadJSON(path string, bs []byte, m metrics.Metrics) (interface{}, error) { m.Timer(metrics.RegoDataParse).Start() - buf := bytes.NewBuffer(bs) - decoder := util.NewJSONDecoder(buf) + var err error var x interface{} - err := decoder.Decode(&x) + if handler := extension.FindExtension(".json"); handler != nil { + x, err = handler(bs) + } else { + buf := bytes.NewBuffer(bs) + err = util.NewJSONDecoder(buf).Decode(&x) + } m.Timer(metrics.RegoDataParse).Stop() + if err != nil { return nil, fmt.Errorf("%s: %w", path, err) } diff --git a/util/json.go b/util/json.go index 283c496973..70ba660cf5 100644 --- a/util/json.go +++ b/util/json.go @@ -12,6 +12,8 @@ import ( "reflect" "github.com/ghodss/yaml" + + "github.com/open-policy-agent/opa/loader/extension" ) // UnmarshalJSON parses the JSON encoded data and stores the result in the value @@ -103,14 +105,25 @@ func Reference(x interface{}) *interface{} { return &x } -// Unmarshal decodes a YAML or JSON value into the specified type. +// Unmarshal decodes a YAML, JSON or JSON extension value into the specified type. func Unmarshal(bs []byte, v interface{}) error { if json.Valid(bs) { return UnmarshalJSON(bs, v) } - bs, err := yaml.YAMLToJSON(bs) - if err != nil { - return err + nbs, err := yaml.YAMLToJSON(bs) + if err == nil { + return UnmarshalJSON(nbs, v) } - return UnmarshalJSON(bs, v) + // not json or yaml: try extensions + if value, ok := v.(*any); ok { + if handler := extension.FindExtension(".json"); handler != nil { + retval, err := handler(bs) + if err != nil { + return err + } + *value = retval + return nil + } + } + return err }