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:
Torin Sandall
2019-02-14 12:22:46 -08:00
parent 7df209106f
commit 7a4e82194a
5 changed files with 550 additions and 66 deletions
+101 -15
View File
@@ -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
}
+87
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+160 -3
View File
@@ -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)