Add support for multiple metadata filters to remaining endpoints

Enabled multiple meta filters for /v1/catalog/nodes and /v1/catalog/services
This commit is contained in:
Kyle Havlovitz 2017-01-13 20:45:34 -05:00
parent 5acd69b4fc
commit 9e696220a8
No known key found for this signature in database
GPG Key ID: 8A5E6B173056AD6C
3 changed files with 84 additions and 78 deletions

View File

@ -198,11 +198,8 @@ func (s *StateStore) Nodes() (uint64, structs.Nodes, error) {
return idx, results, nil return idx, results, nil
} }
// NodesByMeta is used to return all nodes with the given meta key/value pair. // NodesByMeta is used to return all nodes with the given metadata key/value pairs.
func (s *StateStore) NodesByMeta(filters map[string]string) (uint64, structs.Nodes, error) { func (s *StateStore) NodesByMeta(filters map[string]string) (uint64, structs.Nodes, error) {
if len(filters) > 1 {
return 0, nil, fmt.Errorf("multiple meta filters not supported")
}
tx := s.db.Txn(false) tx := s.db.Txn(false)
defer tx.Abort() defer tx.Abort()
@ -213,6 +210,7 @@ func (s *StateStore) NodesByMeta(filters map[string]string) (uint64, structs.Nod
var args []interface{} var args []interface{}
for key, value := range filters { for key, value := range filters {
args = append(args, key, value) args = append(args, key, value)
break
} }
nodes, err := tx.Get("nodes", "meta", args...) nodes, err := tx.Get("nodes", "meta", args...)
if err != nil { if err != nil {
@ -222,7 +220,10 @@ func (s *StateStore) NodesByMeta(filters map[string]string) (uint64, structs.Nod
// Create and return the nodes list. // Create and return the nodes list.
var results structs.Nodes var results structs.Nodes
for node := nodes.Next(); node != nil; node = nodes.Next() { for node := nodes.Next(); node != nil; node = nodes.Next() {
results = append(results, node.(*structs.Node)) n := node.(*structs.Node)
if len(filters) <= 1 || structs.SatisfiesMetaFilters(n.Meta, filters) {
results = append(results, n)
}
} }
return idx, results, nil return idx, results, nil
} }
@ -437,11 +438,8 @@ func (s *StateStore) Services() (uint64, structs.Services, error) {
return idx, results, nil return idx, results, nil
} }
// Services returns all services, filtered by the given node metadata. // ServicesByNodeMeta returns all services, filtered by the given node metadata.
func (s *StateStore) ServicesByNodeMeta(filters map[string]string) (uint64, structs.Services, error) { func (s *StateStore) ServicesByNodeMeta(filters map[string]string) (uint64, structs.Services, error) {
if len(filters) > 1 {
return 0, nil, fmt.Errorf("multiple meta filters not supported")
}
tx := s.db.Txn(false) tx := s.db.Txn(false)
defer tx.Abort() defer tx.Abort()
@ -452,6 +450,7 @@ func (s *StateStore) ServicesByNodeMeta(filters map[string]string) (uint64, stru
var args []interface{} var args []interface{}
for key, value := range filters { for key, value := range filters {
args = append(args, key, value) args = append(args, key, value)
break
} }
nodes, err := tx.Get("nodes", "meta", args...) nodes, err := tx.Get("nodes", "meta", args...)
if err != nil { if err != nil {
@ -462,6 +461,9 @@ func (s *StateStore) ServicesByNodeMeta(filters map[string]string) (uint64, stru
unique := make(map[string]map[string]struct{}) unique := make(map[string]map[string]struct{})
for node := nodes.Next(); node != nil; node = nodes.Next() { for node := nodes.Next(); node != nil; node = nodes.Next() {
n := node.(*structs.Node) n := node.(*structs.Node)
if len(filters) > 1 && !structs.SatisfiesMetaFilters(n.Meta, filters) {
continue
}
// List all the services on the node // List all the services on the node
services, err := tx.Get("services", "node", n.Node) services, err := tx.Get("services", "node", n.Node)
if err != nil { if err != nil {
@ -878,8 +880,8 @@ func (s *StateStore) ServiceChecks(serviceName string) (uint64, structs.HealthCh
} }
// ServiceChecksByNodeMeta is used to get all checks associated with a // ServiceChecksByNodeMeta is used to get all checks associated with a
// given service ID. The query is performed against a service // given service ID, filtered by the given node metadata values. The query
// _name_ instead of a service ID. // is performed against a service _name_ instead of a service ID.
func (s *StateStore) ServiceChecksByNodeMeta(serviceName string, filters map[string]string) (uint64, structs.HealthChecks, error) { func (s *StateStore) ServiceChecksByNodeMeta(serviceName string, filters map[string]string) (uint64, structs.HealthChecks, error) {
tx := s.db.Txn(false) tx := s.db.Txn(false)
defer tx.Abort() defer tx.Abort()
@ -921,8 +923,8 @@ func (s *StateStore) ChecksInState(state string) (uint64, structs.HealthChecks,
return s.parseChecks(idx, checks) return s.parseChecks(idx, checks)
} }
// ChecksInState is used to query the state store for all checks // ChecksInStateByNodeMeta is used to query the state store for all checks
// which are in the provided state. // which are in the provided state, filtered by the given node metadata values.
func (s *StateStore) ChecksInStateByNodeMeta(state string, filters map[string]string) (uint64, structs.HealthChecks, error) { func (s *StateStore) ChecksInStateByNodeMeta(state string, filters map[string]string) (uint64, structs.HealthChecks, error) {
tx := s.db.Txn(false) tx := s.db.Txn(false)
defer tx.Abort() defer tx.Abort()

View File

@ -545,65 +545,50 @@ func TestStateStore_GetNodesByMeta(t *testing.T) {
} }
// Create some nodes in the state store // Create some nodes in the state store
node0 := &structs.Node{Node: "node0", Address: "127.0.0.1", Meta: map[string]string{"role": "client", "common": "1"}} testRegisterNodeWithMeta(t, s, 0, "node0", map[string]string{"role": "client"})
if err := s.EnsureNode(0, node0); err != nil { testRegisterNodeWithMeta(t, s, 1, "node1", map[string]string{"role": "client", "common": "1"})
t.Fatalf("err: %v", err) testRegisterNodeWithMeta(t, s, 2, "node2", map[string]string{"role": "server", "common": "1"})
}
node1 := &structs.Node{Node: "node1", Address: "127.0.0.1", Meta: map[string]string{"role": "server", "common": "1"}} cases := []struct {
if err := s.EnsureNode(1, node1); err != nil { filters map[string]string
t.Fatalf("err: %v", err) nodes []string
}{
// Simple meta filter
{
filters: map[string]string{"role": "server"},
nodes: []string{"node2"},
},
// Common meta filter
{
filters: map[string]string{"common": "1"},
nodes: []string{"node1", "node2"},
},
// Invalid meta filter
{
filters: map[string]string{"invalid": "nope"},
nodes: []string{},
},
// Multiple meta filters
{
filters: map[string]string{"role": "client", "common": "1"},
nodes: []string{"node1"},
},
} }
// Retrieve the node with role=client for _, tc := range cases {
idx, nodes, err := s.NodesByMeta(map[string]string{"role": "client"}) _, result, err := s.NodesByMeta(tc.filters)
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("bad: %v", err)
}
if idx != 1 {
t.Fatalf("bad index: %d", idx)
} }
// Only one node was returned if len(result) != len(tc.nodes) {
if n := len(nodes); n != 1 { t.Fatalf("bad: %v %v", result, tc.nodes)
t.Fatalf("bad node count: %d", n)
} }
// Make sure the node is correct for i, node := range result {
if nodes[0].CreateIndex != 0 || nodes[0].ModifyIndex != 0 { if node.Node != tc.nodes[i] {
t.Fatalf("bad node index: %d, %d", nodes[0].CreateIndex, nodes[0].ModifyIndex) t.Fatalf("bad: %v %v", node.Node, tc.nodes[i])
} }
if nodes[0].Node != "node0" {
t.Fatalf("bad: %#v", nodes[0])
}
if !reflect.DeepEqual(nodes[0].Meta, node0.Meta) {
t.Fatalf("bad: %v != %v", nodes[0].Meta, node0.Meta)
}
// Retrieve both nodes via their common meta field
idx, nodes, err = s.NodesByMeta(map[string]string{"common": "1"})
if err != nil {
t.Fatalf("err: %s", err)
}
if idx != 1 {
t.Fatalf("bad index: %d", idx)
}
// All nodes were returned
if n := len(nodes); n != 2 {
t.Fatalf("bad node count: %d", n)
}
// Make sure the nodes match
for i, node := range nodes {
if node.CreateIndex != uint64(i) || node.ModifyIndex != uint64(i) {
t.Fatalf("bad node index: %d, %d", node.CreateIndex, node.ModifyIndex)
}
name := fmt.Sprintf("node%d", i)
if node.Node != name {
t.Fatalf("bad: %#v", node)
}
if v, ok := node.Meta["common"]; !ok || v != "1" {
t.Fatalf("bad: %v", node.Meta)
} }
} }
} }
@ -976,13 +961,10 @@ func TestStateStore_ServicesByNodeMeta(t *testing.T) {
} }
// Filter the services by the first node's meta value // Filter the services by the first node's meta value
idx, res, err = s.ServicesByNodeMeta(map[string]string{"role": "client"}) _, res, err = s.ServicesByNodeMeta(map[string]string{"role": "client"})
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
if idx != 3 {
t.Fatalf("bad index: %d", idx)
}
expected := structs.Services{ expected := structs.Services{
"redis": []string{"master", "prod"}, "redis": []string{"master", "prod"},
} }
@ -992,13 +974,10 @@ func TestStateStore_ServicesByNodeMeta(t *testing.T) {
} }
// Get all services using the common meta value // Get all services using the common meta value
idx, res, err = s.ServicesByNodeMeta(map[string]string{"common": "1"}) _, res, err = s.ServicesByNodeMeta(map[string]string{"common": "1"})
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
if idx != 3 {
t.Fatalf("bad index: %d", idx)
}
expected = structs.Services{ expected = structs.Services{
"redis": []string{"master", "prod", "slave"}, "redis": []string{"master", "prod", "slave"},
} }
@ -1006,6 +985,29 @@ func TestStateStore_ServicesByNodeMeta(t *testing.T) {
if !reflect.DeepEqual(res, expected) { if !reflect.DeepEqual(res, expected) {
t.Fatalf("bad: %v %v", res, expected) t.Fatalf("bad: %v %v", res, expected)
} }
// Get an empty list for an invalid meta value
_, res, err = s.ServicesByNodeMeta(map[string]string{"invalid": "nope"})
if err != nil {
t.Fatalf("err: %s", err)
}
expected = structs.Services{}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("bad: %v %v", res, expected)
}
// Get the first node's service instance using multiple meta filters
_, res, err = s.ServicesByNodeMeta(map[string]string{"role": "client", "common": "1"})
if err != nil {
t.Fatalf("err: %s", err)
}
expected = structs.Services{
"redis": []string{"master", "prod"},
}
sort.Strings(res["redis"])
if !reflect.DeepEqual(res, expected) {
t.Fatalf("bad: %v %v", res, expected)
}
} }
func TestStateStore_ServiceNodes(t *testing.T) { func TestStateStore_ServiceNodes(t *testing.T) {

View File

@ -200,7 +200,8 @@ node for the sort.
In Consul 0.7.3 and later, the optional `?node-meta=` parameter can be In Consul 0.7.3 and later, the optional `?node-meta=` parameter can be
provided with a desired node metadata key/value pair of the form `key:value`. provided with a desired node metadata key/value pair of the form `key:value`.
This will filter the results to nodes with that pair present. This parameter can be specified multiple times, and will filter the results to
nodes with the specified key/value pair(s).
It returns a JSON body like this: It returns a JSON body like this:
@ -241,7 +242,8 @@ however, the `dc` can be provided using the `?dc=` query parameter.
In Consul 0.7.3 and later, the optional `?node-meta=` parameter can be In Consul 0.7.3 and later, the optional `?node-meta=` parameter can be
provided with a desired node metadata key/value pair of the form `key:value`. provided with a desired node metadata key/value pair of the form `key:value`.
This will filter the results to services with that pair present. This parameter can be specified multiple times, and will filter the results to
services on nodes with the specified key/value pair(s).
It returns a JSON body like this: It returns a JSON body like this: