mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
bundle: Add support for scoping bundle to subset of data
Previously, when OPA activated a bundle, it would erase ALL existing policy and data that had been cached. This meant that the bundles and components like kube-mgmt were mutually exclusive (because the bundles would overwrite the other component's policy and data.) With these changes, bundles can include a set of roots that scope the bundle. When the bundle activates, only policy and data under those roots are erased and overwitten. Signed-off-by: Torin Sandall <torinsandall@gmail.com>
This commit is contained in:
+101
-15
@@ -43,7 +43,70 @@ type Bundle struct {
|
||||
// Manifest represents the manifest from a bundle. The manifest may contain
|
||||
// metadata such as the bundle revision.
|
||||
type Manifest struct {
|
||||
Revision string `json:"revision"`
|
||||
Revision string `json:"revision"`
|
||||
Roots *[]string `json:"roots,omitempty"`
|
||||
}
|
||||
|
||||
// Init initializes the manifest. If you instantiate a manifest
|
||||
// manually, call Init to ensure that the roots are set properly.
|
||||
func (m *Manifest) Init() {
|
||||
if m.Roots == nil {
|
||||
defaultRoots := []string{""}
|
||||
m.Roots = &defaultRoots
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manifest) validateAndInjectDefaults(b Bundle) error {
|
||||
|
||||
m.Init()
|
||||
|
||||
// Validate roots in bundle.
|
||||
roots := *m.Roots
|
||||
for i := range roots {
|
||||
roots[i] = strings.Trim(roots[i], "/")
|
||||
}
|
||||
|
||||
for i := 0; i < len(roots)-1; i++ {
|
||||
for j := i + 1; j < len(roots); j++ {
|
||||
if strings.HasPrefix(roots[i], roots[j]) || strings.HasPrefix(roots[j], roots[i]) {
|
||||
return fmt.Errorf("manifest has overlapped roots: %v and %v", roots[i], roots[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate modules in bundle.
|
||||
for _, module := range b.Modules {
|
||||
found := false
|
||||
if path, err := module.Parsed.Package.Path.Ptr(); err == nil {
|
||||
for i := range roots {
|
||||
if strings.HasPrefix(path, roots[i]) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("manifest roots do not permit '%v' in %v", module.Parsed.Package, module.Path)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate data in bundle.
|
||||
return dfs(b.Data, "", func(path string, node interface{}) (bool, error) {
|
||||
path = strings.Trim(path, "/")
|
||||
for i := range roots {
|
||||
if strings.HasPrefix(path, roots[i]) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
if _, ok := node.(map[string]interface{}); ok {
|
||||
for i := range roots {
|
||||
if strings.HasPrefix(roots[i], path) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, fmt.Errorf("manifest roots do not permit data at path %v", path)
|
||||
})
|
||||
}
|
||||
|
||||
// ModuleFile represents a single module contained a bundle.
|
||||
@@ -142,23 +205,28 @@ func (r *Reader) Read() (Bundle, error) {
|
||||
if err := util.NewJSONDecoder(&buf).Decode(&bundle.Manifest); err != nil {
|
||||
return bundle, errors.Wrap(err, "bundle load failed on manifest decode")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if r.includeManifestInData {
|
||||
var metadata map[string]interface{}
|
||||
b, err := json.Marshal(&bundle.Manifest)
|
||||
if err != nil {
|
||||
return bundle, errors.Wrap(err, "bundle load failed on manifest marshal")
|
||||
}
|
||||
if err := bundle.Manifest.validateAndInjectDefaults(bundle); err != nil {
|
||||
return bundle, err
|
||||
}
|
||||
|
||||
err = util.UnmarshalJSON(b, &metadata)
|
||||
if err != nil {
|
||||
return bundle, errors.Wrap(err, "bundle load failed on manifest unmarshal")
|
||||
}
|
||||
if r.includeManifestInData {
|
||||
var metadata map[string]interface{}
|
||||
|
||||
if err := bundle.insert(manifestPath, metadata); err != nil {
|
||||
return bundle, errors.Wrapf(err, "bundle load failed on %v", manifestPath)
|
||||
}
|
||||
}
|
||||
b, err := json.Marshal(&bundle.Manifest)
|
||||
if err != nil {
|
||||
return bundle, errors.Wrap(err, "bundle load failed on manifest marshal")
|
||||
}
|
||||
|
||||
err = util.UnmarshalJSON(b, &metadata)
|
||||
if err != nil {
|
||||
return bundle, errors.Wrap(err, "bundle load failed on manifest unmarshal")
|
||||
}
|
||||
|
||||
if err := bundle.insert(manifestPath, metadata); err != nil {
|
||||
return bundle, errors.Wrapf(err, "bundle load failed on %v", manifestPath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,3 +350,21 @@ func writeFile(tw *tar.Writer, path string, bs []byte) error {
|
||||
_, err := tw.Write(bs)
|
||||
return err
|
||||
}
|
||||
|
||||
func dfs(value interface{}, path string, fn func(string, interface{}) (bool, error)) error {
|
||||
if stop, err := fn(path, value); err != nil {
|
||||
return err
|
||||
} else if stop {
|
||||
return nil
|
||||
}
|
||||
obj, ok := value.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
for key := range obj {
|
||||
if err := dfs(obj[key], path+"/"+key, fn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
@@ -86,6 +87,92 @@ func TestReadWithManifestInData(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadRootValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
note string
|
||||
files [][2]string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
note: "default: full extent",
|
||||
files: [][2]string{
|
||||
{"/.manifest", `{"revision": "abcd"}`},
|
||||
{"/data.json", `{"a": 1}`},
|
||||
{"/x.rego", `package foo`},
|
||||
},
|
||||
err: "",
|
||||
},
|
||||
{
|
||||
note: "explicit: full extent",
|
||||
files: [][2]string{
|
||||
{"/.manifest", `{"revision": "abcd", "roots": [""]}`},
|
||||
{"/data.json", `{"a": 1}`},
|
||||
{"/x.rego", `package foo`},
|
||||
},
|
||||
err: "",
|
||||
},
|
||||
{
|
||||
note: "implicit: prefixed",
|
||||
files: [][2]string{
|
||||
{"/.manifest", `{"revision": "abcd", "roots": ["a/b", "foo"]}`},
|
||||
{"/data.json", `{"a": {"b": 1}}`},
|
||||
{"/x.rego", `package foo.bar`},
|
||||
},
|
||||
err: "",
|
||||
},
|
||||
{
|
||||
note: "err: empty",
|
||||
files: [][2]string{
|
||||
{"/.manifest", `{"revision": "abcd", "roots": []}`},
|
||||
{"/x.rego", `package foo`},
|
||||
},
|
||||
err: "manifest roots do not permit 'package foo' in /x.rego",
|
||||
},
|
||||
{
|
||||
note: "err: overlapped",
|
||||
files: [][2]string{
|
||||
{"/.manifest", `{"revision": "abcd", "roots": ["a/b", "a"]}`},
|
||||
},
|
||||
err: "manifest has overlapped roots: a/b and a",
|
||||
},
|
||||
{
|
||||
note: "err: package outside scope",
|
||||
files: [][2]string{
|
||||
{"/.manifest", `{"revision": "abcd", "roots": ["a", "b", "c/d"]}`},
|
||||
{"/a.rego", `package b.c`},
|
||||
{"/x.rego", `package c.e`},
|
||||
},
|
||||
err: "manifest roots do not permit 'package c.e' in /x.rego",
|
||||
},
|
||||
{
|
||||
note: "err: data outside scope",
|
||||
files: [][2]string{
|
||||
{"/.manifest", `{"revision": "abcd", "roots": ["a", "b", "c/d"]}`},
|
||||
{"/data.json", `{"a": 1}`},
|
||||
{"/c/e/data.json", `"bad bad bad"`},
|
||||
},
|
||||
err: "manifest roots do not permit data at path c/e",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
buf := writeTarGz(tc.files)
|
||||
_, err := NewReader(buf).IncludeManifestInData(true).Read()
|
||||
if tc.err == "" && err != nil {
|
||||
t.Fatal("Unexpected error occurred:", err)
|
||||
} else if tc.err != "" && err == nil {
|
||||
t.Fatal("Expected error but got success")
|
||||
} else if tc.err != "" && err != nil {
|
||||
if !strings.Contains(err.Error(), tc.err) {
|
||||
t.Fatalf("Expected error to contain %q but got: %v", tc.err, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestReadErrorBadGzip(t *testing.T) {
|
||||
buf := bytes.NewBufferString("bad gzip bytes")
|
||||
_, err := NewReader(buf).Read()
|
||||
|
||||
+42
-16
@@ -152,21 +152,6 @@ func TestLoadDirRecursive(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
var testBundle = bundle.Bundle{
|
||||
Modules: []bundle.ModuleFile{
|
||||
{
|
||||
Path: "x.rego",
|
||||
Raw: []byte(`
|
||||
package baz
|
||||
|
||||
p = 1`),
|
||||
},
|
||||
},
|
||||
Data: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
}
|
||||
|
||||
func TestLoadBundle(t *testing.T) {
|
||||
|
||||
test.WithTempFS(nil, func(rootDir string) {
|
||||
@@ -176,6 +161,25 @@ func TestLoadBundle(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var testBundle = bundle.Bundle{
|
||||
Modules: []bundle.ModuleFile{
|
||||
{
|
||||
Path: "x.rego",
|
||||
Raw: []byte(`
|
||||
package baz
|
||||
|
||||
p = 1`),
|
||||
},
|
||||
},
|
||||
Data: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
Manifest: bundle.Manifest{
|
||||
Revision: "",
|
||||
Roots: &[]string{""},
|
||||
},
|
||||
}
|
||||
|
||||
if err := bundle.Write(f, testBundle); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -187,7 +191,7 @@ func TestLoadBundle(t *testing.T) {
|
||||
}
|
||||
|
||||
actualData := testBundle.Data
|
||||
actualData["system"] = map[string]interface{}{"bundle": map[string]interface{}{"manifest": map[string]interface{}{"revision": ""}}}
|
||||
actualData["system"] = map[string]interface{}{"bundle": map[string]interface{}{"manifest": map[string]interface{}{"revision": "", "roots": []interface{}{""}}}}
|
||||
|
||||
if !reflect.DeepEqual(actualData, loaded.Documents) {
|
||||
t.Fatalf("Expected %v but got: %v", actualData, loaded.Documents)
|
||||
@@ -213,6 +217,25 @@ func TestLoadBundleSubDir(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var testBundle = bundle.Bundle{
|
||||
Modules: []bundle.ModuleFile{
|
||||
{
|
||||
Path: "x.rego",
|
||||
Raw: []byte(`
|
||||
package baz
|
||||
|
||||
p = 1`),
|
||||
},
|
||||
},
|
||||
Data: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
Manifest: bundle.Manifest{
|
||||
Revision: "",
|
||||
Roots: &[]string{""},
|
||||
},
|
||||
}
|
||||
|
||||
if err := bundle.Write(f, testBundle); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -223,6 +246,9 @@ func TestLoadBundleSubDir(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
actualData := testBundle.Data
|
||||
actualData["system"] = map[string]interface{}{"bundle": map[string]interface{}{"manifest": map[string]interface{}{"revision": "", "roots": []interface{}{""}}}}
|
||||
|
||||
if !reflect.DeepEqual(map[string]interface{}{"b": testBundle.Data}, loaded.Documents) {
|
||||
t.Fatalf("Expected %v but got: %v", testBundle.Data, loaded.Documents)
|
||||
}
|
||||
|
||||
+160
-32
@@ -7,6 +7,7 @@ package bundle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -184,8 +185,35 @@ func (p *Plugin) activate(ctx context.Context, b *bundle.Bundle) error {
|
||||
p.logDebug("Opened storage transaction (%v).", txn.ID())
|
||||
defer p.logDebug("Closing storage transaction (%v).", txn.ID())
|
||||
|
||||
// write data from bundle into store, overwritting contents
|
||||
if err := p.manager.Store.Write(ctx, txn, storage.AddOp, storage.Path{}, b.Data); err != nil {
|
||||
// Build set of roots from old and new bundles. This set of
|
||||
// roots should be erased.
|
||||
erase := map[string]struct{}{}
|
||||
|
||||
if b.Manifest.Roots != nil {
|
||||
for _, root := range *b.Manifest.Roots {
|
||||
erase[root] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if roots, err := p.readRoots(ctx, txn); err == nil {
|
||||
for _, root := range roots {
|
||||
erase[root] = struct{}{}
|
||||
}
|
||||
} else if !storage.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.eraseData(ctx, txn, erase); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.erasePolicies(ctx, txn, erase); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write data from new bundle into store. Only write under the
|
||||
// roots contained in the manifest.
|
||||
if err := p.writeData(ctx, txn, *b.Manifest.Roots, b.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -193,43 +221,123 @@ func (p *Plugin) activate(ctx context.Context, b *bundle.Bundle) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// load existing policy ids from store and delete
|
||||
ids, err := p.manager.Store.ListPolicies(ctx, txn)
|
||||
if err != nil {
|
||||
if err := p.writeModules(ctx, txn, b.Modules); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
if err := p.manager.Store.DeletePolicy(ctx, txn, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// ensure that policies compile.
|
||||
modules := map[string]*ast.Module{}
|
||||
|
||||
for _, file := range b.Modules {
|
||||
modules[file.Path] = file.Parsed
|
||||
}
|
||||
|
||||
compiler := ast.NewCompiler().
|
||||
WithPathConflictsCheck(storage.NonEmpty(ctx, p.manager.Store, txn))
|
||||
|
||||
if compiler.Compile(modules); compiler.Failed() {
|
||||
return compiler.Errors
|
||||
}
|
||||
|
||||
// write policies from bundle into store.
|
||||
for _, file := range b.Modules {
|
||||
if err := p.manager.Store.UpsertPolicy(ctx, txn, file.Path, file.Raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Plugin) eraseData(ctx context.Context, txn storage.Transaction, roots map[string]struct{}) error {
|
||||
for root := range roots {
|
||||
path, ok := storage.ParsePathEscaped("/" + root)
|
||||
if !ok {
|
||||
return fmt.Errorf("manifest root path invalid: %v", root)
|
||||
}
|
||||
if len(path) > 0 {
|
||||
if err := p.manager.Store.Write(ctx, txn, storage.RemoveOp, path, nil); err != nil {
|
||||
if !storage.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) erasePolicies(ctx context.Context, txn storage.Transaction, roots map[string]struct{}) error {
|
||||
ids, err := p.manager.Store.ListPolicies(ctx, txn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, id := range ids {
|
||||
bs, err := p.manager.Store.GetPolicy(ctx, txn, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
module, err := ast.ParseModule(id, string(bs))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path, err := module.Package.Path.Ptr()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for root := range roots {
|
||||
if strings.HasPrefix(path, root) {
|
||||
if err := p.manager.Store.DeletePolicy(ctx, txn, id); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) writeData(ctx context.Context, txn storage.Transaction, roots []string, data map[string]interface{}) error {
|
||||
for _, root := range roots {
|
||||
path, ok := storage.ParsePathEscaped("/" + root)
|
||||
if !ok {
|
||||
return fmt.Errorf("manifest root path invalid: %v", root)
|
||||
}
|
||||
if value, ok := lookup(path, data); ok {
|
||||
if len(path) > 0 {
|
||||
if err := storage.MakeDir(ctx, p.manager.Store, txn, path[:len(path)-1]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := p.manager.Store.Write(ctx, txn, storage.AddOp, path, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) writeModules(ctx context.Context, txn storage.Transaction, files []bundle.ModuleFile) error {
|
||||
modules := map[string]*ast.Module{}
|
||||
for _, file := range files {
|
||||
modules[file.Path] = file.Parsed
|
||||
}
|
||||
compiler := ast.NewCompiler().
|
||||
WithPathConflictsCheck(storage.NonEmpty(ctx, p.manager.Store, txn))
|
||||
if compiler.Compile(modules); compiler.Failed() {
|
||||
return compiler.Errors
|
||||
}
|
||||
for _, file := range files {
|
||||
if err := p.manager.Store.UpsertPolicy(ctx, txn, file.Path, file.Raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) readRoots(ctx context.Context, txn storage.Transaction) ([]string, error) {
|
||||
|
||||
value, err := p.manager.Store.Read(ctx, txn, rootsPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sl, ok := value.([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("corrupt manifest roots")
|
||||
}
|
||||
|
||||
roots := make([]string, len(sl))
|
||||
|
||||
for i := range sl {
|
||||
roots[i], ok = sl[i].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("corrupt manifest root")
|
||||
}
|
||||
}
|
||||
|
||||
return roots, nil
|
||||
}
|
||||
|
||||
func (p *Plugin) writeManifest(ctx context.Context, txn storage.Transaction, m bundle.Manifest) error {
|
||||
|
||||
var value interface{} = m
|
||||
@@ -267,4 +375,24 @@ func (p *Plugin) logrusFields() logrus.Fields {
|
||||
var (
|
||||
bundlePath = storage.MustParsePath("/system/bundle")
|
||||
manifestPath = storage.MustParsePath("/system/bundle/manifest")
|
||||
rootsPath = storage.MustParsePath("/system/bundle/manifest/roots")
|
||||
)
|
||||
|
||||
func lookup(path storage.Path, data map[string]interface{}) (interface{}, bool) {
|
||||
if len(path) == 0 {
|
||||
return data, true
|
||||
}
|
||||
for i := 0; i < len(path)-1; i++ {
|
||||
value, ok := data[path[i]]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
obj, ok := value.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
data = obj
|
||||
}
|
||||
value, ok := data[path[len(path)-1]]
|
||||
return value, ok
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
@@ -40,6 +41,8 @@ func TestPluginOneShot(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
b.Manifest.Init()
|
||||
|
||||
plugin.oneShot(ctx, download.Update{Bundle: &b})
|
||||
|
||||
txn := storage.NewTransactionOrDie(ctx, manager.Store)
|
||||
@@ -61,7 +64,7 @@ func TestPluginOneShot(t *testing.T) {
|
||||
}
|
||||
|
||||
data, err := manager.Store.Read(ctx, txn, storage.Path{})
|
||||
expData := util.MustUnmarshalJSON([]byte(`{"foo": {"bar": 1, "baz": "qux"}, "system": {"bundle": {"manifest": {"revision": "quickbrownfaux"}}}}`))
|
||||
expData := util.MustUnmarshalJSON([]byte(`{"foo": {"bar": 1, "baz": "qux"}, "system": {"bundle": {"manifest": {"revision": "quickbrownfaux", "roots": [""]}}}}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !reflect.DeepEqual(data, expData) {
|
||||
@@ -75,17 +78,20 @@ func TestPluginOneShotCompileError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
manager := getTestManager()
|
||||
plugin := Plugin{manager: manager, status: &Status{}}
|
||||
raw1 := "package foo\n\np[x] { x = 1 }"
|
||||
|
||||
b1 := &bundle.Bundle{
|
||||
Data: map[string]interface{}{"a": "b"},
|
||||
Modules: []bundle.ModuleFile{
|
||||
{
|
||||
Path: "/example.rego",
|
||||
Parsed: ast.MustParseModule("package foo\n\np[x] { x = 1 }"),
|
||||
Raw: []byte(raw1),
|
||||
Parsed: ast.MustParseModule(raw1),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b1.Manifest.Init()
|
||||
plugin.oneShot(ctx, download.Update{Bundle: b1})
|
||||
|
||||
b2 := &bundle.Bundle{
|
||||
@@ -98,8 +104,8 @@ func TestPluginOneShotCompileError(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
b2.Manifest.Init()
|
||||
plugin.oneShot(ctx, download.Update{Bundle: b2})
|
||||
|
||||
txn := storage.NewTransactionOrDie(ctx, manager.Store)
|
||||
|
||||
_, err := manager.Store.GetPolicy(ctx, txn, "/example.rego")
|
||||
@@ -124,6 +130,7 @@ func TestPluginOneShotCompileError(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
b3.Manifest.Init()
|
||||
plugin.oneShot(ctx, download.Update{Bundle: b3})
|
||||
|
||||
txn = storage.NewTransactionOrDie(ctx, manager.Store)
|
||||
@@ -163,6 +170,7 @@ func TestPluginOneShotActivatationRemovesOld(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
b1.Manifest.Init()
|
||||
plugin.oneShot(ctx, download.Update{Bundle: &b1})
|
||||
|
||||
module2 := `package example
|
||||
@@ -182,6 +190,7 @@ func TestPluginOneShotActivatationRemovesOld(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
b2.Manifest.Init()
|
||||
plugin.oneShot(ctx, download.Update{Bundle: &b2})
|
||||
|
||||
err := storage.Txn(ctx, manager.Store, storage.TransactionParams{}, func(txn storage.Transaction) error {
|
||||
@@ -234,6 +243,8 @@ func TestPluginListener(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
b.Manifest.Init()
|
||||
|
||||
// Test that initial bundle is ok. Defer to separate goroutine so we can
|
||||
// check result with channel.
|
||||
go plugin.oneShot(ctx, download.Update{Bundle: &b})
|
||||
@@ -303,6 +314,8 @@ func TestPluginListenerErrorClearedOn304(t *testing.T) {
|
||||
Data: map[string]interface{}{"foo": "bar"},
|
||||
}
|
||||
|
||||
b.Manifest.Init()
|
||||
|
||||
// Test that initial bundle is ok.
|
||||
go plugin.oneShot(ctx, download.Update{Bundle: &b})
|
||||
s1 := <-ch
|
||||
@@ -328,6 +341,150 @@ func TestPluginListenerErrorClearedOn304(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginActivateScopedBundle(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
manager := getTestManager()
|
||||
plugin := Plugin{manager: manager, status: &Status{}}
|
||||
|
||||
// Transact test data and policies that represent data coming from
|
||||
// _outside_ the bundle. The test will verify that data _outside_
|
||||
// the bundle is both not erased and is overwritten appropriately.
|
||||
//
|
||||
// The test data claims a/{a1-6} where even paths are policy and
|
||||
// odd paths are raw JSON.
|
||||
if err := storage.Txn(ctx, manager.Store, storage.WriteParams, func(txn storage.Transaction) error {
|
||||
|
||||
externalData := map[string]interface{}{"a": map[string]interface{}{"a1": "x1", "a3": "x2", "a5": "x3"}}
|
||||
|
||||
if err := manager.Store.Write(ctx, txn, storage.AddOp, storage.Path{}, externalData); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := manager.Store.UpsertPolicy(ctx, txn, "some/id1", []byte(`package a.a2`)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := manager.Store.UpsertPolicy(ctx, txn, "some/id2", []byte(`package a.a4`)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := manager.Store.UpsertPolicy(ctx, txn, "some/id3", []byte(`package a.a6`)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Activate a bundle that is scoped to a/a1 and a/a2. This will
|
||||
// erase and overwrite the external data at these paths but leave
|
||||
// a3-6 untouched.
|
||||
module := "package a.a2\n\nbar=1"
|
||||
|
||||
b := bundle.Bundle{
|
||||
Manifest: bundle.Manifest{Revision: "quickbrownfaux", Roots: &[]string{"a/a1", "a/a2"}},
|
||||
Data: map[string]interface{}{
|
||||
"a": map[string]interface{}{
|
||||
"a1": "foo",
|
||||
},
|
||||
},
|
||||
Modules: []bundle.ModuleFile{
|
||||
bundle.ModuleFile{
|
||||
Path: "bundle/id1",
|
||||
Parsed: ast.MustParseModule(module),
|
||||
Raw: []byte(module),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b.Manifest.Init()
|
||||
|
||||
plugin.oneShot(ctx, download.Update{Bundle: &b})
|
||||
|
||||
// Ensure a/a3-6 are intact. a1-2 are overwritten by bundle.
|
||||
if err := storage.Txn(ctx, manager.Store, storage.TransactionParams{}, func(txn storage.Transaction) error {
|
||||
value, err := manager.Store.Read(ctx, txn, storage.Path{"a"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expData := util.MustUnmarshalJSON([]byte(`{"a1": "foo", "a3": "x2", "a5": "x3"}`))
|
||||
|
||||
if !reflect.DeepEqual(value, expData) {
|
||||
return fmt.Errorf("Expected %v but got %v", expData, value)
|
||||
}
|
||||
|
||||
ids, err := manager.Store.ListPolicies(ctx, txn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expIds := []string{"bundle/id1", "some/id2", "some/id3"}
|
||||
sort.Strings(ids)
|
||||
|
||||
if !reflect.DeepEqual(ids, expIds) {
|
||||
return fmt.Errorf("Expected ids %v but got %v", expIds, ids)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Activate a bundle that is scoped to a/a3 ad a/a6.
|
||||
module = "package a.a4\n\nbar=1"
|
||||
|
||||
b = bundle.Bundle{
|
||||
Manifest: bundle.Manifest{Revision: "quickbrownfaux", Roots: &[]string{"a/a3", "a/a4"}},
|
||||
Data: map[string]interface{}{
|
||||
"a": map[string]interface{}{
|
||||
"a3": "foo",
|
||||
},
|
||||
},
|
||||
Modules: []bundle.ModuleFile{
|
||||
bundle.ModuleFile{
|
||||
Path: "bundle/id2",
|
||||
Parsed: ast.MustParseModule(module),
|
||||
Raw: []byte(module),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b.Manifest.Init()
|
||||
plugin.oneShot(ctx, download.Update{Bundle: &b})
|
||||
|
||||
// Ensure a/a5-a6 are intact. a3 and a4 are overwritten by bundle.
|
||||
if err := storage.Txn(ctx, manager.Store, storage.TransactionParams{}, func(txn storage.Transaction) error {
|
||||
value, err := manager.Store.Read(ctx, txn, storage.Path{"a"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expData := util.MustUnmarshalJSON([]byte(`{"a3": "foo", "a5": "x3"}`))
|
||||
|
||||
if !reflect.DeepEqual(value, expData) {
|
||||
return fmt.Errorf("Expected %v but got %v", expData, value)
|
||||
}
|
||||
|
||||
ids, err := manager.Store.ListPolicies(ctx, txn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expIds := []string{"bundle/id2", "some/id3"}
|
||||
sort.Strings(ids)
|
||||
|
||||
if !reflect.DeepEqual(ids, expIds) {
|
||||
return fmt.Errorf("Expected ids %v but got %v", expIds, ids)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func getTestManager() *plugins.Manager {
|
||||
store := inmem.New()
|
||||
manager, err := plugins.New(nil, "test-instance-id", store)
|
||||
|
||||
Reference in New Issue
Block a user