From 7a4e82194ab8ece279c825a7e0d9b2d20f06abe8 Mon Sep 17 00:00:00 2001 From: Torin Sandall Date: Thu, 14 Feb 2019 12:22:46 -0800 Subject: [PATCH] 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 --- bundle/bundle.go | 116 +++++++++++++++++--- bundle/bundle_test.go | 87 +++++++++++++++ loader/loader_test.go | 58 +++++++--- plugins/bundle/plugin.go | 192 ++++++++++++++++++++++++++++------ plugins/bundle/plugin_test.go | 163 ++++++++++++++++++++++++++++- 5 files changed, 550 insertions(+), 66 deletions(-) diff --git a/bundle/bundle.go b/bundle/bundle.go index cb94714749..fe07d12ac9 100644 --- a/bundle/bundle.go +++ b/bundle/bundle.go @@ -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 +} diff --git a/bundle/bundle_test.go b/bundle/bundle_test.go index 06afbe2bd3..6aefcee258 100644 --- a/bundle/bundle_test.go +++ b/bundle/bundle_test.go @@ -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() diff --git a/loader/loader_test.go b/loader/loader_test.go index 3fe5e1590e..eac6b8b79f 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -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) } diff --git a/plugins/bundle/plugin.go b/plugins/bundle/plugin.go index 7207384f8a..a0679b2bd9 100644 --- a/plugins/bundle/plugin.go +++ b/plugins/bundle/plugin.go @@ -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 +} diff --git a/plugins/bundle/plugin_test.go b/plugins/bundle/plugin_test.go index 5877a2557b..694ff53ee5 100644 --- a/plugins/bundle/plugin_test.go +++ b/plugins/bundle/plugin_test.go @@ -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)