From ece99073d4d03493ab23e85468e9947c33994fd2 Mon Sep 17 00:00:00 2001 From: Jarek Kowalski Date: Sat, 7 Dec 2019 12:21:21 -0800 Subject: [PATCH] policy: added sparse policy tree abstraction for quickly retrieving effective policies at runtime when walking a tree --- snapshot/policy/policy_tree.go | 122 +++++++++++++++++++++++++++ snapshot/policy/policy_tree_test.go | 124 ++++++++++++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 snapshot/policy/policy_tree.go create mode 100644 snapshot/policy/policy_tree_test.go diff --git a/snapshot/policy/policy_tree.go b/snapshot/policy/policy_tree.go new file mode 100644 index 000000000..dd82b0d2a --- /dev/null +++ b/snapshot/policy/policy_tree.go @@ -0,0 +1,122 @@ +package policy + +import "strings" + +// DefaultPolicy is a default policy returned by policy tree in absence of other policies. +var DefaultPolicy = &Policy{ + FilesPolicy: defaultFilesPolicy, +} + +// Tree represents a node in the policy tree, where a policy can be +// defined. A nil tree is a valid tree with default policy. +type Tree struct { + effective *Policy + inherited bool + children map[string]*Tree +} + +// DefinedPolicy returns policy that's been explicitly defined for tree node or nil if no policy was defined. +func (t *Tree) DefinedPolicy() *Policy { + if t == nil || t.inherited { + return nil + } + + return t.effective +} + +// EffectivePolicy returns policy that's been defined for this tree node or inherited from its parent. +func (t *Tree) EffectivePolicy() *Policy { + if t == nil { + return DefaultPolicy + } + + return t.effective +} + +func (t *Tree) IsInherited() bool { + if t == nil { + return true + } + + return t.inherited +} + +func (t *Tree) Child(name string) *Tree { + if t == nil { + return nil + } + + parts := strings.Split(name, "/") + switch len(parts) { + case 1: + if name == "." || name == "" { + return t + } + + ch := t.children[name] + if ch != nil { + return ch + } + + // tree with no children, we can just reuse current node + if len(t.children) == 0 && t.inherited { + return t + } + + return &Tree{effective: t.effective, inherited: true} + + default: + ch := t + for _, p := range parts { + ch = ch.Child(p) + } + + return ch + } +} + +// BuildTree builds a policy tree from the given map of paths to policies. +// Each path must be relative and start with "." and be separated by slashes. +func BuildTree(defined map[string]*Policy, defaultPolicy *Policy) *Tree { + return buildTreeNode(defined, ".", defaultPolicy) +} + +func buildTreeNode(defined map[string]*Policy, path string, defaultPolicy *Policy) *Tree { + n := &Tree{ + effective: defined[path], + } + if n.effective == nil { + n.effective = defaultPolicy + n.inherited = true + } + + children := childrenWithPrefix(defined, path+"/") + if len(children) > 0 { + n.children = map[string]*Tree{} + + for childName, descendants := range children { + n.children[childName] = buildTreeNode(descendants, path+"/"+childName, n.effective) + } + } + + return n +} + +func childrenWithPrefix(m map[string]*Policy, path string) map[string]map[string]*Policy { + result := map[string]map[string]*Policy{} + + for k, v := range m { + if !strings.HasPrefix(k, path) { + continue + } + + childName := strings.Split(k[len(path):], "/")[0] + if result[childName] == nil { + result[childName] = map[string]*Policy{} + } + + result[childName][k] = v + } + + return result +} diff --git a/snapshot/policy/policy_tree_test.go b/snapshot/policy/policy_tree_test.go new file mode 100644 index 000000000..a06ec9741 --- /dev/null +++ b/snapshot/policy/policy_tree_test.go @@ -0,0 +1,124 @@ +package policy + +import ( + "fmt" + "reflect" + "testing" +) + +var ( + defaultPolicy = &Policy{ + FilesPolicy: FilesPolicy{ + IgnoreRules: []string{"default"}, + }, + } + policyA = &Policy{ + FilesPolicy: FilesPolicy{ + IgnoreRules: []string{"a"}, + }, + } + policyB = &Policy{ + FilesPolicy: FilesPolicy{ + IgnoreRules: []string{"b"}, + }, + } + policyC = &Policy{ + FilesPolicy: FilesPolicy{ + IgnoreRules: []string{"c"}, + }, + } +) + +func TestTreeChild(t *testing.T) { + complexTree := &Tree{ + effective: policyA, + children: map[string]*Tree{ + "foo": { + effective: policyB, + children: map[string]*Tree{ + "xxx": { + effective: policyC, + }, + "yyy": { + effective: policyB, + }, + "zzz": { + effective: policyA, + }, + }, + }, + "bar": { + effective: policyC, + }, + }, + } + + cases := []struct { + n *Tree + path string + wantPolicy *Policy + wantInherited bool + }{ + {nil, "blah", DefaultPolicy, true}, + {&Tree{effective: policyA}, "blah", policyA, true}, + {complexTree, "", policyA, false}, + {complexTree, "foo", policyB, false}, + {complexTree, "foo/anything", policyB, true}, + {complexTree, "foo/xxx", policyC, false}, + {complexTree, "foo/xxx/child/grand/child", policyC, true}, + {complexTree, "foo/yyy", policyB, false}, + {complexTree, "foo/yyy/child", policyB, true}, + {complexTree, "foo/zzz", policyA, false}, + {complexTree, "foo/zzz/child", policyA, true}, + {complexTree, "bar", policyC, false}, + {complexTree, "bar1", policyA, true}, + } + + for _, tc := range cases { + verifyTreePolicy(t, tc.n, tc.path, tc.wantPolicy, tc.wantInherited) + } +} + +func TestBuildTree(t *testing.T) { + n := BuildTree(map[string]*Policy{ + ".": policyA, + "./foo": policyB, + "./bar/baz/bleh": policyC, + }, defaultPolicy) + + dumpTree(n, "root") + + verifyTreePolicy(t, n, "", policyA, false) + verifyTreePolicy(t, n, ".", policyA, false) + verifyTreePolicy(t, n, "./foo", policyB, false) + verifyTreePolicy(t, n, "foo/.", policyB, false) + verifyTreePolicy(t, n, "foo/bar", policyB, true) + verifyTreePolicy(t, n, "./foo/./././bar", policyB, true) + verifyTreePolicy(t, n, "not-foo", policyA, true) + verifyTreePolicy(t, n, "bar", policyA, true) + verifyTreePolicy(t, n, "bar/./baz", policyA, true) + verifyTreePolicy(t, n, "bar/baz/bleh/././.", policyC, false) + verifyTreePolicy(t, n, "bar/baz/bleh/./././x", policyC, true) +} + +func verifyTreePolicy(t *testing.T, n *Tree, path string, wantPolicy *Policy, wantInherited bool) { + t.Helper() + + c := n.Child(path) + + if got, want := c.EffectivePolicy(), wantPolicy; !reflect.DeepEqual(got, want) { + t.Errorf("invalid policy for %q: %v, want %v", path, got, want) + } + + if got, want := c.IsInherited(), wantInherited; got != want { + t.Errorf("invalid child 'inherited' result for %q, got %v want %v", path, got, want) + } +} + +func dumpTree(n *Tree, prefix string) { + fmt.Println(prefix + ".policy: " + n.effective.FilesPolicy.IgnoreRules[0]) + + for cname, cnode := range n.children { + dumpTree(cnode, prefix+"."+cname) + } +}