diff --git a/consul/state/catalog.go b/consul/state/catalog.go index b5bcbd780d..1f790ab692 100644 --- a/consul/state/catalog.go +++ b/consul/state/catalog.go @@ -198,11 +198,8 @@ func (s *StateStore) Nodes() (uint64, structs.Nodes, error) { 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) { - if len(filters) > 1 { - return 0, nil, fmt.Errorf("multiple meta filters not supported") - } tx := s.db.Txn(false) defer tx.Abort() @@ -213,6 +210,7 @@ func (s *StateStore) NodesByMeta(filters map[string]string) (uint64, structs.Nod var args []interface{} for key, value := range filters { args = append(args, key, value) + break } nodes, err := tx.Get("nodes", "meta", args...) if err != nil { @@ -222,7 +220,10 @@ func (s *StateStore) NodesByMeta(filters map[string]string) (uint64, structs.Nod // Create and return the nodes list. var results structs.Nodes 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 } @@ -437,11 +438,8 @@ func (s *StateStore) Services() (uint64, structs.Services, error) { 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) { - if len(filters) > 1 { - return 0, nil, fmt.Errorf("multiple meta filters not supported") - } tx := s.db.Txn(false) defer tx.Abort() @@ -452,6 +450,7 @@ func (s *StateStore) ServicesByNodeMeta(filters map[string]string) (uint64, stru var args []interface{} for key, value := range filters { args = append(args, key, value) + break } nodes, err := tx.Get("nodes", "meta", args...) if err != nil { @@ -462,6 +461,9 @@ func (s *StateStore) ServicesByNodeMeta(filters map[string]string) (uint64, stru unique := make(map[string]map[string]struct{}) for node := nodes.Next(); node != nil; node = nodes.Next() { n := node.(*structs.Node) + if len(filters) > 1 && !structs.SatisfiesMetaFilters(n.Meta, filters) { + continue + } // List all the services on the node services, err := tx.Get("services", "node", n.Node) 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 -// given service ID. The query is performed against a service -// _name_ instead of a service ID. +// given service ID, filtered by the given node metadata values. The query +// 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) { tx := s.db.Txn(false) defer tx.Abort() @@ -921,8 +923,8 @@ func (s *StateStore) ChecksInState(state string) (uint64, structs.HealthChecks, return s.parseChecks(idx, checks) } -// ChecksInState is used to query the state store for all checks -// which are in the provided state. +// ChecksInStateByNodeMeta is used to query the state store for all checks +// 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) { tx := s.db.Txn(false) defer tx.Abort() diff --git a/consul/state/catalog_test.go b/consul/state/catalog_test.go index bccc7482d0..e7614098f4 100644 --- a/consul/state/catalog_test.go +++ b/consul/state/catalog_test.go @@ -545,65 +545,50 @@ func TestStateStore_GetNodesByMeta(t *testing.T) { } // 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"}} - if err := s.EnsureNode(0, node0); err != nil { - t.Fatalf("err: %v", err) - } - node1 := &structs.Node{Node: "node1", Address: "127.0.0.1", Meta: map[string]string{"role": "server", "common": "1"}} - if err := s.EnsureNode(1, node1); err != nil { - t.Fatalf("err: %v", err) + testRegisterNodeWithMeta(t, s, 0, "node0", map[string]string{"role": "client"}) + testRegisterNodeWithMeta(t, s, 1, "node1", map[string]string{"role": "client", "common": "1"}) + testRegisterNodeWithMeta(t, s, 2, "node2", map[string]string{"role": "server", "common": "1"}) + + cases := []struct { + filters map[string]string + 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 - idx, nodes, err := s.NodesByMeta(map[string]string{"role": "client"}) - if err != nil { - t.Fatalf("err: %s", err) - } - if idx != 1 { - t.Fatalf("bad index: %d", idx) - } - - // Only one node was returned - if n := len(nodes); n != 1 { - t.Fatalf("bad node count: %d", n) - } - - // Make sure the node is correct - if nodes[0].CreateIndex != 0 || nodes[0].ModifyIndex != 0 { - t.Fatalf("bad node index: %d, %d", nodes[0].CreateIndex, nodes[0].ModifyIndex) - } - 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) + for _, tc := range cases { + _, result, err := s.NodesByMeta(tc.filters) + if err != nil { + t.Fatalf("bad: %v", err) } - name := fmt.Sprintf("node%d", i) - if node.Node != name { - t.Fatalf("bad: %#v", node) + + if len(result) != len(tc.nodes) { + t.Fatalf("bad: %v %v", result, tc.nodes) } - if v, ok := node.Meta["common"]; !ok || v != "1" { - t.Fatalf("bad: %v", node.Meta) + + for i, node := range result { + if node.Node != tc.nodes[i] { + t.Fatalf("bad: %v %v", node.Node, tc.nodes[i]) + } } } } @@ -976,13 +961,10 @@ func TestStateStore_ServicesByNodeMeta(t *testing.T) { } // 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 { t.Fatalf("err: %s", err) } - if idx != 3 { - t.Fatalf("bad index: %d", idx) - } expected := structs.Services{ "redis": []string{"master", "prod"}, } @@ -992,13 +974,10 @@ func TestStateStore_ServicesByNodeMeta(t *testing.T) { } // 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 { t.Fatalf("err: %s", err) } - if idx != 3 { - t.Fatalf("bad index: %d", idx) - } expected = structs.Services{ "redis": []string{"master", "prod", "slave"}, } @@ -1006,6 +985,29 @@ func TestStateStore_ServicesByNodeMeta(t *testing.T) { if !reflect.DeepEqual(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) { diff --git a/website/source/docs/agent/http/catalog.html.markdown b/website/source/docs/agent/http/catalog.html.markdown index e98a9de208..6d84eea786 100644 --- a/website/source/docs/agent/http/catalog.html.markdown +++ b/website/source/docs/agent/http/catalog.html.markdown @@ -200,7 +200,8 @@ node for the sort. 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`. -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: @@ -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 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: