mirror of
https://github.com/kopia/kopia.git
synced 2026-02-06 12:43:49 -05:00
policy: added sparse policy tree abstraction for quickly retrieving effective policies at runtime when walking a tree
This commit is contained in:
122
snapshot/policy/policy_tree.go
Normal file
122
snapshot/policy/policy_tree.go
Normal file
@@ -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
|
||||
}
|
||||
124
snapshot/policy/policy_tree_test.go
Normal file
124
snapshot/policy/policy_tree_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user