2013-12-11 01:00:48 +00:00
|
|
|
package consul
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2013-12-18 23:03:25 +00:00
|
|
|
"github.com/armon/gomdb"
|
2013-12-19 20:03:57 +00:00
|
|
|
"github.com/hashicorp/consul/consul/structs"
|
2013-12-18 23:03:25 +00:00
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
2013-12-11 01:00:48 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2014-01-08 18:31:20 +00:00
|
|
|
dbNodes = "nodes"
|
|
|
|
dbServices = "services"
|
|
|
|
dbMaxMapSize = 1024 * 1024 * 1024 // 1GB maximum size
|
2013-12-11 01:00:48 +00:00
|
|
|
)
|
|
|
|
|
2013-12-24 21:25:09 +00:00
|
|
|
var (
|
|
|
|
nullSentinel = []byte{0, 0, 0, 0} // used to encode a null value
|
|
|
|
)
|
|
|
|
|
2013-12-11 01:00:48 +00:00
|
|
|
// The StateStore is responsible for maintaining all the Consul
|
|
|
|
// state. It is manipulated by the FSM which maintains consistency
|
|
|
|
// through the use of Raft. The goals of the StateStore are to provide
|
|
|
|
// high concurrency for read operations without blocking writes, and
|
|
|
|
// to provide write availability in the face of reads. The current
|
2013-12-18 23:03:25 +00:00
|
|
|
// implementation uses the Lightning Memory-Mapped Database (MDB).
|
|
|
|
// This gives us Multi-Version Concurrency Control for "free"
|
2013-12-11 01:00:48 +00:00
|
|
|
type StateStore struct {
|
2014-01-08 18:31:20 +00:00
|
|
|
path string
|
|
|
|
env *mdb.Env
|
|
|
|
nodeTable *MDBTable
|
|
|
|
serviceTable *MDBTable
|
|
|
|
tables MDBTables
|
2013-12-11 01:00:48 +00:00
|
|
|
}
|
|
|
|
|
2013-12-18 23:09:38 +00:00
|
|
|
// StateSnapshot is used to provide a point-in-time snapshot
|
|
|
|
// It works by starting a readonly transaction against all tables.
|
|
|
|
type StateSnapshot struct {
|
2014-01-08 18:31:20 +00:00
|
|
|
store *StateStore
|
|
|
|
tx *MDBTxn
|
2013-12-18 23:09:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Close is used to abort the transaction and allow for cleanup
|
|
|
|
func (s *StateSnapshot) Close() error {
|
|
|
|
s.tx.Abort()
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2013-12-11 01:00:48 +00:00
|
|
|
// NewStateStore is used to create a new state store
|
|
|
|
func NewStateStore() (*StateStore, error) {
|
2013-12-18 23:03:25 +00:00
|
|
|
// Create a new temp dir
|
|
|
|
path, err := ioutil.TempDir("", "consul")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2013-12-12 22:41:13 +00:00
|
|
|
|
2013-12-18 23:03:25 +00:00
|
|
|
// Open the env
|
|
|
|
env, err := mdb.NewEnv()
|
2013-12-11 01:00:48 +00:00
|
|
|
if err != nil {
|
2013-12-18 23:03:25 +00:00
|
|
|
return nil, err
|
2013-12-11 01:00:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
s := &StateStore{
|
2013-12-18 23:03:25 +00:00
|
|
|
path: path,
|
|
|
|
env: env,
|
2013-12-11 01:00:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure we can initialize
|
|
|
|
if err := s.initialize(); err != nil {
|
2013-12-18 23:03:25 +00:00
|
|
|
env.Close()
|
|
|
|
os.RemoveAll(path)
|
2013-12-11 01:00:48 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return s, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Close is used to safely shutdown the state store
|
|
|
|
func (s *StateStore) Close() error {
|
2013-12-18 23:03:25 +00:00
|
|
|
s.env.Close()
|
|
|
|
os.RemoveAll(s.path)
|
|
|
|
return nil
|
2013-12-11 01:00:48 +00:00
|
|
|
}
|
|
|
|
|
2013-12-18 23:03:25 +00:00
|
|
|
// initialize is used to setup the store for use
|
2013-12-11 01:00:48 +00:00
|
|
|
func (s *StateStore) initialize() error {
|
2013-12-18 23:03:25 +00:00
|
|
|
// Setup the Env first
|
2014-01-08 00:58:16 +00:00
|
|
|
if err := s.env.SetMaxDBs(mdb.DBI(32)); err != nil {
|
2013-12-18 23:03:25 +00:00
|
|
|
return err
|
2013-12-11 01:00:48 +00:00
|
|
|
}
|
|
|
|
|
2014-01-01 01:43:05 +00:00
|
|
|
// Increase the maximum map size
|
|
|
|
if err := s.env.SetMapSize(dbMaxMapSize); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2013-12-18 23:03:25 +00:00
|
|
|
// Optimize our flags for speed over safety, since the Raft log + snapshots
|
|
|
|
// are durable. We treat this as an ephemeral in-memory DB, since we nuke
|
|
|
|
// the data anyways.
|
|
|
|
var flags uint = mdb.NOMETASYNC | mdb.NOSYNC | mdb.NOTLS
|
|
|
|
if err := s.env.Open(s.path, flags, 0755); err != nil {
|
|
|
|
return err
|
2013-12-11 01:00:48 +00:00
|
|
|
}
|
|
|
|
|
2014-01-08 18:31:20 +00:00
|
|
|
// Setup our tables
|
|
|
|
s.nodeTable = &MDBTable{
|
|
|
|
Env: s.env,
|
|
|
|
Name: dbNodes,
|
|
|
|
Indexes: map[string]*MDBIndex{
|
|
|
|
"id": &MDBIndex{
|
|
|
|
Unique: true,
|
|
|
|
Fields: []string{"Node"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Encoder: func(obj interface{}) []byte {
|
|
|
|
buf, err := structs.Encode(255, obj)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return buf[1:]
|
|
|
|
},
|
|
|
|
Decoder: func(buf []byte) interface{} {
|
|
|
|
out := new(structs.Node)
|
|
|
|
if err := structs.Decode(buf, out); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return out
|
|
|
|
},
|
|
|
|
}
|
|
|
|
if err := s.nodeTable.Init(); err != nil {
|
2013-12-18 23:03:25 +00:00
|
|
|
return err
|
2013-12-11 01:00:48 +00:00
|
|
|
}
|
2013-12-11 22:03:09 +00:00
|
|
|
|
2014-01-08 18:31:20 +00:00
|
|
|
s.serviceTable = &MDBTable{
|
|
|
|
Env: s.env,
|
|
|
|
Name: dbServices,
|
|
|
|
Indexes: map[string]*MDBIndex{
|
|
|
|
"id": &MDBIndex{
|
|
|
|
Unique: true,
|
|
|
|
Fields: []string{"Node", "ServiceID"},
|
|
|
|
},
|
|
|
|
"service": &MDBIndex{
|
|
|
|
AllowBlank: true,
|
|
|
|
Fields: []string{"ServiceName", "ServiceTag"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Encoder: func(obj interface{}) []byte {
|
|
|
|
buf, err := structs.Encode(255, obj)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return buf[1:]
|
|
|
|
},
|
|
|
|
Decoder: func(buf []byte) interface{} {
|
|
|
|
out := new(structs.ServiceNode)
|
|
|
|
if err := structs.Decode(buf, out); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return out
|
|
|
|
},
|
|
|
|
}
|
|
|
|
if err := s.serviceTable.Init(); err != nil {
|
|
|
|
return err
|
2013-12-11 22:03:09 +00:00
|
|
|
}
|
2013-12-18 23:03:25 +00:00
|
|
|
|
2014-01-08 18:31:20 +00:00
|
|
|
// Store the set of tables
|
|
|
|
s.tables = []*MDBTable{s.nodeTable, s.serviceTable}
|
|
|
|
return nil
|
2013-12-11 22:03:09 +00:00
|
|
|
}
|
|
|
|
|
2013-12-18 23:03:25 +00:00
|
|
|
// EnsureNode is used to ensure a given node exists, with the provided address
|
2014-01-08 18:31:20 +00:00
|
|
|
func (s *StateStore) EnsureNode(node structs.Node) error {
|
|
|
|
return s.nodeTable.Insert(node)
|
2013-12-11 22:03:09 +00:00
|
|
|
}
|
|
|
|
|
2013-12-11 22:27:27 +00:00
|
|
|
// GetNode returns all the address of the known and if it was found
|
|
|
|
func (s *StateStore) GetNode(name string) (bool, string) {
|
2014-01-08 18:31:20 +00:00
|
|
|
res, err := s.nodeTable.Get("id", name)
|
2013-12-18 23:03:25 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(fmt.Errorf("Failed to get node: %v", err))
|
|
|
|
}
|
2014-01-08 18:31:20 +00:00
|
|
|
if len(res) == 0 {
|
2013-12-18 23:03:25 +00:00
|
|
|
return false, ""
|
2013-12-11 22:27:27 +00:00
|
|
|
}
|
2014-01-08 18:31:20 +00:00
|
|
|
return true, res[0].(*structs.Node).Address
|
2013-12-11 22:27:27 +00:00
|
|
|
}
|
|
|
|
|
2013-12-11 22:03:09 +00:00
|
|
|
// GetNodes returns all the known nodes, the slice alternates between
|
|
|
|
// the node name and address
|
2014-01-08 18:31:20 +00:00
|
|
|
func (s *StateStore) Nodes() structs.Nodes {
|
|
|
|
res, err := s.nodeTable.Get("id")
|
2013-12-11 22:03:09 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(fmt.Errorf("Failed to get nodes: %v", err))
|
|
|
|
}
|
2014-01-08 18:31:20 +00:00
|
|
|
results := make([]structs.Node, len(res))
|
|
|
|
for i, r := range res {
|
|
|
|
results[i] = *r.(*structs.Node)
|
2013-12-11 22:03:09 +00:00
|
|
|
}
|
2014-01-08 18:31:20 +00:00
|
|
|
return results
|
2013-12-11 22:03:09 +00:00
|
|
|
}
|
2013-12-11 22:27:27 +00:00
|
|
|
|
|
|
|
// EnsureService is used to ensure a given node exposes a service
|
2014-01-06 22:18:38 +00:00
|
|
|
func (s *StateStore) EnsureService(name, id, service, tag string, port int) error {
|
2014-01-08 18:31:20 +00:00
|
|
|
// Ensure the node exists
|
|
|
|
res, err := s.nodeTable.Get("id", name)
|
2013-12-18 23:03:25 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2014-01-08 18:31:20 +00:00
|
|
|
if len(res) == 0 {
|
|
|
|
return fmt.Errorf("Missing node registration")
|
2013-12-18 23:03:25 +00:00
|
|
|
}
|
|
|
|
|
2014-01-08 18:31:20 +00:00
|
|
|
// Create the entry
|
|
|
|
entry := structs.ServiceNode{
|
2013-12-18 23:03:25 +00:00
|
|
|
Node: name,
|
2014-01-06 22:18:38 +00:00
|
|
|
ServiceID: id,
|
2014-01-08 18:31:20 +00:00
|
|
|
ServiceName: service,
|
2013-12-18 23:03:25 +00:00
|
|
|
ServiceTag: tag,
|
|
|
|
ServicePort: port,
|
|
|
|
}
|
|
|
|
|
2014-01-08 18:31:20 +00:00
|
|
|
// Ensure the service entry is set
|
|
|
|
return s.serviceTable.Insert(&entry)
|
2013-12-11 22:27:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NodeServices is used to return all the services of a given node
|
2014-01-03 01:29:39 +00:00
|
|
|
func (s *StateStore) NodeServices(name string) *structs.NodeServices {
|
2014-01-08 18:31:20 +00:00
|
|
|
tx, err := s.tables.StartTxn(true)
|
2013-12-18 23:03:25 +00:00
|
|
|
if err != nil {
|
2014-01-08 18:31:20 +00:00
|
|
|
panic(fmt.Errorf("Failed to start txn: %v", err))
|
2013-12-18 23:03:25 +00:00
|
|
|
}
|
|
|
|
defer tx.Abort()
|
2014-01-08 18:31:20 +00:00
|
|
|
return s.parseNodeServices(tx, name)
|
2013-12-18 23:03:25 +00:00
|
|
|
}
|
|
|
|
|
2014-01-08 18:31:20 +00:00
|
|
|
// parseNodeServices is used to get the services belonging to a
|
|
|
|
// node, using a given txn
|
|
|
|
func (s *StateStore) parseNodeServices(tx *MDBTxn, name string) *structs.NodeServices {
|
|
|
|
ns := &structs.NodeServices{
|
|
|
|
Services: make(map[string]*structs.NodeService),
|
|
|
|
}
|
2013-12-12 22:41:13 +00:00
|
|
|
|
2014-01-08 18:31:20 +00:00
|
|
|
// Get the node first
|
|
|
|
res, err := s.nodeTable.GetTxn(tx, "id", name)
|
2013-12-11 22:27:27 +00:00
|
|
|
if err != nil {
|
2014-01-08 18:31:20 +00:00
|
|
|
panic(fmt.Errorf("Failed to get node: %v", err))
|
|
|
|
}
|
|
|
|
if len(res) == 0 {
|
|
|
|
return ns
|
2013-12-11 22:27:27 +00:00
|
|
|
}
|
|
|
|
|
2014-01-08 18:31:20 +00:00
|
|
|
// Set the address
|
|
|
|
node := res[0].(*structs.Node)
|
|
|
|
ns.Address = node.Address
|
2013-12-18 23:03:25 +00:00
|
|
|
|
2014-01-08 18:31:20 +00:00
|
|
|
// Get the services
|
|
|
|
res, err = s.serviceTable.GetTxn(tx, "id", name)
|
|
|
|
if err != nil {
|
|
|
|
panic(fmt.Errorf("Failed to get node: %v", err))
|
|
|
|
}
|
2013-12-18 23:03:25 +00:00
|
|
|
|
2014-01-08 18:31:20 +00:00
|
|
|
// Add each service
|
|
|
|
for _, r := range res {
|
|
|
|
service := r.(*structs.ServiceNode)
|
|
|
|
srv := &structs.NodeService{
|
|
|
|
ID: service.ServiceID,
|
|
|
|
Service: service.ServiceName,
|
|
|
|
Tag: service.ServiceTag,
|
|
|
|
Port: service.ServicePort,
|
2013-12-18 23:03:25 +00:00
|
|
|
}
|
2014-01-08 18:31:20 +00:00
|
|
|
ns.Services[srv.ID] = srv
|
2013-12-11 22:27:27 +00:00
|
|
|
}
|
2014-01-03 01:29:39 +00:00
|
|
|
return ns
|
2013-12-11 22:27:27 +00:00
|
|
|
}
|
2013-12-11 23:34:10 +00:00
|
|
|
|
|
|
|
// DeleteNodeService is used to delete a node service
|
2014-01-06 22:18:38 +00:00
|
|
|
func (s *StateStore) DeleteNodeService(node, id string) error {
|
2014-01-08 18:31:20 +00:00
|
|
|
_, err := s.serviceTable.Delete("id", node, id)
|
|
|
|
return err
|
2013-12-11 23:34:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteNode is used to delete a node and all it's services
|
|
|
|
func (s *StateStore) DeleteNode(node string) error {
|
2014-01-08 18:31:20 +00:00
|
|
|
if _, err := s.serviceTable.Delete("id", node); err != nil {
|
2013-12-18 23:03:25 +00:00
|
|
|
return err
|
|
|
|
}
|
2014-01-08 18:31:20 +00:00
|
|
|
if _, err := s.nodeTable.Delete("id", node); err != nil {
|
|
|
|
return err
|
2013-12-18 23:03:25 +00:00
|
|
|
}
|
2014-01-08 18:31:20 +00:00
|
|
|
return nil
|
2013-12-11 23:34:10 +00:00
|
|
|
}
|
2013-12-12 19:07:14 +00:00
|
|
|
|
|
|
|
// Services is used to return all the services with a list of associated tags
|
|
|
|
func (s *StateStore) Services() map[string][]string {
|
2014-01-08 18:31:20 +00:00
|
|
|
// TODO: Optimize to not table scan.. We can do a distinct
|
|
|
|
// type of query to avoid this
|
|
|
|
res, err := s.serviceTable.Get("id")
|
2013-12-18 23:03:25 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(fmt.Errorf("Failed to get node servicess: %v", err))
|
|
|
|
}
|
2013-12-12 19:07:14 +00:00
|
|
|
services := make(map[string][]string)
|
2014-01-08 18:31:20 +00:00
|
|
|
for _, r := range res {
|
|
|
|
srv := r.(*structs.ServiceNode)
|
|
|
|
|
|
|
|
tags := services[srv.ServiceName]
|
|
|
|
if !strContains(tags, srv.ServiceTag) {
|
|
|
|
tags = append(tags, srv.ServiceTag)
|
|
|
|
services[srv.ServiceName] = tags
|
2013-12-18 23:03:25 +00:00
|
|
|
}
|
2013-12-12 19:07:14 +00:00
|
|
|
}
|
|
|
|
return services
|
|
|
|
}
|
2013-12-12 19:37:19 +00:00
|
|
|
|
|
|
|
// ServiceNodes returns the nodes associated with a given service
|
2013-12-19 20:03:57 +00:00
|
|
|
func (s *StateStore) ServiceNodes(service string) structs.ServiceNodes {
|
2014-01-08 18:31:20 +00:00
|
|
|
tx, err := s.tables.StartTxn(true)
|
2013-12-18 23:03:25 +00:00
|
|
|
if err != nil {
|
2014-01-08 18:31:20 +00:00
|
|
|
panic(fmt.Errorf("Failed to start txn: %v", err))
|
2013-12-18 23:03:25 +00:00
|
|
|
}
|
|
|
|
defer tx.Abort()
|
2014-01-08 18:31:20 +00:00
|
|
|
|
|
|
|
res, err := s.serviceTable.Get("service", service)
|
|
|
|
return parseServiceNodes(tx, s.nodeTable, res, err)
|
2013-12-12 19:37:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ServiceTagNodes returns the nodes associated with a given service matching a tag
|
2013-12-19 20:03:57 +00:00
|
|
|
func (s *StateStore) ServiceTagNodes(service, tag string) structs.ServiceNodes {
|
2014-01-08 18:31:20 +00:00
|
|
|
tx, err := s.tables.StartTxn(true)
|
2013-12-12 19:37:19 +00:00
|
|
|
if err != nil {
|
2014-01-08 18:31:20 +00:00
|
|
|
panic(fmt.Errorf("Failed to start txn: %v", err))
|
2013-12-12 19:37:19 +00:00
|
|
|
}
|
2013-12-18 23:03:25 +00:00
|
|
|
defer tx.Abort()
|
2014-01-08 18:31:20 +00:00
|
|
|
|
|
|
|
res, err := s.serviceTable.Get("service", service, tag)
|
|
|
|
return parseServiceNodes(tx, s.nodeTable, res, err)
|
2013-12-12 19:37:19 +00:00
|
|
|
}
|
2013-12-12 23:14:08 +00:00
|
|
|
|
2013-12-18 23:03:25 +00:00
|
|
|
// parseServiceNodes parses results ServiceNodes and ServiceTagNodes
|
2014-01-08 18:31:20 +00:00
|
|
|
func parseServiceNodes(tx *MDBTxn, table *MDBTable, res []interface{}, err error) structs.ServiceNodes {
|
2013-12-12 23:14:08 +00:00
|
|
|
if err != nil {
|
2013-12-18 23:03:25 +00:00
|
|
|
panic(fmt.Errorf("Failed to get node services: %v", err))
|
2013-12-12 23:14:08 +00:00
|
|
|
}
|
2014-01-08 18:31:20 +00:00
|
|
|
println(fmt.Sprintf("res: %#v", res))
|
2013-12-12 23:14:08 +00:00
|
|
|
|
2014-01-08 18:31:20 +00:00
|
|
|
nodes := make(structs.ServiceNodes, len(res))
|
|
|
|
for i, r := range res {
|
|
|
|
srv := r.(*structs.ServiceNode)
|
2013-12-12 23:14:08 +00:00
|
|
|
|
2014-01-08 18:31:20 +00:00
|
|
|
// Get the address of the node
|
|
|
|
nodeRes, err := table.GetTxn(tx, "id", srv.Node)
|
|
|
|
if err != nil || len(nodeRes) != 1 {
|
|
|
|
panic(fmt.Errorf("Failed to join node: %v", err))
|
2013-12-12 23:14:08 +00:00
|
|
|
}
|
2014-01-08 18:31:20 +00:00
|
|
|
srv.Address = nodeRes[0].(*structs.Node).Address
|
2013-12-12 23:14:08 +00:00
|
|
|
|
2014-01-08 18:31:20 +00:00
|
|
|
nodes[i] = *srv
|
2013-12-12 23:14:08 +00:00
|
|
|
}
|
2014-01-08 18:31:20 +00:00
|
|
|
|
2013-12-18 23:03:25 +00:00
|
|
|
return nodes
|
|
|
|
}
|
2013-12-12 23:14:08 +00:00
|
|
|
|
2013-12-18 23:03:25 +00:00
|
|
|
// Snapshot is used to create a point in time snapshot
|
2013-12-18 23:09:38 +00:00
|
|
|
func (s *StateStore) Snapshot() (*StateSnapshot, error) {
|
2014-01-08 18:31:20 +00:00
|
|
|
// Begin a new txn on all tables
|
|
|
|
tx, err := s.tables.StartTxn(true)
|
2013-12-18 23:09:38 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return the snapshot
|
|
|
|
snap := &StateSnapshot{
|
2014-01-08 18:31:20 +00:00
|
|
|
store: s,
|
|
|
|
tx: tx,
|
2013-12-18 23:09:38 +00:00
|
|
|
}
|
|
|
|
return snap, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Nodes returns all the known nodes, the slice alternates between
|
|
|
|
// the node name and address
|
2014-01-08 18:31:20 +00:00
|
|
|
func (s *StateSnapshot) Nodes() structs.Nodes {
|
|
|
|
res, err := s.store.nodeTable.GetTxn(s.tx, "id")
|
2013-12-18 23:09:38 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(fmt.Errorf("Failed to get nodes: %v", err))
|
|
|
|
}
|
2014-01-08 18:31:20 +00:00
|
|
|
results := make([]structs.Node, len(res))
|
|
|
|
for i, r := range res {
|
|
|
|
results[i] = *r.(*structs.Node)
|
2013-12-18 23:09:38 +00:00
|
|
|
}
|
2014-01-08 18:31:20 +00:00
|
|
|
return results
|
2013-12-18 23:09:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NodeServices is used to return all the services of a given node
|
2014-01-03 01:29:39 +00:00
|
|
|
func (s *StateSnapshot) NodeServices(name string) *structs.NodeServices {
|
2014-01-08 18:31:20 +00:00
|
|
|
return s.store.parseNodeServices(s.tx, name)
|
2013-12-24 21:25:09 +00:00
|
|
|
}
|