Single-key matching behavior in generic.Matcher

This commit is contained in:
Daniel Smith
2015-04-02 11:11:00 -07:00
parent 3d1dfd47ee
commit 0c2d3ffe68
9 changed files with 243 additions and 141 deletions

View File

@@ -62,13 +62,10 @@ type Etcd struct {
// Used for listing/watching; should not include trailing "/"
KeyRootFunc func(ctx api.Context) string
// Called for Create/Update/Get/Delete
// Called for Create/Update/Get/Delete. Note that 'namespace' can be
// gotten from ctx.
KeyFunc func(ctx api.Context, name string) (string, error)
// If field.Selector of Watch contains a label with such name, this will be
// translated to watching a single object (not all objects of that type).
WatchSingleFieldName string
// Called to get the name of an object
ObjectNameFunc func(obj runtime.Object) (string, error)
@@ -407,30 +404,32 @@ func (e *Etcd) finalizeDelete(obj runtime.Object, runHooks bool) (runtime.Object
return &api.Status{Status: api.StatusSuccess}, nil
}
// WatchPredicate starts a watch for the items that m matches.
// Watch makes a matcher for the given label and field, and calls
// WatchPredicate. If possible, you should customize PredicateFunc to produre a
// matcher that matches by key.
func (e *Etcd) Watch(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) {
if value, found := field.RequiresExactMatch(e.WatchSingleFieldName); found && len(e.WatchSingleFieldName) > 0 {
key, err := e.KeyFunc(ctx, value)
if err != nil {
return nil, err
}
return e.watchPredicate(key, e.PredicateFunc(label, field), resourceVersion)
}
return e.watchPredicate(e.KeyRootFunc(ctx), e.PredicateFunc(label, field), resourceVersion)
return e.WatchPredicate(ctx, e.PredicateFunc(label, field), resourceVersion)
}
// WatchPredicate starts a watch for the items that m matches.
// TODO: Detect if m references a single object instead of a list.
func (e *Etcd) WatchPredicate(ctx api.Context, m generic.Matcher, resourceVersion string) (watch.Interface, error) {
return e.watchPredicate(e.KeyRootFunc(ctx), m, resourceVersion)
}
func (e *Etcd) watchPredicate(key string, m generic.Matcher, resourceVersion string) (watch.Interface, error) {
version, err := tools.ParseWatchResourceVersion(resourceVersion, e.EndpointName)
if err != nil {
return nil, err
}
return e.Helper.WatchList(key, version, func(obj runtime.Object) bool {
var watchKey string
if name, ok := m.MatchesSingle(); ok {
key, err := e.KeyFunc(ctx, name)
if err != nil {
return nil, err
}
watchKey = key
} else {
watchKey = e.KeyRootFunc(ctx)
}
return e.Helper.WatchList(watchKey, version, func(obj runtime.Object) bool {
matches, err := m.Matches(obj)
if err != nil {
glog.Errorf("unable to match watch: %v", err)

View File

@@ -83,13 +83,13 @@ func NewTestGenericEtcdRegistry(t *testing.T) (*tools.FakeEtcdClient, *Etcd) {
}
}
// SetMatcher is a matcher that matches any pod with id in the set.
// setMatcher is a matcher that matches any pod with id in the set.
// Makes testing simpler.
type SetMatcher struct {
type setMatcher struct {
util.StringSet
}
func (sm SetMatcher) Matches(obj runtime.Object) (bool, error) {
func (sm setMatcher) Matches(obj runtime.Object) (bool, error) {
pod, ok := obj.(*api.Pod)
if !ok {
return false, fmt.Errorf("wrong object")
@@ -97,13 +97,25 @@ func (sm SetMatcher) Matches(obj runtime.Object) (bool, error) {
return sm.Has(pod.Name), nil
}
// EverythingMatcher matches everything
type EverythingMatcher struct{}
func (sm setMatcher) MatchesSingle() (string, bool) {
if sm.Len() == 1 {
// Since pod name is its key, we can optimize this case.
return sm.List()[0], true
}
return "", false
}
func (EverythingMatcher) Matches(obj runtime.Object) (bool, error) {
// everythingMatcher matches everything
type everythingMatcher struct{}
func (everythingMatcher) Matches(obj runtime.Object) (bool, error) {
return true, nil
}
func (everythingMatcher) MatchesSingle() (string, bool) {
return "", false
}
func TestEtcdList(t *testing.T) {
podA := &api.Pod{
ObjectMeta: api.ObjectMeta{Name: "foo"},
@@ -138,7 +150,7 @@ func TestEtcdList(t *testing.T) {
},
E: nil,
},
m: EverythingMatcher{},
m: everythingMatcher{},
out: &api.PodList{Items: []api.Pod{}},
succeed: true,
},
@@ -147,7 +159,7 @@ func TestEtcdList(t *testing.T) {
R: &etcd.Response{},
E: tools.EtcdErrorNotFound,
},
m: EverythingMatcher{},
m: everythingMatcher{},
out: &api.PodList{Items: []api.Pod{}},
succeed: true,
},
@@ -156,7 +168,7 @@ func TestEtcdList(t *testing.T) {
R: normalListResp,
E: nil,
},
m: EverythingMatcher{},
m: everythingMatcher{},
out: &api.PodList{Items: []api.Pod{*podA, *podB}},
succeed: true,
},
@@ -165,7 +177,16 @@ func TestEtcdList(t *testing.T) {
R: normalListResp,
E: nil,
},
m: SetMatcher{util.NewStringSet("foo")},
m: setMatcher{util.NewStringSet("foo")},
out: &api.PodList{Items: []api.Pod{*podA}},
succeed: true,
},
"normalFilteredMatchMultiple": {
in: tools.EtcdResponseWithError{
R: normalListResp,
E: nil,
},
m: setMatcher{util.NewStringSet("foo", "makeMatchSingleReturnFalse")},
out: &api.PodList{Items: []api.Pod{*podA}},
succeed: true,
},
@@ -648,36 +669,45 @@ func TestEtcdDelete(t *testing.T) {
}
func TestEtcdWatch(t *testing.T) {
podA := &api.Pod{
ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "1"},
Spec: api.PodSpec{Host: "machine"},
}
respWithPodA := &etcd.Response{
Node: &etcd.Node{
Value: runtime.EncodeOrDie(testapi.Codec(), podA),
ModifiedIndex: 1,
CreatedIndex: 1,
},
Action: "create",
table := map[string]generic.Matcher{
"single": setMatcher{util.NewStringSet("foo")},
"multi": setMatcher{util.NewStringSet("foo", "bar")},
}
fakeClient, registry := NewTestGenericEtcdRegistry(t)
wi, err := registry.WatchPredicate(api.NewContext(), EverythingMatcher{}, "1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
fakeClient.WaitForWatchCompletion()
for name, m := range table {
podA := &api.Pod{
ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "1"},
Spec: api.PodSpec{Host: "machine"},
}
respWithPodA := &etcd.Response{
Node: &etcd.Node{
Value: runtime.EncodeOrDie(testapi.Codec(), podA),
ModifiedIndex: 1,
CreatedIndex: 1,
},
Action: "create",
}
go func() {
fakeClient.WatchResponse <- respWithPodA
}()
fakeClient, registry := NewTestGenericEtcdRegistry(t)
wi, err := registry.WatchPredicate(api.NewContext(), m, "1")
if err != nil {
t.Errorf("%v: unexpected error: %v", name, err)
continue
}
fakeClient.WaitForWatchCompletion()
got, open := <-wi.ResultChan()
if !open {
t.Fatalf("unexpected channel close")
}
go func() {
fakeClient.WatchResponse <- respWithPodA
}()
if e, a := podA, got.Object; !api.Semantic.DeepDerivative(e, a) {
t.Errorf("difference: %s", util.ObjectDiff(e, a))
got, open := <-wi.ResultChan()
if !open {
t.Errorf("%v: unexpected channel close", name)
continue
}
if e, a := podA, got.Object; !api.Semantic.DeepDerivative(e, a) {
t.Errorf("%v: difference: %s", name, util.ObjectDiff(e, a))
}
}
}

View File

@@ -49,14 +49,37 @@ func (s *SelectionPredicate) Matches(obj runtime.Object) (bool, error) {
return s.Label.Matches(labels) && s.Field.Matches(fields), nil
}
// MatchesSingle will return (name, true) iff s.Field matches on the object's
// name.
func (s *SelectionPredicate) MatchesSingle() (string, bool) {
// TODO: should be namespace.name
if name, ok := s.Field.RequiresExactMatch("name"); ok {
return name, true
}
return "", false
}
// Matcher can return true if an object matches the Matcher's selection
// criteria.
// criteria. If it is known that the matcher will match only a single object
// then MatchesSingle should return the key of that object and true. This is an
// optimization only--Matches() should continue to work.
type Matcher interface {
Matches(obj runtime.Object) (bool, error)
// Matches should return true if obj matches this matcher's requirements.
Matches(obj runtime.Object) (matchesThisObject bool, err error)
// If this matcher matches a single object, return the key for that
// object and true here. This will greatly increase efficiency. You
// must still implement Matches(). Note that key does NOT need to
// include the object's namespace.
MatchesSingle() (key string, matchesSingleObject bool)
// TODO: when we start indexing objects, add something like the below:
// MatchesIndices() (indexName []string, indexValue []string)
// where indexName/indexValue are the same length.
}
// MatcherFunc makes a matcher from the provided function. For easy definition
// of matchers for testing.
// of matchers for testing. Note: use SelectionPredicate above for real code!
func MatcherFunc(f func(obj runtime.Object) (bool, error)) Matcher {
return matcherFunc(f)
}
@@ -68,6 +91,36 @@ func (m matcherFunc) Matches(obj runtime.Object) (bool, error) {
return m(obj)
}
// MatchesSingle always returns "", false-- because this is a predicate
// implementation of Matcher.
func (m matcherFunc) MatchesSingle() (string, bool) {
return "", false
}
// MatchOnKey returns a matcher that will send only the object matching key
// through the matching function f. For testing!
// Note: use SelectionPredicate above for real code!
func MatchOnKey(key string, f func(obj runtime.Object) (bool, error)) Matcher {
return matchKey{key, f}
}
type matchKey struct {
key string
matcherFunc
}
// MatchesSingle always returns its key, true.
func (m matchKey) MatchesSingle() (string, bool) {
return m.key, true
}
var (
// Assert implementations match the interface.
_ = Matcher(matchKey{})
_ = Matcher(&SelectionPredicate{})
_ = Matcher(matcherFunc(nil))
)
// DecoratorFunc can mutate the provided object prior to being returned.
type DecoratorFunc func(obj runtime.Object) error

View File

@@ -44,6 +44,7 @@ func TestSelectionPredicate(t *testing.T) {
fields fields.Set
err error
shouldMatch bool
matchSingleKey string
}{
"A": {
labelSelector: "name=foo",
@@ -66,6 +67,13 @@ func TestSelectionPredicate(t *testing.T) {
fields: fields.Set{"uid": "12345"},
shouldMatch: false,
},
"D": {
fieldSelector: "name=12345",
labels: labels.Set{},
fields: fields.Set{"name": "12345"},
shouldMatch: true,
matchSingleKey: "12345",
},
"error": {
labelSelector: "name=foo",
fieldSelector: "uid=12345",
@@ -98,6 +106,15 @@ func TestSelectionPredicate(t *testing.T) {
if e, a := item.shouldMatch, got; e != a {
t.Errorf("%v: expected %v, got %v", name, e, a)
}
if key := item.matchSingleKey; key != "" {
got, ok := sp.MatchesSingle()
if !ok {
t.Errorf("%v: expected single match", name)
}
if e, a := key, got; e != a {
t.Errorf("%v: expected %v, got %v", name, e, a)
}
}
}
}
@@ -118,16 +135,18 @@ func TestFilterList(t *testing.T) {
},
}
got, err := FilterList(try,
MatcherFunc(func(obj runtime.Object) (bool, error) {
i, ok := obj.(*Ignored)
if !ok {
return false, errors.New("wrong type")
}
return i.ID[0] == 'b', nil
}),
nil,
)
m := MatcherFunc(func(obj runtime.Object) (bool, error) {
i, ok := obj.(*Ignored)
if !ok {
return false, errors.New("wrong type")
}
return i.ID[0] == 'b', nil
})
if _, matchesSingleObject := m.MatchesSingle(); matchesSingleObject {
t.Errorf("matcher unexpectedly matches only a single object.")
}
got, err := FilterList(try, m, nil)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
@@ -136,3 +155,14 @@ func TestFilterList(t *testing.T) {
t.Errorf("Expected %#v, got %#v", e, a)
}
}
func TestSingleMatch(t *testing.T) {
m := MatchOnKey("pod-name-here", func(obj runtime.Object) (bool, error) { return true, nil })
got, ok := m.MatchesSingle()
if !ok {
t.Errorf("Expected MatchesSingle to return true")
}
if e, a := "pod-name-here", got; e != a {
t.Errorf("Expected %#v, got %#v", e, a)
}
}